From 7c20557632cb4e36e657a5d4e1087ef03fab9720 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Fri, 9 Oct 2026 23:48:41 +0300 Subject: [PATCH] fix(filter): read the zone of a date and time value only after the fraction The date parser accepted the zone before the fraction, which is not an ISO 8601 format. The parser now reads the date, the time, the fraction, and then the zone. Behavior change: an unquoted date and time value with the zone before the fraction throws ParsingException. Write the zone after the fraction. Examples: SpecificDateTime == 2024-01-15T08:00:00Z.5 before: parses after: ParsingException SpecificDate == 2024-01-15T10:00:00+02:00.500 before: parses after: ParsingException SpecificDateTime == 2024-01-15T08:00:00.5Z before and after: parses --- .../Tests/FilterParsingRegressionTests.cs | 1 - QueryKit.UnitTests/FilterParsingRegressionTests.cs | 12 ++++++++++-- QueryKit/FilterParser.cs | 6 ++---- README.md | 1 + 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs index 211c198..a87ce98 100644 --- a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs +++ b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs @@ -145,7 +145,6 @@ public async Task date_time_offset_value_with_offset_matches_same_instant(string [Theory] [InlineData("SpecificDateTime == 2024-01-15T08:00:00.500Z", true)] - [InlineData("SpecificDateTime == 2024-01-15T08:00:00Z.5", true)] [InlineData("SpecificDate == 2024-01-15T10:00:00.5+02:00", true)] [InlineData("Time == 08:30:00.5", true)] [InlineData("Time == \"08:30:00.500\"", true)] diff --git a/QueryKit.UnitTests/FilterParsingRegressionTests.cs b/QueryKit.UnitTests/FilterParsingRegressionTests.cs index 74a38a0..3892da6 100644 --- a/QueryKit.UnitTests/FilterParsingRegressionTests.cs +++ b/QueryKit.UnitTests/FilterParsingRegressionTests.cs @@ -150,8 +150,6 @@ public void enum_list_value_is_split_into_items() [InlineData("SpecificDateTime ^^ [2024-01-15T08:00:00.5Z]")] [InlineData("SpecificDate == 2024-01-15T10:00:00.5+02:00")] [InlineData("SpecificDate == 2024-01-15T08:00:00.5000000Z")] - [InlineData("SpecificDateTime == 2024-01-15T08:00:00Z.5")] - [InlineData("SpecificDate == 2024-01-15T10:00:00+02:00.500")] [InlineData("Time == 08:30:00.5")] [InlineData("Time == \"08:30:00.500\"")] [InlineData("Time == \"08:30:00.5\"")] @@ -164,6 +162,16 @@ public void fractional_seconds_are_kept(string input) result.Select(x => x.Title).Should().Equal("match"); } + [Theory] + [InlineData("SpecificDateTime == 2024-01-15T08:00:00Z.5")] + [InlineData("SpecificDate == 2024-01-15T10:00:00+02:00.500")] + public void zone_before_the_fraction_throws(string input) + { + var act = () => FilterParser.ParseFilter(input); + + act.Should().Throw(); + } + private static TestingPerson[] FractionalSecondPeople() => new[] { new TestingPerson diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 551d5ff..1b5daad 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -297,14 +297,12 @@ private static readonly Parser DoubleQuoteParser private static readonly Parser DateTimeTimeParser = Parse.Regex(@"T\d{2}:\d{2}:\d{2}").Text().Optional().Select(x => x.GetOrElse("")); private static readonly Parser DateTimeMicrosParser = Parse.Regex(@"\.\d{1,7}").Text().Optional().Select(x => x.GetOrElse("")); private static readonly Parser DateTimeZoneParser = Parse.Regex(@"Z|[+-]\d{2}(:\d{2})?").Text().Optional().Select(x => x.GetOrElse("")); - // v1.14.2 read the zone before the fraction, so 2022-07-01T00:00:02Z.5 is a valid value. A zone after the fraction is also valid. private static readonly Parser DateTimeFormatParser = from dateFormat in Parse.Regex(@"\d{4}-\d{2}-\d{2}").Text() from timeFormat in DateTimeTimeParser - from zoneBeforeMicros in DateTimeZoneParser from micros in DateTimeMicrosParser - from zoneAfterMicros in zoneBeforeMicros == "" ? DateTimeZoneParser : Parse.Return("") - select dateFormat + timeFormat + micros + zoneBeforeMicros + zoneAfterMicros; + from timeZone in DateTimeZoneParser + select dateFormat + timeFormat + micros + timeZone; // A number always uses the '.' decimal point, so a filter has the same meaning in every culture. private static readonly Parser NumberParser = diff --git a/README.md b/README.md index 7299a7f..8edcf02 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,7 @@ Property list grouping is particularly useful for: * `var filterInput = """Time == "12:30:00" """;` * `var filterInput = """Time == "12:30:00.678722" """;` * A time fraction keeps every digit. For example, `"12:30:00.5"` is 12:30:00 and 500 milliseconds. + * In a date and time value, write the zone after the fraction: `2022-07-01T00:00:03.5Z`. An unquoted value with the zone before the fraction (`2022-07-01T00:00:03Z.5`) throws a `ParsingException`. * `bool` properties need to use `== true`, `== false`, or the same using the `!=` operator. they can not be standalone properies: