Skip to content

Commit ee0c328

Browse files
committed
Fallback to using DateTimeOffset.TryParse if normal parsing doesn't work
1 parent 078afb1 commit ee0c328

File tree

2 files changed

+150
-7
lines changed

2 files changed

+150
-7
lines changed

src/Exceptionless.DateTimeExtensions/DateMath.cs

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ public static bool TryParse(string expression, DateTimeOffset relativeBaseTime,
6464

6565
var match = Parser.Match(expression);
6666
if (!match.Success)
67-
return false;
67+
{
68+
return TryParseFallbackDate(expression, relativeBaseTime.Offset, isUpperLimit, out result);
69+
}
6870

6971
return TryParseFromMatch(match, relativeBaseTime, isUpperLimit, out result);
7072
}
@@ -110,7 +112,9 @@ public static bool TryParse(string expression, TimeZoneInfo timeZone, bool isUpp
110112

111113
var match = Parser.Match(expression);
112114
if (!match.Success)
113-
return false;
115+
{
116+
return TryParseFallbackDate(expression, timeZone, isUpperLimit, out result);
117+
}
114118

115119
return TryParseFromMatch(match, timeZone, isUpperLimit, out result);
116120
}
@@ -211,7 +215,87 @@ public static bool IsValidExpression(string expression)
211215
if (String.IsNullOrEmpty(expression))
212216
return false;
213217

214-
return Parser.IsMatch(expression);
218+
if (Parser.IsMatch(expression))
219+
return true;
220+
221+
// Fallback: Check if it's a valid explicit date
222+
return TryParseFallbackDate(expression, TimeZoneInfo.Local, false, out _);
223+
}
224+
225+
/// <summary>
226+
/// Attempts to parse the expression as an explicit date when date math parsing fails, using the provided timezone for missing offsets.
227+
/// </summary>
228+
/// <param name="expression">The original expression to interpret as an explicit date.</param>
229+
/// <param name="defaultTimeZone">The timezone applied when the expression lacks explicit offset information.</param>
230+
/// <param name="isUpperLimit">Whether the value should be treated as an upper bound, rounding end-of-day when applicable.</param>
231+
/// <param name="result">Receives the parsed <see cref="DateTimeOffset"/> when parsing succeeds.</param>
232+
/// <returns><see langword="true"/> when the expression is successfully parsed as an explicit date; otherwise, <see langword="false"/>.</returns>
233+
private static bool TryParseFallbackDate(string expression, TimeZoneInfo defaultTimeZone, bool isUpperLimit, out DateTimeOffset result)
234+
{
235+
if (Regex.IsMatch(expression, @"(Z|[+-]\d{2}:\d{2})$") && DateTimeOffset.TryParse(expression, out DateTimeOffset explicitDate))
236+
{
237+
result = explicitDate;
238+
239+
if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
240+
{
241+
// If time is exactly midnight, and it's an upper limit, set to end of day
242+
result = result.EndOfDay();
243+
}
244+
245+
return true;
246+
}
247+
248+
if (DateTime.TryParse(expression, out DateTime dt))
249+
{
250+
result = new DateTimeOffset(dt, defaultTimeZone.GetUtcOffset(dt));
251+
252+
if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
253+
{
254+
// If time is exactly midnight, and it's an upper limit, set to end of day
255+
result = result.EndOfDay();
256+
}
257+
return true;
258+
}
259+
260+
return false;
261+
}
262+
263+
/// <summary>
264+
/// Attempts to parse the expression as an explicit date when date math parsing fails, using the provided offset for missing timezone information.
265+
/// </summary>
266+
/// <param name="expression">The original expression to interpret as an explicit date.</param>
267+
/// <param name="offset">The fallback UTC offset applied when the expression omits timezone data.</param>
268+
/// <param name="isUpperLimit">Whether the value should be treated as an upper bound, rounding to the end of day when appropriate.</param>
269+
/// <param name="result">Receives the parsed <see cref="DateTimeOffset"/> when parsing succeeds.</param>
270+
/// <returns><see langword="true"/> when the expression is successfully parsed as an explicit date; otherwise, <see langword="false"/>.</returns>
271+
private static bool TryParseFallbackDate(string expression, TimeSpan offset, bool isUpperLimit, out DateTimeOffset result)
272+
{
273+
if (Regex.IsMatch(expression, @"(Z|[+-]\d{2}:\d{2})$") && DateTimeOffset.TryParse(expression, out DateTimeOffset explicitDate))
274+
{
275+
result = explicitDate;
276+
277+
if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
278+
{
279+
// If time is exactly midnight, and it's an upper limit, set to end of day
280+
result = result.EndOfDay();
281+
}
282+
283+
return true;
284+
}
285+
286+
if (DateTime.TryParse(expression, out DateTime dt))
287+
{
288+
result = new DateTimeOffset(dt, offset);
289+
290+
if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
291+
{
292+
// If time is exactly midnight, and it's an upper limit, set to end of day
293+
result = result.EndOfDay();
294+
}
295+
return true;
296+
}
297+
298+
return false;
215299
}
216300

217301
/// <summary>

