Skip to content

Commit cd021ef

Browse files
authored
Adds support for common operators for time.Time (#256)
* adds support for common operators for time.Time * adds eval tests for time operators. updates checker to allow time.Time and time.Duration to be passed to the operator fns. * allows duration to be the first arg in time addition. updates the checker return types as they were incorrect. * add ability to compare time to any interface * adds checker test for time + interface combinations
1 parent c774746 commit cd021ef

File tree

7 files changed

+268
-14
lines changed

7 files changed

+268
-14
lines changed

checker/checker.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ func (v *visitor) BinaryNode(node *ast.BinaryNode) reflect.Type {
208208
if isComparable(l, r) {
209209
return boolType
210210
}
211+
if isTime(l) && isTime(r) {
212+
return boolType
213+
}
211214

212215
case "or", "||", "and", "&&":
213216
if isBool(l) && isBool(r) {
@@ -232,8 +235,19 @@ func (v *visitor) BinaryNode(node *ast.BinaryNode) reflect.Type {
232235
if isString(l) && isString(r) {
233236
return boolType
234237
}
238+
if isTime(l) && isTime(r) {
239+
return boolType
240+
}
235241

236-
case "/", "-", "*":
242+
case "-":
243+
if isNumber(l) && isNumber(r) {
244+
return combined(l, r)
245+
}
246+
if isTime(l) && isTime(r) {
247+
return durationType
248+
}
249+
250+
case "/", "*":
237251
if isNumber(l) && isNumber(r) {
238252
return combined(l, r)
239253
}
@@ -255,6 +269,12 @@ func (v *visitor) BinaryNode(node *ast.BinaryNode) reflect.Type {
255269
if isString(l) && isString(r) {
256270
return stringType
257271
}
272+
if isTime(l) && isDuration(r) {
273+
return timeType
274+
}
275+
if isDuration(l) && isTime(r) {
276+
return timeType
277+
}
258278

259279
case "contains", "startsWith", "endsWith":
260280
if isString(l) && isString(r) {
@@ -348,9 +368,9 @@ func (v *visitor) FunctionNode(node *ast.FunctionNode) reflect.Type {
348368
fn.NumIn() == inputParamsCount &&
349369
((fn.NumOut() == 1 && // Function with one return value
350370
fn.Out(0).Kind() == reflect.Interface) ||
351-
(fn.NumOut() == 2 && // Function with one return value and an error
352-
fn.Out(0).Kind() == reflect.Interface &&
353-
fn.Out(1) == errorType)) {
371+
(fn.NumOut() == 2 && // Function with one return value and an error
372+
fn.Out(0).Kind() == reflect.Interface &&
373+
fn.Out(1) == errorType)) {
354374
rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
355375
if rest.Kind() == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
356376
node.Fast = true

checker/checker_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,31 @@ func TestCheck(t *testing.T) {
176176
"count(1..30, {# % 3 == 0}) > 0",
177177
"map(1..3, {#}) == [1,2,3]",
178178
"map(filter(ArrayOfFoo, {.Int64 > 0}), {.Bar})",
179+
"Any == Time",
180+
"Any != Time",
181+
"Any > Time",
182+
"Any >= Time",
183+
"Any < Time",
184+
"Any <= Time",
185+
"Any - Time",
186+
"Any == Any",
187+
"Any != Any",
188+
"Any > Any",
189+
"Any >= Any",
190+
"Any < Any",
191+
"Any <= Any",
192+
"Any - Any",
193+
"Time == Any",
194+
"Time != Any",
195+
"Time > Any",
196+
"Time >= Any",
197+
"Time < Any",
198+
"Time <= Any",
199+
"Time - Any",
200+
"Any + Duration",
201+
"Duration + Any",
202+
"Time + Duration",
203+
"Duration + Time",
179204
}
180205
for _, test := range typeTests {
181206
var err error
@@ -634,6 +659,8 @@ type mockEnv2 struct {
634659
BoolFn func() bool
635660
NilFn func()
636661
Variadic func(head string, xs ...int) int
662+
Time time.Time
663+
Duration time.Duration
637664
}
638665

639666
func (p mockEnv2) Method(_ bar) int {

checker/types.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package checker
22

33
import (
44
"reflect"
5+
"time"
56

67
"github.com/antonmedv/expr/ast"
78
)
@@ -15,6 +16,8 @@ var (
1516
arrayType = reflect.TypeOf([]interface{}{})
1617
mapType = reflect.TypeOf(map[string]interface{}{})
1718
interfaceType = reflect.TypeOf(new(interface{})).Elem()
19+
timeType = reflect.TypeOf(time.Time{})
20+
durationType = reflect.TypeOf(time.Duration(0))
1821
)
1922

2023
func typeWeight(t reflect.Type) int {
@@ -125,6 +128,28 @@ func isNumber(t reflect.Type) bool {
125128
return isInteger(t) || isFloat(t)
126129
}
127130

131+
func isTime(t reflect.Type) bool {
132+
t = dereference(t)
133+
if t != nil {
134+
switch t {
135+
case timeType:
136+
return true
137+
}
138+
}
139+
return isInterface(t)
140+
}
141+
142+
func isDuration(t reflect.Type) bool {
143+
t = dereference(t)
144+
if t != nil {
145+
switch t {
146+
case durationType:
147+
return true
148+
}
149+
}
150+
return false
151+
}
152+
128153
func isBool(t reflect.Type) bool {
129154
t = dereference(t)
130155
if t != nil {

expr_test.go

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,10 @@ func TestExpr_readme_example(t *testing.T) {
474474

475475
func TestExpr(t *testing.T) {
476476
date := time.Date(2017, time.October, 23, 18, 30, 0, 0, time.UTC)
477+
tnow := time.Now()
478+
oneDay, _ := time.ParseDuration("24h")
479+
tnowPlusOne := tnow.Add(oneDay)
480+
477481
env := &mockEnv{
478482
Any: "any",
479483
Int: 0,
@@ -494,12 +498,14 @@ func TestExpr(t *testing.T) {
494498
{Origin: "MOW", Destination: "LED"},
495499
{Origin: "LED", Destination: "MOW"},
496500
},
497-
BirthDay: date,
498-
Now: time.Now(),
499-
One: 1,
500-
Two: 2,
501-
Three: 3,
502-
MultiDimArray: [][]int{{1, 2, 3}, {1, 2, 3}},
501+
BirthDay: date,
502+
Now: tnow,
503+
NowPlusOne: tnowPlusOne,
504+
OneDayDuration: oneDay,
505+
One: 1,
506+
Two: 2,
507+
Three: 3,
508+
MultiDimArray: [][]int{{1, 2, 3}, {1, 2, 3}},
503509
Sum: func(list []int) int {
504510
var ret int
505511
for _, el := range list {
@@ -884,6 +890,50 @@ func TestExpr(t *testing.T) {
884890
`Concat("a", 1, [])`,
885891
`a1[]`,
886892
},
893+
{
894+
`Tweets[0].Date < Now`,
895+
true,
896+
},
897+
{
898+
`Now > Tweets[0].Date`,
899+
true,
900+
},
901+
{
902+
`Now == Now`,
903+
true,
904+
},
905+
{
906+
`Now >= Now`,
907+
true,
908+
},
909+
{
910+
`Now <= Now`,
911+
true,
912+
},
913+
{
914+
`Now == NowPlusOne`,
915+
false,
916+
},
917+
{
918+
`Now != Now`,
919+
false,
920+
},
921+
{
922+
`Now != NowPlusOne`,
923+
true,
924+
},
925+
{
926+
`NowPlusOne - Now`,
927+
oneDay,
928+
},
929+
{
930+
`Now + OneDayDuration`,
931+
tnowPlusOne,
932+
},
933+
{
934+
`OneDayDuration + Now`,
935+
tnowPlusOne,
936+
},
887937
}
888938

889939
for _, tt := range tests {
@@ -1339,9 +1389,7 @@ func TestIssue138(t *testing.T) {
13391389
require.Error(t, err)
13401390
}
13411391

1342-
//
13431392
// Mock types
1344-
//
13451393
type mockEnv struct {
13461394
Any interface{}
13471395
Int, One, Two, Three int
@@ -1360,6 +1408,8 @@ type mockEnv struct {
13601408
Segments []*segment
13611409
BirthDay time.Time
13621410
Now time.Time
1411+
NowPlusOne time.Time
1412+
OneDayDuration time.Duration
13631413
Nil interface{}
13641414
NilStruct *time.Time
13651415
NilInt *int

file/source_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ func TestStringSource_SnippetSingleLine(t *testing.T) {
4848
t.Errorf(unexpectedSnippet, t.Name(), str, "hello, world")
4949
}
5050
if str2, found := source.Snippet(2); found {
51-
t.Error(snippetFound, t.Name(), 2)
51+
t.Errorf(snippetFound, t.Name(), 2)
5252
} else if str2 != "" {
53-
t.Error(unexpectedSnippet, t.Name(), str2, "")
53+
t.Errorf(unexpectedSnippet, t.Name(), str2, "")
5454
}
5555
}

vm/helpers.go

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)