diff --git a/Fluid.Tests/TemplateTests.cs b/Fluid.Tests/TemplateTests.cs index c12637a4..c803486d 100644 --- a/Fluid.Tests/TemplateTests.cs +++ b/Fluid.Tests/TemplateTests.cs @@ -285,6 +285,86 @@ public async Task ShouldEvaluateTimeSpanValue() Assert.Equal("1970-01-01 01:00:00Z", result); } + [Fact] + public async Task ShouldHandleDateTimeMinValueWithPositiveTimezoneOffset() + { + // Set a timezone offset of +2 hours (like EET - Eastern European Time) + var plusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom+2", TimeSpan.FromHours(2), "UTC+2", "UTC+2"); + + _parser.TryParse("{{ foo }} {{ date }}", out var template, out var error); + + var context = new TemplateContext { TimeZone = plusTwoTimezone }; + context.SetValue("foo", "bar"); + context.SetValue("date", DateTime.MinValue); + + // This should not throw ArgumentOutOfRangeException + var result = await template.RenderAsync(context); + + // DateTime.MinValue should be rendered as the minimum DateTimeOffset value + Assert.Contains("bar", result); + Assert.Contains("0001-01-01", result); + } + + [Fact] + public async Task ShouldHandleDateTimeNearMinValueWithPositiveTimezoneOffset() + { + // Set a timezone offset of +2 hours (like EET - Eastern European Time) + var plusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom+2", TimeSpan.FromHours(2), "UTC+2", "UTC+2"); + + _parser.TryParse("{{ foo }} {{ date }}", out var template, out var error); + + var context = new TemplateContext { TimeZone = plusTwoTimezone }; + context.SetValue("foo", "bar"); + context.SetValue("date", DateTime.MinValue.AddHours(1)); + + // This should not throw ArgumentOutOfRangeException even with DateTime.MinValue + 1 hour + var result = await template.RenderAsync(context); + + // DateTime near MinValue should be rendered as the minimum DateTimeOffset value + Assert.Contains("bar", result); + Assert.Contains("0001-01-01", result); + } + + [Fact] + public async Task ShouldHandleDateTimeMaxValueWithNegativeTimezoneOffset() + { + // Set a timezone offset of -2 hours (like Brazil Standard Time) + var minusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom-2", TimeSpan.FromHours(-2), "UTC-2", "UTC-2"); + + _parser.TryParse("{{ foo }} {{ date }}", out var template, out var error); + + var context = new TemplateContext { TimeZone = minusTwoTimezone }; + context.SetValue("foo", "bar"); + context.SetValue("date", DateTime.MaxValue); + + // This should not throw ArgumentOutOfRangeException + var result = await template.RenderAsync(context); + + // DateTime.MaxValue should be rendered as the maximum DateTimeOffset value + Assert.Contains("bar", result); + Assert.Contains("9999-12-31", result); + } + + [Fact] + public async Task ShouldHandleDateTimeNearMaxValueWithNegativeTimezoneOffset() + { + // Set a timezone offset of -2 hours (like Brazil Standard Time) + var minusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom-2", TimeSpan.FromHours(-2), "UTC-2", "UTC-2"); + + _parser.TryParse("{{ foo }} {{ date }}", out var template, out var error); + + var context = new TemplateContext { TimeZone = minusTwoTimezone }; + context.SetValue("foo", "bar"); + context.SetValue("date", DateTime.MaxValue.AddHours(-1)); + + // This should not throw ArgumentOutOfRangeException even with DateTime.MaxValue - 1 hour + var result = await template.RenderAsync(context); + + // DateTime near MaxValue should be rendered as the maximum DateTimeOffset value + Assert.Contains("bar", result); + Assert.Contains("9999-12-31", result); + } + [Fact] public async Task ShouldEvaluateObjectProperty() { diff --git a/Fluid/Values/DateTimeValue.cs b/Fluid/Values/DateTimeValue.cs index 2e1ea48d..a14d8cda 100644 --- a/Fluid/Values/DateTimeValue.cs +++ b/Fluid/Values/DateTimeValue.cs @@ -13,6 +13,45 @@ public DateTimeValue(DateTimeOffset value) _value = value; } + public DateTimeValue(DateTime value) + { + // Handle edge cases where DateTime cannot be safely converted to DateTimeOffset + // with local timezone offset due to overflow (e.g., DateTime.MinValue with positive offset) + + // Check if the value is within one day of the boundaries where overflow might occur + if (value <= DateTime.MinValue.AddDays(1)) + { + // Value is close to MinValue - attempt conversion with try-catch + try + { + _value = value; + } + catch (ArgumentOutOfRangeException) + { + // Offset caused underflow - use minimum boundary + _value = DateTimeOffset.MinValue; + } + } + else if (value >= DateTime.MaxValue.AddDays(-1)) + { + // Value is close to MaxValue - attempt conversion with try-catch + try + { + _value = value; + } + catch (ArgumentOutOfRangeException) + { + // Offset caused overflow - use maximum boundary + _value = DateTimeOffset.MaxValue; + } + } + else + { + // Normal case - direct conversion without try-catch overhead + _value = value; + } + } + public override FluidValues Type => FluidValues.DateTime; public override bool Equals(FluidValue other)