Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v2] Added decimal, float & double support for filter #71

Merged
merged 3 commits into from
Jul 31, 2024
Merged
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
1 change: 1 addition & 0 deletions example/Dto/UserDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record UserDto
public string Firstname { get; set; } = string.Empty;
public string Lastname { get; set; } = string.Empty;
public int Age { get; set; }
public double Test { get; set; }
public DateTime DateOfBirthUtc { get; set; }
public DateTime DateOfBirthTz { get; set; }
}
1 change: 1 addition & 0 deletions example/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record User
public string Lastname { get; set; } = string.Empty;
public int Age { get; set; }
public bool IsDeleted { get; set; }
public double Test { get; set; }

[Column(TypeName = "timestamp with time zone")]
public DateTime DateOfBirthUtc { get; set; }
Expand Down
1 change: 1 addition & 0 deletions example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
.RuleFor(x => x.Lastname, f => f.Person.LastName)
.RuleFor(x => x.Age, f => f.Random.Int(0, 100))
.RuleFor(x => x.IsDeleted, f => f.Random.Bool())
.RuleFor(x => x.Test, f => f.Random.Double())
.Rules((f, u) =>
{
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
Expand Down
30 changes: 30 additions & 0 deletions src/GoatQuery/src/Ast/StringLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ public IntegerLiteral(Token token, int value) : base(token)
}
}

public sealed class DecimalLiteral : QueryExpression
{
public decimal Value { get; set; }

public DecimalLiteral(Token token, decimal value) : base(token)
{
Value = value;
}
}

public sealed class FloatLiteral : QueryExpression
{
public float Value { get; set; }

public FloatLiteral(Token token, float value) : base(token)
{
Value = value;
}
}

public sealed class DoubleLiteral : QueryExpression
{
public double Value { get; set; }

public DoubleLiteral(Token token, double value) : base(token)
{
Value = value;
}
}

public sealed class DateTimeLiteral : QueryExpression
{
public DateTime Value { get; set; }
Expand Down
9 changes: 9 additions & 0 deletions src/GoatQuery/src/Evaluator/FilterEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ public static Result<Expression> Evaluate(QueryExpression expression, ParameterE

value = integerConstant.Value;
break;
case DecimalLiteral literal:
value = Expression.Constant(literal.Value, property.Type);
break;
case FloatLiteral literal:
value = Expression.Constant(literal.Value, property.Type);
break;
case DoubleLiteral literal:
value = Expression.Constant(literal.Value, property.Type);
break;
case StringLiteral literal:
value = Expression.Constant(literal.Value, property.Type);
break;
Expand Down
18 changes: 18 additions & 0 deletions src/GoatQuery/src/Lexer/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ public Token NextToken()
return token;
}

if (token.Literal.EndsWith("f", StringComparison.OrdinalIgnoreCase))
{
token.Type = TokenType.FLOAT;
return token;
}

if (token.Literal.EndsWith("m", StringComparison.OrdinalIgnoreCase))
{
token.Type = TokenType.DECIMAL;
return token;
}

if (token.Literal.EndsWith("d", StringComparison.OrdinalIgnoreCase))
{
token.Type = TokenType.DOUBLE;
return token;
}

token.Type = TokenType.INT;
return token;
}
Expand Down
28 changes: 26 additions & 2 deletions src/GoatQuery/src/Parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private Result<InfixExpression> ParseFilterStatement()

var statement = new InfixExpression(_currentToken, identifier, _currentToken.Literal);

if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME))
if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME, TokenType.DECIMAL, TokenType.FLOAT, TokenType.DOUBLE))
{
return Result.Fail("Invalid value type within filter");
}
Expand All @@ -152,7 +152,7 @@ private Result<InfixExpression> ParseFilterStatement()
return Result.Fail("Value must be a string when using 'contains' operand");
}

