Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions src/Exceptionless.DateTimeExtensions/DateMath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public static class DateMath
// Pre-compiled regex for operation parsing to avoid repeated compilation
private static readonly Regex _operationRegex = new(@"([+\-/])(\d*)([yMwdhHms])", RegexOptions.Compiled);

// Pre-compiled regex for offset parsing to avoid repeated compilation
private static readonly Regex _offsetRegex = new(@"(Z|[+-]\d{2}:\d{2})$", RegexOptions.Compiled);

/// <summary>
/// Parses a date math expression and returns the resulting DateTimeOffset.
/// </summary>
Expand Down Expand Up @@ -64,7 +67,9 @@ public static bool TryParse(string expression, DateTimeOffset relativeBaseTime,

var match = Parser.Match(expression);
if (!match.Success)
return false;
{
return TryParseFallbackDate(expression, relativeBaseTime.Offset, isUpperLimit, out result);
}

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

var match = Parser.Match(expression);
if (!match.Success)
return false;
{
return TryParseFallbackDate(expression, timeZone, isUpperLimit, out result);
}

return TryParseFromMatch(match, timeZone, isUpperLimit, out result);
}
Expand Down Expand Up @@ -211,7 +218,54 @@ public static bool IsValidExpression(string expression)
if (String.IsNullOrEmpty(expression))
return false;

return Parser.IsMatch(expression);
if (Parser.IsMatch(expression))
return true;

// Fallback: Check if it's a valid explicit date
return TryParseFallbackDate(expression, TimeZoneInfo.Local, false, out _);
}

private static bool TryParseFallbackDate(string expression, TimeZoneInfo defaultTimeZone, bool isUpperLimit, out DateTimeOffset result)
{
return TryParseFallbackDateCore(expression, isUpperLimit, out result, defaultTimeZone.GetUtcOffset);
}

private static bool TryParseFallbackDate(string expression, TimeSpan offset, bool isUpperLimit, out DateTimeOffset result)
{
return TryParseFallbackDateCore(expression, isUpperLimit, out result, _ => offset);
}

private static bool TryParseFallbackDateCore(string expression, bool isUpperLimit, out DateTimeOffset result, Func<DateTime, TimeSpan> offsetResolver)
{
result = default;

if (_offsetRegex.IsMatch(expression) && DateTimeOffset.TryParse(expression, out DateTimeOffset explicitDate))
{
result = explicitDate;

if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
{
// If time is exactly midnight, and it's an upper limit, set to end of day
result = result.EndOfDay();
}

return true;
}

if (DateTime.TryParse(expression, out DateTime dt))
{
result = new DateTimeOffset(dt, offsetResolver(dt));

if (result.TimeOfDay == TimeSpan.Zero && isUpperLimit)
{
// If time is exactly midnight, and it's an upper limit, set to end of day
result = result.EndOfDay();
}

return true;
}

return false;
}

/// <summary>
Expand Down
67 changes: 63 additions & 4 deletions tests/Exceptionless.DateTimeExtensions.Tests/DateMathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ public void Parse_ExplicitDateWithOperations_ReturnsCorrectResult(string express
[InlineData(" ")]
[InlineData("invalid")]
[InlineData("now+1x")] // Invalid unit
[InlineData("2023-01-01")] // Missing ||
[InlineData("||+1d")] // Missing anchor
[InlineData("now/x")] // Invalid rounding unit
[InlineData("2023-13-01||")] // Invalid month
Expand Down Expand Up @@ -344,6 +343,7 @@ public void Parse_NullExpression_ThrowsArgumentException()
[InlineData("now")]
[InlineData("now+1h")]
[InlineData("now-1d/d")]
[InlineData("2023-06-15")]
[InlineData("2023-06-15||")]
[InlineData("2023-06-15||+1M/d")]
[InlineData("2025-01-01T01:25:35Z||+3d/d")]
Expand All @@ -368,7 +368,6 @@ public void TryParse_ValidExpressions_ReturnsTrueAndCorrectResult(string express
[InlineData("")]
[InlineData("invalid")]
[InlineData("now+")]
[InlineData("2023-01-01")] // Missing ||
[InlineData("||+1d")] // Missing anchor
[InlineData("2001.02.01||")] // Dotted format no longer supported
[InlineData("now/d+1h")] // Rounding must be final operation
Expand Down Expand Up @@ -398,9 +397,69 @@ public void TryParse_NullExpression_ReturnsFalse()
Assert.Equal(default, result);
}

[Fact]
public void TryParse_FallbackExplicitDate_AppliesBaseOffset()
{
const string expression = "2023-04-01";
_logger.LogDebug("Testing TryParse fallback with expression: '{Expression}', BaseTime: {BaseTime}", expression, _baseTime);

bool success = DateMath.TryParse(expression, _baseTime, false, out var result);

_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);

Assert.True(success);
var expected = new DateTimeOffset(2023, 4, 1, 0, 0, 0, _baseTime.Offset);
Assert.Equal(expected, result);
}

[Fact]
public void TryParse_FallbackExplicitDateUpperLimit_AdjustsToEndOfDay()
{
const string expression = "2023-07-10";
_logger.LogDebug("Testing TryParse fallback upper limit with expression: '{Expression}', BaseTime: {BaseTime}", expression, _baseTime);

bool success = DateMath.TryParse(expression, _baseTime, true, out var result);

_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);

Assert.True(success);
var expected = new DateTimeOffset(2023, 7, 10, 23, 59, 59, 999, _baseTime.Offset);
Assert.Equal(expected, result);
}

[Fact]
public void TryParse_FallbackExplicitDateWithTimezone_PreservesOffset()
{
const string expression = "2023-05-05T18:45:00-07:00";
_logger.LogDebug("Testing TryParse fallback with explicit offset expression: '{Expression}'", expression);

bool success = DateMath.TryParse(expression, _baseTime, false, out var result);

_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);

Assert.True(success);
Assert.Equal(new DateTimeOffset(2023, 5, 5, 18, 45, 0, TimeSpan.FromHours(-7)), result);
}

[Fact]
public void TryParse_FallbackExplicitDateWithTimeZoneInfo_UsesProvidedOffset()
{
const string expression = "2023-09-15";
var customZone = TimeZoneInfo.CreateCustomTimeZone("TestPlusThree", TimeSpan.FromHours(3), "Test +3", "Test +3");
_logger.LogDebug("Testing TryParse fallback with TimeZoneInfo: '{Expression}', TimeZone: {TimeZone}", expression, customZone);

bool success = DateMath.TryParse(expression, customZone, false, out var result);

_logger.LogDebug("TryParse success: {Success}, Result: {Result}", success, result);

Assert.True(success);
Assert.Equal(new DateTimeOffset(2023, 9, 15, 0, 0, 0, customZone.BaseUtcOffset), result);
}

[Theory]
[InlineData("now+1h", false)]
[InlineData("now-1d/d", true)]
[InlineData("2023-06-15", false)]
[InlineData("2023-06-15||+1M", false)]
[InlineData("2025-01-01T01:25:35Z||+3d/d", true)]
public void Parse_And_TryParse_ReturnSameResults(string expression, bool isUpperLimit)
Expand Down Expand Up @@ -556,7 +615,7 @@ public void ParseTimeZone_Now_ReturnsCorrectTimezone(string timeZoneId, int expe
public void ParseTimeZone_ExplicitDateWithoutTimezone_UsesSpecifiedTimezone()
{
var easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("US/Eastern");
const string expression = "2023-06-15T14:30:00||";
const string expression = "2023-06-15T14:30:00";

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

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