diff --git a/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/ExplicitDateFormatParser.cs b/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/ExplicitDateFormatParser.cs index 10b0ef2..b00fc6b 100644 --- a/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/ExplicitDateFormatParser.cs +++ b/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/ExplicitDateFormatParser.cs @@ -1,10 +1,11 @@ using System; +using System.Globalization; using System.Text.RegularExpressions; namespace Exceptionless.DateTimeExtensions.FormatParsers { [Priority(30)] public class ExplicitDateFormatParser : IFormatParser { - private static readonly Regex _parser = new(@"^\s*(?\d{4}-\d{2}-\d{2}(?:T(?:\d{2}\:\d{2}\:\d{2}|\d{2}\:\d{2}|\d{2}))?)\s*$"); + private static readonly Regex _parser = new(@"^\s*(?\d{4}-\d{2}-\d{2}(?:T(?:\d{2}\:\d{2}\:\d{2}(?:\.\d{3})?|\d{2}\:\d{2}|\d{2})Z?)?)\s*$"); public DateTimeRange Parse(string content, DateTimeOffset relativeBaseTime) { content = content.Trim(); @@ -18,10 +19,13 @@ public DateTimeRange Parse(string content, DateTimeOffset relativeBaseTime) { if (value.Length == 16) value += ":00"; - if (!DateTimeOffset.TryParse(value, out var date)) + // NOTE: AssumeUniversal here because this might parse a date (E.G., 03/22/2023). If no offset is specified, we assume it's UTC. + if (!DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date)) return null; - date = date.ChangeOffset(relativeBaseTime.Offset); + if (relativeBaseTime.Offset != date.Offset) + date = date.ChangeOffset(relativeBaseTime.Offset); + return content.Length switch { 10 => new DateTimeRange(date, date.EndOfDay()), 13 => new DateTimeRange(date, date.EndOfHour()), diff --git a/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/PartParsers/ExplicitDatePartParser.cs b/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/PartParsers/ExplicitDatePartParser.cs index 608fd58..e01ba51 100644 --- a/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/PartParsers/ExplicitDatePartParser.cs +++ b/src/Exceptionless.DateTimeExtensions/FormatParsers/FormatParsers/PartParsers/ExplicitDatePartParser.cs @@ -1,10 +1,11 @@ using System; +using System.Globalization; using System.Text.RegularExpressions; namespace Exceptionless.DateTimeExtensions.FormatParsers.PartParsers { [Priority(50)] public class ExplicitDatePartParser : IPartParser { - private static readonly Regex _parser = new(@"\G(?\d{4}-\d{2}-\d{2}(?:T(?:\d{2}\:\d{2}\:\d{2}|\d{2}\:\d{2}|\d{2}))?)"); + private static readonly Regex _parser = new(@"\G(?\d{4}-\d{2}-\d{2}(?:T(?:\d{2}\:\d{2}\:\d{2}(?:\.\d{3})?|\d{2}\:\d{2}|\d{2})Z?)?)"); public Regex Regex => _parser; public DateTimeOffset? Parse(Match match, DateTimeOffset relativeBaseTime, bool isUpperLimit) { @@ -14,10 +15,13 @@ public class ExplicitDatePartParser : IPartParser { if (value.Length == 16) value += ":00"; - if (!DateTimeOffset.TryParse(value, out var date)) + // NOTE: AssumeUniversal here because this might parse a date (E.G., 03/22/2023). If no offset is specified, we assume it's UTC. + if (!DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date)) return null; - date = date.ChangeOffset(relativeBaseTime.Offset); + if (relativeBaseTime.Offset != date.Offset) + date = date.ChangeOffset(relativeBaseTime.Offset); + if (!isUpperLimit) return date; diff --git a/tests/Exceptionless.DateTimeExtensions.Tests/DateTimeRangeTests.cs b/tests/Exceptionless.DateTimeExtensions.Tests/DateTimeRangeTests.cs index 400c020..8d77eb4 100644 --- a/tests/Exceptionless.DateTimeExtensions.Tests/DateTimeRangeTests.cs +++ b/tests/Exceptionless.DateTimeExtensions.Tests/DateTimeRangeTests.cs @@ -42,6 +42,16 @@ public void CanParseIntoLocalTime() { Assert.Equal(new DateTime(2016, 12, 28, 6, 30, 0, DateTimeKind.Utc), localRange.UtcEnd); } + [Fact] + public void CanParse8601() { + const string time = "2023-12-28T05:00:00.000Z-2023-12-28T05:30:00.000Z"; + var range = DateTimeRange.Parse(time, DateTimeOffset.UtcNow); + Assert.Equal(new DateTime(2023, 12, 28, 5, 0, 0, DateTimeKind.Utc), range.Start); + Assert.Equal(new DateTime(2023, 12, 28, 5, 30, 0, DateTimeKind.Utc), range.End); + Assert.Equal(new DateTime(2023, 12, 28, 5, 0, 0, DateTimeKind.Utc), range.UtcStart); + Assert.Equal(new DateTime(2023, 12, 28, 5, 30, 0, DateTimeKind.Utc), range.UtcEnd); + } + [Theory] [MemberData(nameof(Inputs))] public void CanParseNamedRanges(string input, DateTime start, DateTime end) { diff --git a/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/ExplicitDateFormatParserTests.cs b/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/ExplicitDateFormatParserTests.cs index cb9b9d6..94ec148 100644 --- a/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/ExplicitDateFormatParserTests.cs +++ b/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/ExplicitDateFormatParserTests.cs @@ -17,15 +17,17 @@ public void ParseInput(string input, DateTime? start, DateTime? end) { public static IEnumerable Inputs { get { return new[] { - new object[] { "2014-02-01", _now.Change(null, 2, 1).StartOfDay(), _now.Change(null, 2, 1).EndOfDay() }, - new object[] { "2014-02-01T05", _now.Change(null, 2, 1, 5).StartOfHour(), _now.Change(null, 2, 1, 5).EndOfHour() }, - new object[] { "2014-02-01T05:30", _now.Change(null, 2, 1, 5, 30).StartOfMinute(), _now.Change(null, 2, 1, 5, 30).EndOfMinute() }, - new object[] { "2014-02-01T05:30:20", _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond(), _now.Change(null, 2, 1, 5, 30, 20).EndOfSecond() }, - new object[] { "2014-11-06", _now.Change(null, 11, 6).StartOfDay(), _now.Change(null, 11, 6).EndOfDay() }, - new object[] { "2014-12-24", _now.Change(null, 12, 24).StartOfDay(), _now.Change(null, 12, 24).EndOfDay() }, - new object[] { "2014-12-45", null, null }, - new object[] { "blah", null, null }, - new object[] { "blah blah", null, null } + new object[] { "2014-02-01", _now.Change(null, 2, 1).StartOfDay(), _now.Change(null, 2, 1).EndOfDay() }, + new object[] { "2014-02-01T05", _now.Change(null, 2, 1, 5).StartOfHour(), _now.Change(null, 2, 1, 5).EndOfHour() }, + new object[] { "2014-02-01T05:30", _now.Change(null, 2, 1, 5, 30).StartOfMinute(), _now.Change(null, 2, 1, 5, 30).EndOfMinute() }, + new object[] { "2014-02-01T05:30:20", _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond(), _now.Change(null, 2, 1, 5, 30, 20).EndOfSecond() }, + new object[] { "2014-02-01T05:30:20.000", _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond(), _now.Change(null, 2, 1, 5, 30, 20).EndOfSecond() }, + new object[] { "2014-02-01T05:30:20.000Z", _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond(), _now.Change(null, 2, 1, 5, 30, 20).EndOfSecond() }, + new object[] { "2014-11-06", _now.Change(null, 11, 6).StartOfDay(), _now.Change(null, 11, 6).EndOfDay() }, + new object[] { "2014-12-24", _now.Change(null, 12, 24).StartOfDay(), _now.Change(null, 12, 24).EndOfDay() }, + new object[] { "2014-12-45", null, null }, + new object[] { "blah", null, null }, + new object[] { "blah blah", null, null } }; } } diff --git a/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/FormatParserTestsBase.cs b/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/FormatParserTestsBase.cs index 97b5267..49ebbe1 100644 --- a/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/FormatParserTestsBase.cs +++ b/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/FormatParserTestsBase.cs @@ -17,7 +17,10 @@ static FormatParserTestsBase() { public void ValidateInput(IFormatParser parser, string input, DateTime? start, DateTime? end) { _logger.LogInformation("Input: {Input}, Now: {Now}, Start: {Start}, End: {End}", input, _now, start, end); + var range = parser.Parse(input, _now); + _logger.LogInformation("Parsed range: Start: {Start}, End: {End}", range?.Start, range?.End); + if (range == null) { Assert.Null(start); Assert.Null(end); diff --git a/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/PartParsers/ExplicitDatePartParserTests.cs b/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/PartParsers/ExplicitDatePartParserTests.cs index 14846eb..39ba1d6 100644 --- a/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/PartParsers/ExplicitDatePartParserTests.cs +++ b/tests/Exceptionless.DateTimeExtensions.Tests/FormatParsers/PartParsers/ExplicitDatePartParserTests.cs @@ -17,21 +17,25 @@ public void ParseInput(string input, bool isUpperLimit, DateTimeOffset? expected public static IEnumerable Inputs { get { return new[] { - new object[] { "2014-02-01", false, _now.Change(null, 2, 1).StartOfDay() }, - new object[] { "2014-02-01", true, _now.Change(null, 2, 1).EndOfDay() }, - new object[] { "2014-02-01T05", false, _now.Change(null, 2, 1, 5).StartOfHour() }, - new object[] { "2014-02-01T05", true, _now.Change(null, 2, 1, 5).EndOfHour() }, - new object[] { "2014-02-01T05:30", false, _now.Change(null, 2, 1, 5, 30).StartOfMinute() }, - new object[] { "2014-02-01T05:30", true, _now.Change(null, 2, 1, 5, 30).EndOfMinute() }, - new object[] { "2014-02-01T05:30:20", false, _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond() }, - new object[] { "2014-02-01T05:30:20", true, _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond() }, - new object[] { "2014-11-06", false, _now.Change(null, 11, 6).StartOfDay() }, - new object[] { "2014-11-06", true, _now.Change(null, 11, 6).EndOfDay() }, - new object[] { "2014-12-24", false, _now.Change(null, 12, 24).StartOfDay() }, - new object[] { "2014-12-24", true, _now.Change(null, 12, 24).EndOfDay() }, - new object[] { "2014-12-45", true, null }, - new object[] { "blah", false, null }, - new object[] { "blah blah", true, null } + new object[] { "2014-02-01", false, _now.Change(null, 2, 1).StartOfDay() }, + new object[] { "2014-02-01", true, _now.Change(null, 2, 1).EndOfDay() }, + new object[] { "2014-02-01T05", false, _now.Change(null, 2, 1, 5).StartOfHour() }, + new object[] { "2014-02-01T05", true, _now.Change(null, 2, 1, 5).EndOfHour() }, + new object[] { "2014-02-01T05:30", false, _now.Change(null, 2, 1, 5, 30).StartOfMinute() }, + new object[] { "2014-02-01T05:30", true, _now.Change(null, 2, 1, 5, 30).EndOfMinute() }, + new object[] { "2014-02-01T05:30:20", false, _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond() }, + new object[] { "2014-02-01T05:30:20", true, _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond() }, + new object[] { "2014-02-01T05:30:20.000", false, _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond() }, + new object[] { "2014-02-01T05:30:20.999", true, _now.Change(null, 2, 1, 5, 30, 20).EndOfSecond() }, + new object[] { "2014-02-01T05:30:20.000Z", false, _now.Change(null, 2, 1, 5, 30, 20).StartOfSecond() }, + new object[] { "2014-02-01T05:30:20.999Z", true, _now.Change(null, 2, 1, 5, 30, 20).EndOfSecond() }, + new object[] { "2014-11-06", false, _now.Change(null, 11, 6).StartOfDay() }, + new object[] { "2014-11-06", true, _now.Change(null, 11, 6).EndOfDay() }, + new object[] { "2014-12-24", false, _now.Change(null, 12, 24).StartOfDay() }, + new object[] { "2014-12-24", true, _now.Change(null, 12, 24).EndOfDay() }, + new object[] { "2014-12-45", true, null }, + new object[] { "blah", false, null }, + new object[] { "blah blah", true, null } }; } }