if (statement.Operator.In(Keywords.Lt, Keywords.Lte, Keywords.Gt, Keywords.Gte) && !CurrentTokenIn(TokenType.INT, TokenType.DATETIME))
if (statement.Operator.In(Keywords.Lt, Keywords.Lte, Keywords.Gt, Keywords.Gte) && !CurrentTokenIn(TokenType.INT, TokenType.DECIMAL, TokenType.FLOAT, TokenType.DOUBLE, TokenType.DATETIME))
{
return Result.Fail($"Value must be an integer when using '{statement.Operator}' operand");
}
Expand All @@ -174,6 +174,30 @@ private Result<InfixExpression> ParseFilterStatement()
statement.Right = new IntegerLiteral(_currentToken, intValue);
}
break;
case TokenType.FLOAT:
var floatValueWithoutSuffixLiteral = _currentToken.Literal.TrimEnd('f');

if (float.TryParse(floatValueWithoutSuffixLiteral, out var floatValue))
{
statement.Right = new FloatLiteral(_currentToken, floatValue);
}
break;
case TokenType.DECIMAL:
var decimalValueWithoutSuffixLiteral = _currentToken.Literal.TrimEnd('m');

if (decimal.TryParse(decimalValueWithoutSuffixLiteral, out var decimalValue))
{
statement.Right = new DecimalLiteral(_currentToken, decimalValue);
}
break;
case TokenType.DOUBLE:
var doubleValueWithoutSuffixLiteral = _currentToken.Literal.TrimEnd('d');

if (double.TryParse(doubleValueWithoutSuffixLiteral, out var doubleValue))
{
statement.Right = new DoubleLiteral(_currentToken, doubleValue);
}
break;
case TokenType.DATETIME:
if (DateTime.TryParse(_currentToken.Literal, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dateTimeValue))
{
Expand Down
3 changes: 3 additions & 0 deletions src/GoatQuery/src/Token/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ public enum TokenType
IDENT,
STRING,
INT,
DECIMAL,
FLOAT,
DOUBLE,
GUID,
DATETIME,
LPAREN,
Expand Down
99 changes: 99 additions & 0 deletions src/GoatQuery/tests/Filter/FilterLexerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,105 @@ public static IEnumerable<object[]> Parameters()
}
};

yield return new object[]
{
"id eq 10m",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.DECIMAL, "10m"),
}
};

yield return new object[]
{
"id eq 10.50m",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.DECIMAL, "10.50m"),
}
};

yield return new object[]
{
"id eq 10.50M",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.DECIMAL, "10.50M"),
}
};

yield return new object[]
{
"id eq 10f",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.FLOAT, "10f"),
}
};

yield return new object[]
{
"id ne 0.1121563052701180f",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "ne"),
new (TokenType.FLOAT, "0.1121563052701180f"),
}
};

yield return new object[]
{
"id ne 0.1121563052701180F",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "ne"),
new (TokenType.FLOAT, "0.1121563052701180F"),
}
};

yield return new object[]
{
"id eq 10d",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.DOUBLE, "10d"),
}
};

yield return new object[]
{
"id eq 3.14159265359d",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.DOUBLE, "3.14159265359d"),
}
};

yield return new object[]
{
"id eq 3.14159265359D",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "id"),
new (TokenType.IDENT, "eq"),
new (TokenType.DOUBLE, "3.14159265359D"),
}
};