tests/Exceptionless.DateTimeExtensions.Tests/DateMathTests.cs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ public void Parse_ExplicitDateWithOperations_ReturnsCorrectResult(string express
311311
[InlineData(" ")]
312312
[InlineData("invalid")]
313313
[InlineData("now+1x")] // Invalid unit
314-
[InlineData("2023-01-01")] // Missing ||
315314
[InlineData("||+1d")] // Missing anchor
316315
[InlineData("now/x")] // Invalid rounding unit
317316
[InlineData("2023-13-01||")] // Invalid month
@@ -344,6 +343,7 @@ public void Parse_NullExpression_ThrowsArgumentException()
344343
[InlineData("now")]
345344
[InlineData("now+1h")]
346345
[InlineData("now-1d/d")]
346+
[InlineData("2023-06-15")]
347347
[InlineData("2023-06-15||")]
348348
[InlineData("2023-06-15||+1M/d")]
349349
[InlineData("2025-01-01T01:25:35Z||+3d/d")]
@@ -368,7 +368,6 @@ public void TryParse_ValidExpressions_ReturnsTrueAndCorrectResult(string express
368368
[InlineData("")]
369369
[InlineData("invalid")]
370370
[InlineData("now+")]
371-
[InlineData("2023-01-01")] // Missing ||
372371
[InlineData("||+1d")] // Missing anchor
373372
[InlineData("2001.02.01||")] // Dotted format no longer supported
374373
[InlineData("now/d+1h")] // Rounding must be final operation
@@ -398,9 +397,69 @@ public void TryParse_NullExpression_ReturnsFalse()
398397
Assert.Equal(default, result);
399398
}
400399

400+
[Fact]
401+
public void TryParse_FallbackExplicitDate_AppliesBaseOffset()
402+
{
403+
const string expression = "2023-04-01";
404+
_logger.LogDebug("Testing TryParse fallback with expression: '{Expression}', BaseTime: {BaseTime}", expression, _baseTime);
405+
406+
bool success = DateMath.TryParse(expression, _baseTime, false, out var result);
407+
408+
_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);
409+
410+
Assert.True(success);
411+
var expected = new DateTimeOffset(2023, 4, 1, 0, 0, 0, _baseTime.Offset);
412+
Assert.Equal(expected, result);
413+
}
414+
415+
[Fact]
416+
public void TryParse_FallbackExplicitDateUpperLimit_AdjustsToEndOfDay()
417+
{
418+
const string expression = "2023-07-10";
419+
_logger.LogDebug("Testing TryParse fallback upper limit with expression: '{Expression}', BaseTime: {BaseTime}", expression, _baseTime);
420+
421+
bool success = DateMath.TryParse(expression, _baseTime, true, out var result);
422+
423+
_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);
424+
425+
Assert.True(success);
426+
var expected = new DateTimeOffset(2023, 7, 10, 23, 59, 59, 999, _baseTime.Offset);
427+
Assert.Equal(expected, result);
428+
}
429+
430+
[Fact]
431+
public void TryParse_FallbackExplicitDateWithTimezone_PreservesOffset()
432+
{
433+
const string expression = "2023-05-05T18:45:00-07:00";
434+
_logger.LogDebug("Testing TryParse fallback with explicit offset expression: '{Expression}'", expression);
435+
436+
bool success = DateMath.TryParse(expression, _baseTime, false, out var result);
437+
438+
_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);
439+
440+
Assert.True(success);
441+
Assert.Equal(new DateTimeOffset(2023, 5, 5, 18, 45, 0, TimeSpan.FromHours(-7)), result);
442+
}
443+
444+
[Fact]
445+
public void TryParse_FallbackExplicitDateWithTimeZoneInfo_UsesProvidedOffset()
446+
{
447+
const string expression = "2023-09-15";
448+
var customZone = TimeZoneInfo.CreateCustomTimeZone("TestPlusThree", TimeSpan.FromHours(3), "Test +3", "Test +3");
449+
_logger.LogDebug("Testing TryParse fallback with TimeZoneInfo: '{Expression}', TimeZone: {TimeZone}", expression, customZone);
450+
451+
bool success = DateMath.TryParse(expression, customZone, false, out var result);
452+
453+
_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);
454+
455+
Assert.True(success);
456+
Assert.Equal(new DateTimeOffset(2023, 9, 15, 0, 0, 0, customZone.BaseUtcOffset), result);
457+
}
458+
401459
[Theory]
402460
[InlineData("now+1h", false)]
403461
[InlineData("now-1d/d", true)]
462+
[InlineData("2023-06-15", false)]
404463
[InlineData("2023-06-15||+1M", false)]
405464
[InlineData("2025-01-01T01:25:35Z||+3d/d", true)]
406465
public void Parse_And_TryParse_ReturnSameResults(string expression, bool isUpperLimit)
@@ -556,7 +615,7 @@ public void ParseTimeZone_Now_ReturnsCorrectTimezone(string timeZoneId, int expe
556615
public void ParseTimeZone_ExplicitDateWithoutTimezone_UsesSpecifiedTimezone()
557616
{
558617
var easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("US/Eastern");
559-
const string expression = "2023-06-15T14:30:00||";
618+
const string expression = "2023-06-15T14:30:00";
560619

561620
_logger.LogDebug("Testing Parse with TimeZoneInfo for expression: '{Expression}', TimeZone: {TimeZone}",
562621
expression, easternTimeZone.Id);
@@ -581,7 +640,7 @@ public void ParseTimeZone_ExplicitDateWithoutTimezone_UsesSpecifiedTimezone()
581640
public void ParseTimeZone_ExplicitDateWithTimezone_PreservesOriginalTimezone()
582641
{
583642
var pacificTimeZone = TimeZoneInfo.FindSystemTimeZoneById("US/Pacific");
584-
const string expression = "2023-06-15T14:30:00+05:00||"; // Explicit +05:00 timezone
643+
const string expression = "2023-06-15T14:30:00+05:00"; // Explicit +05:00 timezone
585644

586645
_logger.LogDebug("Testing Parse with TimeZoneInfo for expression: '{Expression}', TimeZone: {TimeZone}",
587646
expression, pacificTimeZone.Id);

0 commit comments

Comments
 (0)