Skip to content

Commit fc0160f

Browse files
[v2] feat: Added datetime (#70)
1 parent e9cf2b5 commit fc0160f

File tree

12 files changed

+191
-15
lines changed

12 files changed

+191
-15
lines changed

example/Dto/UserDto.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ public record UserDto
88
public string Firstname { get; set; } = string.Empty;
99
public string Lastname { get; set; } = string.Empty;
1010
public int Age { get; set; }
11+
public DateTime DateOfBirthUtc { get; set; }
12+
public DateTime DateOfBirthTz { get; set; }
1113
}

example/Entities/User.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
using System.ComponentModel.DataAnnotations.Schema;
2+
13
public record User
24
{
35
public Guid Id { get; set; }
46
public string Firstname { get; set; } = string.Empty;
57
public string Lastname { get; set; } = string.Empty;
68
public int Age { get; set; }
79
public bool IsDeleted { get; set; }
10+
11+
[Column(TypeName = "timestamp with time zone")]
12+
public DateTime DateOfBirthUtc { get; set; }
13+
14+
[Column(TypeName = "timestamp without time zone")]
15+
public DateTime DateOfBirthTz { get; set; }
816
}

example/Program.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using Microsoft.EntityFrameworkCore;
77
using Testcontainers.PostgreSql;
88

9+
Randomizer.Seed = new Random(8675309);
10+
911
var builder = WebApplication.CreateBuilder(args);
1012

1113
var postgreSqlContainer = new PostgreSqlBuilder()
@@ -37,7 +39,15 @@
3739
.RuleFor(x => x.Firstname, f => f.Person.FirstName)
3840
.RuleFor(x => x.Lastname, f => f.Person.LastName)
3941
.RuleFor(x => x.Age, f => f.Random.Int(0, 100))
40-
.RuleFor(x => x.IsDeleted, f => f.Random.Bool());
42+
.RuleFor(x => x.IsDeleted, f => f.Random.Bool())
43+
.Rules((f, u) =>
44+
{
45+
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
46+
var date = f.Date.Past().ToUniversalTime();
47+
48+
u.DateOfBirthUtc = date;
49+
u.DateOfBirthTz = TimeZoneInfo.ConvertTimeFromUtc(date, timeZone);
50+
});
4151

4252
context.Users.AddRange(users.Generate(1_000));
4353
context.SaveChanges();

src/GoatQuery/src/Ast/StringLiteral.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,14 @@ public IntegerLiteral(Token token, int value) : base(token)
2828
{
2929
Value = value;
3030
}
31+
}
32+
33+
public sealed class DateTimeLiteral : QueryExpression
34+
{
35+
public DateTime Value { get; set; }
36+
37+
public DateTimeLiteral(Token token, DateTime value) : base(token)
38+
{
39+
Value = value;
40+
}
3141
}

src/GoatQuery/src/Evaluator/FilterEvaluator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Linq.Expressions;
45
using FluentResults;
56

@@ -38,6 +39,9 @@ public static Result<Expression> Evaluate(QueryExpression expression, ParameterE
3839
case StringLiteral literal:
3940
value = Expression.Constant(literal.Value, property.Type);
4041
break;
42+
case DateTimeLiteral literal:
43+
value = Expression.Constant(literal.Value, property.Type);
44+
break;
4145
default:
4246
return Result.Fail($"Unsupported literal type: {exp.Right.GetType().Name}");
4347
}

src/GoatQuery/src/Lexer/Lexer.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Globalization;
23

34
public sealed class QueryLexer
45
{
@@ -63,6 +64,12 @@ public Token NextToken()
6364

6465
if (IsDigit(token.Literal[0]))
6566
{
67+
if (IsDateTime(token.Literal))
68+
{
69+
token.Type = TokenType.DATETIME;
70+
return token;
71+
}
72+
6673
token.Type = TokenType.INT;
6774
return token;
6875
}
@@ -78,6 +85,11 @@ public Token NextToken()
7885
return token;
7986
}
8087

88+
private bool IsDateTime(string value)
89+
{
90+
return DateTime.TryParse(value, out _);
91+
}
92+
8193
private bool IsGuid(string value)
8294
{
8395
return Guid.TryParse(value, out _);
@@ -87,7 +99,7 @@ private string ReadIdentifier()
8799
{
88100
var currentPosition = _position;
89101

90-
while (IsLetter(_character) || IsDigit(_character))
102+
while (IsLetter(_character) || IsDigit(_character) || _character == '-' || _character == ':' || _character == '.')
91103
{
92104
ReadCharacter();
93105
}
@@ -97,7 +109,7 @@ private string ReadIdentifier()
97109

98110
private bool IsLetter(char ch)
99111
{
100-
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch == '-';
112+
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_';
101113
}
102114

103115
private bool IsDigit(char ch)

src/GoatQuery/src/Parser/Parser.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Linq;
45
using FluentResults;
56

@@ -139,7 +140,7 @@ private Result<InfixExpression> ParseFilterStatement()
139140

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

142-
if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID))
143+
if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME))
143144
{
144145
return Result.Fail("Invalid value type within filter");
145146
}
@@ -151,7 +152,7 @@ private Result<InfixExpression> ParseFilterStatement()
151152
return Result.Fail("Value must be a string when using 'contains' operand");
152153
}
153154

154-
if (statement.Operator.In(Keywords.Lt, Keywords.Lte, Keywords.Gt, Keywords.Gte) && _currentToken.Type != TokenType.INT)
155+
if (statement.Operator.In(Keywords.Lt, Keywords.Lte, Keywords.Gt, Keywords.Gte) && !CurrentTokenIn(TokenType.INT, TokenType.DATETIME))
155156
{
156157
return Result.Fail($"Value must be an integer when using '{statement.Operator}' operand");
157158
}
@@ -173,6 +174,12 @@ private Result<InfixExpression> ParseFilterStatement()
173174
statement.Right = new IntegerLiteral(_currentToken, intValue);
174175
}
175176
break;
177+
case TokenType.DATETIME:
178+
if (DateTime.TryParse(_currentToken.Literal, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dateTimeValue))
179+
{
180+
statement.Right = new DateTimeLiteral(_currentToken, dateTimeValue);
181+
}
182+
break;
176183
}
177184

178185
return statement;
@@ -196,9 +203,14 @@ private bool CurrentTokenIs(TokenType token)
196203
return _currentToken.Type == token;
197204
}
198205

199-
private bool PeekTokenIn(params TokenType[] token)
206+
private bool CurrentTokenIn(params TokenType[] tokens)
207+
{
208+
return tokens.Contains(_currentToken.Type);
209+
}
210+
211+
private bool PeekTokenIn(params TokenType[] tokens)
200212
{
201-
return token.Contains(_peekToken.Type);
213+
return tokens.Contains(_peekToken.Type);
202214
}
203215

204216
private bool PeekIdentifierIs(string identifier)

src/GoatQuery/src/Token/Token.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public enum TokenType
66
STRING,
77
INT,
88
GUID,
9+
DATETIME,
910
LPAREN,
1011
RPAREN,
1112
}

src/GoatQuery/tests/Filter/FilterLexerTest.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,83 @@ public static IEnumerable<object[]> Parameters()
224224
new (TokenType.INT, "50"),
225225
}
226226
};
227+
228+
yield return new object[]
229+
{
230+
"dateOfBirth eq 2000-01-01",
231+
new KeyValuePair<TokenType, string>[]
232+
{
233+
new (TokenType.IDENT, "dateOfBirth"),
234+
new (TokenType.IDENT, "eq"),
235+
new (TokenType.DATETIME, "2000-01-01"),
236+
}
237+
};
238+
239+
yield return new object[]
240+
{
241+
"dateOfBirth lt 2000-01-01",
242+
new KeyValuePair<TokenType, string>[]
243+
{
244+
new (TokenType.IDENT, "dateOfBirth"),
245+
new (TokenType.IDENT, "lt"),
246+
new (TokenType.DATETIME, "2000-01-01"),
247+
}
248+
};
249+
250+
yield return new object[]
251+
{
252+
"dateOfBirth lte 2000-01-01",
253+
new KeyValuePair<TokenType, string>[]
254+
{
255+
new (TokenType.IDENT, "dateOfBirth"),
256+
new (TokenType.IDENT, "lte"),
257+
new (TokenType.DATETIME, "2000-01-01"),
258+
}
259+
};
260+
261+
yield return new object[]
262+
{
263+
"dateOfBirth gt 2000-01-01",
264+
new KeyValuePair<TokenType, string>[]
265+
{
266+
new (TokenType.IDENT, "dateOfBirth"),
267+
new (TokenType.IDENT, "gt"),
268+
new (TokenType.DATETIME, "2000-01-01"),
269+
}
270+
};
271+
272+
yield return new object[]
273+
{
274+
"dateOfBirth gte 2000-01-01",
275+
new KeyValuePair<TokenType, string>[]
276+
{
277+
new (TokenType.IDENT, "dateOfBirth"),
278+
new (TokenType.IDENT, "gte"),
279+
new (TokenType.DATETIME, "2000-01-01"),
280+
}
281+
};
282+
283+
yield return new object[]
284+
{
285+
"dateOfBirth eq 2023-01-01T15:30:00Z",
286+
new KeyValuePair<TokenType, string>[]
287+
{
288+
new (TokenType.IDENT, "dateOfBirth"),
289+
new (TokenType.IDENT, "eq"),
290+
new (TokenType.DATETIME, "2023-01-01T15:30:00Z"),
291+
}
292+
};
293+
294+
yield return new object[]
295+
{
296+
"dateOfBirth eq 2023-01-30T09:29:55.1750906Z",
297+
new KeyValuePair<TokenType, string>[]
298+
{
299+
new (TokenType.IDENT, "dateOfBirth"),
300+
new (TokenType.IDENT, "eq"),
301+
new (TokenType.DATETIME, "2023-01-30T09:29:55.1750906Z"),
302+
}
303+
};
227304
}
228305

229306
[Theory]

src/GoatQuery/tests/Filter/FilterParserTest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ public sealed class FilterParserTest
1313
[InlineData("Age lte 99", "Age", "lte", "99")]
1414
[InlineData("Age gt 99", "Age", "gt", "99")]
1515
[InlineData("Age gte 99", "Age", "gte", "99")]
16+
[InlineData("dateOfBirth eq 2000-01-01", "dateOfBirth", "eq", "2000-01-01")]
17+
[InlineData("dateOfBirth lt 2000-01-01", "dateOfBirth", "lt", "2000-01-01")]
18+
[InlineData("dateOfBirth lte 2000-01-01", "dateOfBirth", "lte", "2000-01-01")]
19+
[InlineData("dateOfBirth gt 2000-01-01", "dateOfBirth", "gt", "2000-01-01")]
20+
[InlineData("dateOfBirth gte 2000-01-01", "dateOfBirth", "gte", "2000-01-01")]
21+
[InlineData("dateOfBirth eq 2023-01-30T09:29:55.1750906Z", "dateOfBirth", "eq", "2023-01-30T09:29:55.1750906Z")]
1622
public void Test_ParsingFilterStatement(string input, string expectedLeft, string expectedOperator, string expectedRight)
1723
{
1824
var lexer = new QueryLexer(input);

0 commit comments

Comments
 (0)