diff --git a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs index 99bf6bd..951aa5b 100644 --- a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs +++ b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs @@ -147,7 +147,7 @@ public async Task date_time_offset_value_with_offset_matches_same_instant(string [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)] - [InlineData("Time == \"08:30:00.5\"", false)] + [InlineData("Time == \"08:30:00.5\"", true)] public async Task fractional_second_value_matches_by_its_fraction(string valueFilter, bool expectFractionPerson) { // Arrange diff --git a/QueryKit.UnitTests/FilterParsingRegressionTests.cs b/QueryKit.UnitTests/FilterParsingRegressionTests.cs index 6c23b3b..c2c2f33 100644 --- a/QueryKit.UnitTests/FilterParsingRegressionTests.cs +++ b/QueryKit.UnitTests/FilterParsingRegressionTests.cs @@ -132,6 +132,8 @@ public void enum_list_value_is_split_into_items() [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\"")] + [InlineData("Time == \"08:30:00.50\"")] [InlineData("Time ^^ [08:30:00.5]")] public void fractional_seconds_are_kept(string input) { @@ -140,16 +142,6 @@ public void fractional_seconds_are_kept(string input) result.Select(x => x.Title).Should().Equal("match"); } - [Theory] - [InlineData("Time == \"08:30:00.5\"")] - [InlineData("Time == \"08:30:00.50\"")] - public void quoted_time_with_fewer_than_three_fraction_digits_drops_the_fraction(string input) - { - var result = FractionalSecondPeople().AsQueryable().ApplyQueryKitFilter(input).ToList(); - - result.Select(x => x.Title).Should().Equal("whole second"); - } - private static TestingPerson[] FractionalSecondPeople() => new[] { new TestingPerson diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 64f81f5..5b02bda 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -442,7 +442,7 @@ private static DateTimeOffset ToParameterOffset(DateTimeOffset value) { typeof(sbyte), value => sbyte.Parse(value, CultureInfo.InvariantCulture) }, }; - private static Expression CreateRightExpr(Expression leftExpr, string right, bool rightIsQuotedLiteral, ComparisonOperator op, + private static Expression CreateRightExpr(Expression leftExpr, string right, ComparisonOperator op, IQueryKitConfiguration? config = null, string? propertyPath = null, string? memberPath = null) { var targetType = leftExpr.Type; @@ -524,11 +524,11 @@ private static Expression CreateRightExpr(Expression leftExpr, string right, boo { // The lookup by query name above misses a property with a different query name. v1.14.2 then threw, // because it can not read a value of this type, so find the conversion by the property path instead. - return CreateStringConversionRightExpr(leftExpr.Type, right) ?? CreateRightExprFromType(targetType, right, rightIsQuotedLiteral, op); + return CreateStringConversionRightExpr(leftExpr.Type, right) ?? CreateRightExprFromType(targetType, right, op); } } - return CreateRightExprFromType(targetType, right, rightIsQuotedLiteral, op); + return CreateRightExprFromType(targetType, right, op); } // Builds the right side for a property with HasConversion() from a constructor that takes a string. @@ -558,7 +558,7 @@ private static bool CanCreateRightExprFromType(Type type) return IsEnumerable(type) || TypeConversionFunctions.ContainsKey(targetType) || targetType.IsEnum || targetType == typeof(object); } - private static Expression CreateRightExprFromType(Type leftExprType, string right, bool rightIsQuotedLiteral, ComparisonOperator op) + private static Expression CreateRightExprFromType(Type leftExprType, string right, ComparisonOperator op) { var isEnumerable = IsEnumerable(leftExprType); var targetType = leftExprType; @@ -569,7 +569,7 @@ private static Expression CreateRightExprFromType(Type leftExprType, string righ return FilterValue.Create(intVal, typeof(int)); } targetType = targetType.GetGenericArguments()[0]; - return CreateRightExprFromType(targetType, right, rightIsQuotedLiteral, op); + return CreateRightExprFromType(targetType, right, op); } var rawType = targetType; @@ -648,30 +648,9 @@ private static Expression CreateRightExprFromType(Type leftExprType, string righ { var time = TimeOnly.Parse(right, CultureInfo.InvariantCulture); - int millisecond = 0, microsecond = 0; - if (rightIsQuotedLiteral) - { - // Like v1.14.2, the milliseconds of a quoted value need at least 3 fraction digits and the microseconds need at least 6. - if (right.Contains('.')) - { - var fractionalSeconds = right.Split('.')[1]; - if (fractionalSeconds.Length >= 3) - { - millisecond = int.Parse(fractionalSeconds.Substring(0, 3)); - } - if (fractionalSeconds.Length >= 6) - { - microsecond = int.Parse(fractionalSeconds.Substring(3, 3)); - } - } - } - else - { - // v1.14.2 did not accept an unquoted fraction, so an unquoted value keeps its full fraction. - var fractionalTicks = time.Ticks % TimeSpan.TicksPerSecond; - millisecond = (int)(fractionalTicks / TimeSpan.TicksPerMillisecond); - microsecond = (int)(fractionalTicks % TimeSpan.TicksPerMillisecond / 10); - } + var fractionalTicks = time.Ticks % TimeSpan.TicksPerSecond; + var millisecond = (int)(fractionalTicks / TimeSpan.TicksPerMillisecond); + var microsecond = (int)(fractionalTicks % TimeSpan.TicksPerMillisecond / 10); // One microsecond is 10 ticks. The TimeOnly constructor with microseconds needs .NET 7. var value = new TimeOnly(time.Hour, time.Minute, time.Second, millisecond) @@ -935,12 +914,12 @@ private static Parser ComparisonExprParser(ParameterExpression pa var leftExprForRightSide = guidConfig?.UsesConversion == true && guidConfig.ConversionTargetType == typeof(string) ? guidStringExpr : leftExpr; - return temp.op.GetExpression(guidStringExpr, CreateRightExpr(leftExprForRightSide, temp.right, temp.rightIsQuotedLiteral, temp.op, config, guidPropertyPath), + return temp.op.GetExpression(guidStringExpr, CreateRightExpr(leftExprForRightSide, temp.right, temp.op, config, guidPropertyPath), config?.DbContextType, ResolveCaseMode(guidPropertyPath, config)); } // For non-string operators, use direct GUID comparison - return temp.op.GetExpression(leftExpr, CreateRightExpr(leftExpr, temp.right, temp.rightIsQuotedLiteral, temp.op, config, guidPropertyPath), + return temp.op.GetExpression(leftExpr, CreateRightExpr(leftExpr, temp.right, temp.op, config, guidPropertyPath), config?.DbContextType); } @@ -1050,7 +1029,7 @@ private static Parser ComparisonExprParser(ParameterExpression pa } } - var rightExpr = CreateRightExpr(leftExprForComparison, temp.right, temp.rightIsQuotedLiteral, temp.op, config, propertyPath); + var rightExpr = CreateRightExpr(leftExprForComparison, temp.right, temp.op, config, propertyPath); // Handle nested collection filtering if (leftExprForComparison is MethodCallExpression methodCall && IsNestedCollectionExpression(methodCall)) @@ -1271,7 +1250,7 @@ private static Parser PropertyListComparisonExprParser( .SelectMany(properties => comparisonOperatorParser, (properties, op) => new { properties, op }) .SelectMany(temp => rightSideValueParser, - (temp, rightValue) => new { temp.properties, temp.op, right = rightValue.Value, rightIsQuotedLiteral = rightValue.IsQuotedLiteral, cultureNumberPrefix = rightValue.CultureNumberPrefix }) + (temp, rightValue) => new { temp.properties, temp.op, right = rightValue.Value, cultureNumberPrefix = rightValue.CultureNumberPrefix }) .Select(clause => BuildClauseLikeV1142(clause.cultureNumberPrefix, clause.right, right => { var temp = clause with { right = right }; @@ -1319,7 +1298,7 @@ private static Parser PropertyListComparisonExprParser( leftExpr = HandleGuidConversion(leftExpr, leftExpr.Type); } - var rightExpr = CreateRightExpr(leftExpr, temp.right, temp.rightIsQuotedLiteral, temp.op, config, fullPropPath, reference.Path); + var rightExpr = CreateRightExpr(leftExpr, temp.right, temp.op, config, fullPropPath, reference.Path); var comparison = temp.op.GetExpression(leftExpr, rightExpr, config?.DbContextType, ResolveCaseMode(fullPropPath, config)); // Combine with AND for negative operators, OR for positive operators diff --git a/README.md b/README.md index d5324de..36ad5a1 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,7 @@ Property list grouping is particularly useful for: * `TimeOnly`: * `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. * `bool` properties need to use `== true`, `== false`, or the same using the `!=` operator. they can not be standalone properies: