Skip to content

Commit a61c97c

Browse files
authored
fix(parser)!: parse numbers with the invariant culture only (#121)
The number grammar also accepted the decimal separator of the current culture. Under de-DE, Rating > 4,5 parsed, and the value conversion read it with the invariant culture as 45. The API returned wrong rows with no error. The number grammar, the count operator value, and the literal checks in IsPropertyPath now use the invariant culture only. A number always uses the '.' decimal point. BREAKING CHANGE: a number with a decimal comma, for example Rating > 4,5, throws a QueryKitException in every culture. Before, a server with a comma culture read this value as 45. Send the number with a '.' decimal point, for example Rating > 4.5.
1 parent 0f82f3d commit a61c97c

3 files changed

Lines changed: 7 additions & 34 deletions

File tree

‎QueryKit.UnitTests/DotNumberCultureTests.cs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public void dot_number_on_an_integer_property_throws_parsing_exception(string cu
3030

3131
[Theory]
3232
[InlineData("en-US", "Rating > 4.4")]
33-
[InlineData("de-DE", "Rating > 4,4")]
3433
[InlineData("de-DE", "Rating > \"4.4\"")]
3534
[InlineData("de-DE", "Rating ^^ [\"4.0\"]")]
3635
[InlineData("de-DE", "Rating > @4.4")]

‎QueryKit.UnitTests/FilterParsingRegressionTests.cs‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,12 @@ public void decimal_value_uses_invariant_culture(string cultureName)
2828
[Theory]
2929
[InlineData("de-DE")]
3030
[InlineData("fr-FR")]
31-
public void decimal_value_with_culture_decimal_comma_is_read_as_before(string cultureName)
32-
{
33-
var input = """Rating > 4,5""";
34-
35-
var filterExpression = WithCulture(cultureName, () => FilterParser.ParseFilter<TestingPerson>(input));
36-
37-
filterExpression.ToDisplayString().Should().Be("x => (x.Rating > 45)");
38-
}
39-
40-
[Fact]
41-
public void decimal_value_with_decimal_comma_is_not_accepted_in_a_culture_with_decimal_point()
31+
[InlineData("en-US")]
32+
public void decimal_value_with_decimal_comma_is_not_accepted(string cultureName)
4233
{
4334
var input = """Rating > 4,5""";
4435

45-
var act = () => WithCulture("en-US", () => FilterParser.ParseFilter<TestingPerson>(input));
36+
var act = () => WithCulture(cultureName, () => FilterParser.ParseFilter<TestingPerson>(input));
4637

4738
act.Should().Throw<QueryKitException>();
4839
}

‎QueryKit/FilterParser.cs‎

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -256,24 +256,8 @@ from micros in DateTimeMicrosParser
256256
from zoneAfterMicros in zoneBeforeMicros == "" ? DateTimeZoneParser : Parse.Return("")
257257
select dateFormat + timeFormat + micros + zoneBeforeMicros + zoneAfterMicros;
258258

259-
// A number with a '.' decimal point, or with the decimal separator of the current culture.
260-
// The longer match wins, so '4.5' parses in every culture and '4,5' still parses in a culture that uses ','.
261-
private static readonly Parser<string> UnsignedNumberParser = input =>
262-
{
263-
var invariant = Parse.DecimalInvariant(input);
264-
var culture = Parse.Decimal(input);
265-
return culture.WasSuccessful && (!invariant.WasSuccessful || culture.Remainder.Position > invariant.Remainder.Position)
266-
? culture
267-
: invariant;
268-
};
269-
259+
// A number always uses the '.' decimal point, so a filter has the same meaning in every culture.
270260
private static readonly Parser<string> NumberParser =
271-
from sign in Parse.Char('-').Optional().Select(x => x.IsDefined ? "-" : "")
272-
from number in UnsignedNumberParser
273-
select sign + number;
274-
275-
// List items are separated by ',', so a list number always uses the '.' decimal point.
276-
private static readonly Parser<string> ListNumberParser =
277261
from sign in Parse.Char('-').Optional().Select(x => x.IsDefined ? "-" : "")
278262
from number in Parse.DecimalInvariant
279263
select sign + number;
@@ -344,7 +328,7 @@ from closingQuotes in Parse.Char('"').Repeat(count).Text()
344328
.Or(DateTimeFormatParser)
345329
.Or(TimeFormatParser)
346330
.Select(v => (v, false))
347-
.Or(ListNumberParser.Select(v => (v, CultureNumberPrefix(v) != null)))
331+
.Or(NumberParser.Select(v => (v, CultureNumberPrefix(v) != null)))
348332
.Or(RawStringLiteralParser.Or(DoubleQuoteParser).Or(Identifier).Select(v => (v, false)))
349333
.DelimitedBy(Parse.Char(',').Token());
350334

@@ -580,7 +564,7 @@ private static Expression CreateRightExprFromType(Type leftExprType, string righ
580564
var targetType = leftExprType;
581565
if (isEnumerable)
582566
{
583-
if (op.IsCountOperator() && (int.TryParse(right, out var intVal) || int.TryParse(right, NumberStyles.Integer, CultureInfo.InvariantCulture, out intVal)))
567+
if (op.IsCountOperator() && int.TryParse(right, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
584568
{
585569
return FilterValue.Create(intVal, typeof(int));
586570
}
@@ -1542,8 +1526,7 @@ private static bool IsPropertyPath(string value, Type entityType)
15421526
if (value == "null" ||
15431527
value.StartsWith("\"") ||
15441528
value.StartsWith("[") ||
1545-
value.Contains("-") && (DateTime.TryParse(value, out _) || DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) ||
1546-
decimal.TryParse(value, out _) ||
1529+
value.Contains("-") && DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out _) ||
15471530
decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out _) ||
15481531
bool.TryParse(value, out _) ||
15491532
Guid.TryParse(value, out _))

0 commit comments

Comments
 (0)