Skip to content

Commit cabdd34

Browse files
authored
Fallback to using DateTimeOffset.TryParse if normal parsing doesn't work (#118)
* Fallback to using DateTimeOffset.TryParse if normal parsing doesn't work * Use shared regex * Refactor method
1 parent 078afb1 commit cabdd34

File tree

2 files changed

+120
-7
lines changed

2 files changed

+120
-7
lines changed

src/Exceptionless.DateTimeExtensions/DateMath.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public static class DateMath
3131
// Pre-compiled regex for operation parsing to avoid repeated compilation
3232
private static readonly Regex _operationRegex = new(@"([+\-/])(\d*)([yMwdhHms])", RegexOptions.Compiled);
3333

34+
// Pre-compiled regex for offset parsing to avoid repeated compilation
35+
private static readonly Regex _offsetRegex = new(@"(Z|[+-]\d{2}:\d{2})$", RegexOptions.Compiled);
36+
3437
/// <summary>
3538
/// Parses a date math expression and returns the resulting DateTimeOffset.
3639
/// </summary>
@@ -64,7 +67,9 @@ public static bool TryParse(string expression, DateTimeOffset relativeBaseTime,
6467

6568
var match = Parser.Match(expression);
6669
if (!match.Success)
67-
return false;
70+
{
71+
return TryParseFallbackDate(expression, relativeBaseTime.Offset, isUpperLimit, out result);
72+
}
6873

6974
return TryParseFromMatch(match, relativeBaseTime, isUpperLimit, out result);
7075
}
@@ -110,7 +115,9 @@ public static bool TryParse(string expression, TimeZoneInfo timeZone, bool isUpp
110115

111116
var match = Parser.Match(expression);
112117
if (!match.Success)
113-
return false;
118+
{
119+
return TryParseFallbackDate(expression, timeZone, isUpperLimit, out result);
120+
}
114121

115122
return TryParseFromMatch(match, timeZone, isUpperLimit, out result);
116123
}
@@ -211,7 +218,54 @@ public static bool IsValidExpression(string expression)
211218
if (String.IsNullOrEmpty(expression))
212219
return false;
213220

214-
return Parser.IsMatch(expression);
221+
if (Parser.IsMatch(expression))
222+
return true;
223+
224+
// Fallback: Check if it's a valid explicit date
225+
return TryParseFallbackDate(expression, TimeZoneInfo.Local, false, out _);
226+
}
227+
228+
private static bool TryParseFallbackDate(string expression, TimeZoneInfo defaultTimeZone, bool isUpperLimit, out DateTimeOffset result)
229+
{
230+
return TryParseFallbackDateCore(expression, isUpperLimit, out result, defaultTimeZone.GetUtcOffset);
231+
}
232+
233+
private static bool TryParseFallbackDate(string expression, TimeSpan offset, bool isUpperLimit, out DateTimeOffset result)
234+
{
235+
return TryParseFallbackDateCore(expression, isUpperLimit, out result, _ => offset);
236+
}
237+
238+
private static bool TryParseFallbackDateCore(string expression, bool isUpperLimit, out DateTimeOffset result, Func<DateTime, TimeSpan> offsetResolver)
239+
{
240+
result = default;
241+
242+
if (_offsetRegex.IsMatch(expression) && DateTimeOffset.TryParse(expression, out DateTimeOffset explicitDate))
243+
{
244+
result = explicitDate;
245+
246+
if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
247+
{
248+
// If time is exactly midnight, and it's an upper limit, set to end of day
249+
result = result.EndOfDay();
250+
}
251+
252+
return true;
253+
}
254+
255+
if (DateTime.TryParse(expression, out DateTime dt))
256+
{
257+
result = new DateTimeOffset(dt, offsetResolver(dt));
258+
259+
if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
260+
{
261+
// If time is exactly midnight, and it's an upper limit, set to end of day
262+
result = result.EndOfDay();
263+
}
264+
265+
return true;
266+
}
267+
268+
return false;
215269
}
216270

217271
/// <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)