yield return new object[]
{
"age lt 50",
Expand Down
3 changes: 3 additions & 0 deletions src/GoatQuery/tests/Filter/FilterParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public sealed class FilterParserTest
[InlineData("Age ne 10", "Age", "ne", "10")]
[InlineData("Name contains 'John'", "Name", "contains", "John")]
[InlineData("Id eq e4c7772b-8947-4e46-98ed-644b417d2a08", "Id", "eq", "e4c7772b-8947-4e46-98ed-644b417d2a08")]
[InlineData("Id eq 3.14159265359f", "Id", "eq", "3.14159265359f")]
[InlineData("Id eq 3.14159265359m", "Id", "eq", "3.14159265359m")]
[InlineData("Id eq 3.14159265359d", "Id", "eq", "3.14159265359d")]
[InlineData("Age lt 99", "Age", "lt", "99")]
[InlineData("Age lte 99", "Age", "lte", "99")]
[InlineData("Age gt 99", "Age", "gt", "99")]
Expand Down
47 changes: 41 additions & 6 deletions src/GoatQuery/tests/Filter/FilterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ public sealed class FilterTest
{
private static readonly Dictionary<string, User> _users = new Dictionary<string, User>
{
["John"] = new User { Age = 2, Firstname = "John", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2004-01-31 23:59:59") },
["Jane"] = new User { Age = 1, Firstname = "Jane", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2020-05-09 15:30:00") },
["Apple"] = new User { Age = 2, Firstname = "Apple", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("1980-12-31 00:00:01") },
["Harry"] = new User { Age = 1, Firstname = "Harry", UserId = Guid.Parse("e4c7772b-8947-4e46-98ed-644b417d2a08"), DateOfBirth = DateTime.Parse("2002-08-01") },
["Doe"] = new User { Age = 3, Firstname = "Doe", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2023-07-26 12:00:30") },
["Egg"] = new User { Age = 3, Firstname = "Egg", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2000-01-01 00:00:00") },
["John"] = new User { Age = 2, Firstname = "John", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2004-01-31 23:59:59"), BalanceDecimal = 1.50m },
["Jane"] = new User { Age = 1, Firstname = "Jane", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2020-05-09 15:30:00"), BalanceDecimal = 0 },
["Apple"] = new User { Age = 2, Firstname = "Apple", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("1980-12-31 00:00:01"), BalanceFloat = 1204050.98f },
["Harry"] = new User { Age = 1, Firstname = "Harry", UserId = Guid.Parse("e4c7772b-8947-4e46-98ed-644b417d2a08"), DateOfBirth = DateTime.Parse("2002-08-01"), BalanceDecimal = 0.5372958205929493m },
["Doe"] = new User { Age = 3, Firstname = "Doe", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2023-07-26 12:00:30"), BalanceDecimal = null },
["Egg"] = new User { Age = 3, Firstname = "Egg", UserId = Guid.Parse("58cdeca3-645b-457c-87aa-7d5f87734255"), DateOfBirth = DateTime.Parse("2000-01-01 00:00:00"), BalanceDouble = 1334534453453433.33435443343231235652d },
};

public static IEnumerable<object[]> Parameters()
Expand Down Expand Up @@ -134,6 +134,41 @@ public static IEnumerable<object[]> Parameters()
new[] { _users["John"], _users["Apple"] }
};

yield return new object[] {
"balanceDecimal eq 1.50m",
new[] { _users["John"] }
};

yield return new object[] {
"balanceDecimal gt 1m",
new[] { _users["John"] }
};

yield return new object[] {
"balanceDecimal gt 0.50m",
new[] { _users["John"], _users["Harry"] }
};

yield return new object[] {
"balanceDecimal eq 0.5372958205929493m",
new[] { _users["Harry"] }
};

yield return new object[] {
"balanceDouble eq 1334534453453433.33435443343231235652d",
new[] { _users["Egg"] }
};

yield return new object[] {
"balanceFloat eq 1204050.98f",
new[] { _users["Apple"] }
};

yield return new object[] {
"balanceFloat gt 2204050f",
Array.Empty<User>()
};

yield return new object[] {
"dateOfBirth eq 2000-01-01",
new[] { _users["Egg"] }
Expand Down
3 changes: 3 additions & 0 deletions src/GoatQuery/tests/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ public record User
public int Age { get; set; }
public Guid UserId { get; set; }
public string Firstname { get; set; } = string.Empty;
public decimal? BalanceDecimal { get; set; }
public double? BalanceDouble { get; set; }
public float? BalanceFloat { get; set; }
public DateTime DateOfBirth { get; set; }
}

Expand Down