Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions QueryKit.UnitTests/FilterParsingRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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)
{
Expand All @@ -149,16 +151,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
Expand Down
47 changes: 13 additions & 34 deletions QueryKit/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,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;
Expand Down Expand Up @@ -540,11 +540,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<string>() from a constructor that takes a string.
Expand Down Expand Up @@ -574,7 +574,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;
Expand All @@ -585,7 +585,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;
Expand Down Expand Up @@ -664,30 +664,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)
Expand Down Expand Up @@ -924,12 +903,12 @@ private static Parser<Expression> ComparisonExprParser<T>(ParameterExpression pa
var leftExprForRightSide = guidConfig?.UsesConversion == true && guidConfig.ConversionTargetType == typeof(string)
? guidStringExpr
: leftExpr;
return temp.op.GetExpression<T>(guidStringExpr, CreateRightExpr(leftExprForRightSide, temp.right, temp.rightIsQuotedLiteral, temp.op, config, guidPropertyPath),
return temp.op.GetExpression<T>(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<T>(leftExpr, CreateRightExpr(leftExpr, temp.right, temp.rightIsQuotedLiteral, temp.op, config, guidPropertyPath),
return temp.op.GetExpression<T>(leftExpr, CreateRightExpr(leftExpr, temp.right, temp.op, config, guidPropertyPath),
config?.DbContextType);
}

Expand Down Expand Up @@ -1039,7 +1018,7 @@ private static Parser<Expression> ComparisonExprParser<T>(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))
Expand Down Expand Up @@ -1260,7 +1239,7 @@ private static Parser<Expression> PropertyListComparisonExprParser<T>(
.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 };
Expand Down Expand Up @@ -1308,7 +1287,7 @@ private static Parser<Expression> PropertyListComparisonExprParser<T>(
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<T>(leftExpr, rightExpr, config?.DbContextType, ResolveCaseMode(fullPropPath, config));

// Combine with AND for negative operators, OR for positive operators
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading