diff --git a/.gitignore b/.gitignore index 93f3f29..c0fe392 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Folders _obj _test +test # Architecture specific extensions/prefixes *.[568vq] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..40d24b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ + +.PHONY: test + + +lint: + go fmt ./... + go vet ./... + +test: + @mkdir -p test + go test ./... -race -coverprofile=test/coverage.out ./... + diff --git a/README.md b/README.md index a4737af..14fe462 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ Currently only the following operators are supported. |-|-|-| |`And`|`&&`|| |`Or`||||| -|`Equal`, `Eq`|`==`|`string`, `float64`| -|`NotEqual`, `Neq`|`!=`|`string`, `float64`| -|`GreaterThan `, `Gt`|`>`|`string`, `float64`| -|`GreaterThanEqual `, `Gte`|`>=`|`string`, `float64`| -|`LessThan `, `Lt`|`<`|`string`, `float64`| -|`LessThanEqual `, `Lte`|`<=`|`string`, `float64`| +|`Equal`, `Eq`|`==`|`string`, `float64`, `RFC3339 timestamp`| +|`NotEqual`, `Neq`|`!=`|`string`, `float64`, `RFC3339 timestamp`| +|`GreaterThan `, `Gt`|`>`|`string`, `float64`, `RFC3339 timestamp`| +|`GreaterThanEqual `, `Gte`|`>=`|`string`, `float64`, `RFC3339 timestamp`| +|`LessThan `, `Lt`|`<`|`string`, `float64`, `RFC3339 timestamp`| +|`LessThanEqual `, `Lte`|`<=`|`string`, `float64`, `RFC3339 timestamp`| ## Documentation diff --git a/exp.go b/exp.go index 0cd7142..beb5942 100644 --- a/exp.go +++ b/exp.go @@ -88,6 +88,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + v = l.Value().Value } switch r.Value().Type { case parse.T_IDENTIFIER: @@ -100,6 +102,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + v = r.Value().Value } if v == "" { return Equal(k, f), nil @@ -123,6 +127,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + v = l.Value().Value } switch r.Value().Type { case parse.T_IDENTIFIER: @@ -135,6 +141,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + v = r.Value().Value } if v == "" { return NotEqual(k, f), nil @@ -142,7 +150,7 @@ func visit(t parse.Tree) (Exp, error) { return Not(Match(k, v)), nil case parse.T_IS_GREATER: var ( - k string + k, tm string v float64 l = t.Left() r = t.Right() @@ -156,6 +164,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = l.Value().Value } switch r.Value().Type { case parse.T_IDENTIFIER: @@ -166,11 +176,16 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = r.Value().Value + } + if tm != "" { + return TimeGreaterThan(k, tm), nil } return GreaterThan(k, v), nil case parse.T_IS_GREATER_OR_EQUAL: var ( - k string + k, tm string v float64 l = t.Left() r = t.Right() @@ -184,6 +199,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = l.Value().Value } switch r.Value().Type { case parse.T_IDENTIFIER: @@ -194,11 +211,16 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = r.Value().Value + } + if tm != ""{ + return TimeGreaterOrEqual(k, tm), nil } return GreaterOrEqual(k, v), nil case parse.T_IS_SMALLER: var ( - k string + k, tm string v float64 l = t.Left() r = t.Right() @@ -212,6 +234,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = l.Value().Value } switch r.Value().Type { case parse.T_IDENTIFIER: @@ -222,11 +246,16 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = r.Value().Value + } + if tm != "" { + return TimeLessThan(k, tm), nil } return LessThan(k, v), nil case parse.T_IS_SMALLER_OR_EQUAL: var ( - k string + k , tm string v float64 l = t.Left() r = t.Right() @@ -240,6 +269,8 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = l.Value().Value } switch r.Value().Type { case parse.T_IDENTIFIER: @@ -250,6 +281,11 @@ func visit(t parse.Tree) (Exp, error) { if err != nil { return nil, err } + case parse.T_TIMESTAMP: + tm = r.Value().Value + } + if tm != ""{ + return TimeLessOrEqual(k, tm), nil } return LessOrEqual(k, v), nil } diff --git a/exp_test.go b/exp_test.go index 1dc387e..e988f90 100644 --- a/exp_test.go +++ b/exp_test.go @@ -49,3 +49,25 @@ func TestParse(t *testing.T) { t.Logf("%s", exp) } } + +func TestTimestampParse(t *testing.T) { + m := Map{ + "timestamp": "2020-04-07T20:05:00+05:30", + } + for _, s := range []string{ + `(timestamp == "2020-04-07T20:05:00+05:30")`, + `(timestamp > "2020-04-07T20:04:00+05:30")`, + `(timestamp < "2020-04-07T20:06:00+05:30")`, + `(timestamp <= "2020-04-07T20:05:00+05:30")`, + `(timestamp >= "2020-04-07T20:05:00+05:30")`, + } { + exp, err := Parse(s) + if err != nil { + t.Fatal(err) + } + if !exp.Eval(m) { + t.Error("unexpected output") + } + t.Logf("%s", exp) + } +} diff --git a/parse/lex.go b/parse/lex.go index ad95a02..43038cc 100644 --- a/parse/lex.go +++ b/parse/lex.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "strings" + "time" "unicode" "unicode/utf8" ) @@ -36,6 +37,7 @@ const ( T_NUMBER T_STRING T_BOOLEAN + T_TIMESTAMP T_LOGICAL_AND T_LOGICAL_OR @@ -331,7 +333,11 @@ loop: switch l.next() { case '"': l.backup() - l.emit(T_STRING) + if isTimestamp(l.buffer()) { + l.emit(T_TIMESTAMP) + } else { + l.emit(T_STRING) + } l.next() l.ignore() break loop @@ -380,3 +386,11 @@ func isAlphanum(r rune) bool { func isOperator(r rune) bool { return r == '=' || r == '!' || r == '>' || r == '<' || r == '&' || r == '|' } + +// isTimestamp reports whether r is a Timestamp. Timestamp supports time.RFC3339 +func isTimestamp(r string) bool { + if _, err := time.Parse(time.RFC3339, r); err != nil { + return false + } + return true +} diff --git a/parse/parse.go b/parse/parse.go index e8c7371..f29efb7 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -55,7 +55,7 @@ loop: node.right = newTree() stack.push(node) node = node.right - case T_IDENTIFIER, T_NUMBER, T_STRING, T_BOOLEAN: + case T_IDENTIFIER, T_NUMBER, T_STRING, T_BOOLEAN, T_TIMESTAMP: node.value = token node, err = stack.pop() if err != nil { diff --git a/timestamp.go b/timestamp.go new file mode 100644 index 0000000..8f539e2 --- /dev/null +++ b/timestamp.go @@ -0,0 +1,224 @@ +package exp + +import ( + "regexp" + "strings" + "time" +) + +/* +Perform Timestamp eq(==), gt(>), lt(<), gte(>) and lte(<=) operations. +time.RFC3339 Timestamp is supported. Timestamp will be converted to epcoh value for operations. +*/ + +// Eq +type expTimeEq struct { + key string + value string +} + +func (eq expTimeEq) Eval(p Params) bool { + var pValue, cValue int64 + var err error + pValue, err = getEpochValue(p.Get(eq.key)) + if err != nil { + return false + } + cValue, err = getEpochValue(eq.value) + if err != nil { + return false + } + + return pValue == cValue +} + +func (eq expTimeEq) String() string { + return sprintf("(%s==%s)", eq.key, eq.value) +} + +// TimeEqual ... +func TimeEqual(k string, v string) Exp { + return expTimeEq{k, v} +} + +// TimeEq is an alias for TimeEqual ... +func TimeEq(k string, v string) Exp { + return TimeEqual(k, v) +} + +// Gt +type expTimeGt struct { + key string + value string +} + +func (gt expTimeGt) Eval(p Params) bool { + var pValue, cValue int64 + var err error + pValue, err = getEpochValue(p.Get(gt.key)) + if err != nil { + return false + } + cValue, err = getEpochValue(gt.value) + if err != nil { + return false + } + + return pValue > cValue +} + +func (gt expTimeGt) String() string { + return sprintf("(%s>%s)", gt.key, gt.value) +} + +// TimeGreaterThan ... +func TimeGreaterThan(k string, v string) Exp { + return expTimeGt{k, v} +} + +// TimeGt is an alias for TimeGreaterThan ... +func TimeGt(k string, v string) Exp { + return TimeGreaterThan(k, v) +} + +type expTimeGte struct { + key string + value string +} + +func (gte expTimeGte) Eval(p Params) bool { + var pValue, cValue int64 + var err error + pValue, err = getEpochValue(p.Get(gte.key)) + if err != nil { + return false + } + cValue, err = getEpochValue(gte.value) + if err != nil { + return false + } + return pValue > cValue || pValue == cValue +} + +func (gte expTimeGte) String() string { + return sprintf("(%s>=%s)", gte.key, gte.value) +} + +// TimeGreaterOrEqual ... +func TimeGreaterOrEqual(k string, v string) Exp { + return expTimeGte{k, v} +} + +// TimeGte is an alias for TimeGreaterOrEqual ... +func TimeGte(k string, v string) Exp { + return TimeGreaterOrEqual(k, v) +} + +// Lt ... +type expTimeLt struct { + key string + value string +} + +func (lt expTimeLt) Eval(p Params) bool { + var pValue, cValue int64 + var err error + pValue, err = getEpochValue(p.Get(lt.key)) + if err != nil { + return false + } + cValue, err = getEpochValue(lt.value) + if err != nil { + return false + } + return pValue < cValue +} + +func (lt expTimeLt) String() string { + return sprintf("(%s<%s)", lt.key, lt.value) +} + +// TimeLessThan ... +func TimeLessThan(k string, v string) Exp { + return expTimeLt{k, v} +} + +// TimeLt is an alias for TimeLessThan ... +func TimeLt(k string, v string) Exp { + return TimeLessThan(k, v) +} + +type expTimeLte struct { + key string + value string +} + +func (lte expTimeLte) Eval(p Params) bool { + var pValue, cValue int64 + var err error + pValue, err = getEpochValue(p.Get(lte.key)) + if err != nil { + return false + } + cValue, err = getEpochValue(lte.value) + if err != nil { + return false + } + return pValue < cValue || pValue == cValue +} + +func (lte expTimeLte) String() string { + return sprintf("(%s<=%s)", lte.key, lte.value) +} + +// TimeLessOrEqual ... +func TimeLessOrEqual(k string, v string) Exp { + return expTimeLte{k, v} +} + +// TimeLte is an alias for TimeLessOrEqual ... +func TimeLte(k string, v string) Exp { + return TimeLessOrEqual(k, v) +} + +func getEpochValue(pVal string) (value int64, err error) { + if pVal == "" { + value = int64(0) + } else { + value, err = convertTimestampToEpoch(pVal) + if err != nil { + return + } + } + return +} + +// convertTimestampToEpoch convert RFC3339 timestamp to epoch value +func convertTimestampToEpoch(val string) (value int64, err error) { + t, err := time.Parse(time.RFC3339, val) + if err != nil { + return + } + return t.UTC().Unix(), nil +} + +// Regex +type expTimeContains struct { + key, substr string + substrRegex *regexp.Regexp +} + +// TimeContains ... +//Contains is an expression that evaluates to true if timestamp as a substr is within the value pointed to by key. +func TimeContains(key, str string) Exp { + regex := regexp.MustCompile("(?i)" + strings.ReplaceAll(str, "+", "\\+")) + return expTimeContains{key, str, regex} +} + +func (e expTimeContains) Eval(p Params) bool { + return e.substrRegex.MatchString(p.Get(e.key)) +} + +func (e expTimeContains) String() string { + return sprintf("(%s:\"%s\")", e.key, e.substr) +} diff --git a/timestamp_test.go b/timestamp_test.go new file mode 100644 index 0000000..7e16be7 --- /dev/null +++ b/timestamp_test.go @@ -0,0 +1,130 @@ +package exp + +import ( + "testing" +) + +type testcase struct { + key string + exp map[string]string + data []Data +} + +type Data struct { + InputData Map + Result bool +} + +func TestTimeEqual(t *testing.T) { + param := "timestamp" + testCases := []testcase{ + {param, map[string]string{param: "2020-04-07T20:05:00+05:30"}, + []Data{ + {map[string]string{param: "2020-04-07T20:05:00+05:30"}, true}, + {map[string]string{param: "2020-05-07T20:05:00+05:30"}, false}, + {map[string]string{param: "2020-03-07T20:05:00+05:30"}, false}, + }}} + + for _, itr := range testCases { + for _, d := range itr.data { + if !d.Result == TimeEqual(itr.key, itr.exp[param]).Eval(d.InputData) { + t.Errorf("onTestTimeEqual(%q = %q) should evaluate to %v", d.InputData.Get(param), itr.exp[param], d.Result) + } + } + } +} + +func TestTimeGreaterThan(t *testing.T) { + param := "timestamp" + testCases := []testcase{ + {param, map[string]string{param: "2020-04-07T20:05:00+05:30"}, + []Data{ + {map[string]string{param: "2020-04-07T20:05:00+05:30"}, false}, + {map[string]string{param: "2020-04-07T20:05:01+05:30"}, true}, + {map[string]string{param: "2020-04-07T20:04:59+05:30"}, false}, + }}} + + for _, itr := range testCases { + for _, d := range itr.data { + if !d.Result == TimeGreaterThan(itr.key, itr.exp[param]).Eval(d.InputData) { + t.Errorf("OnTestTimeGreaterThan(%q > %q) should evaluate to %v", d.InputData.Get(param), itr.exp[param], d.Result) + } + } + } +} + +func TestTimeGreaterOrEqual(t *testing.T) { + param := "timestamp" + testCases := []testcase{ + {param, map[string]string{param: "2020-04-07T20:05:00+05:30"}, + []Data{ + {map[string]string{param: "2020-04-07T20:05:00+05:30"}, true}, + {map[string]string{param: "2020-04-07T20:05:01+05:30"}, true}, + {map[string]string{param: "2020-04-07T20:04:00+05:30"}, false}, + }}} + + for _, itr := range testCases { + for _, d := range itr.data { + if !d.Result == TimeGreaterOrEqual(itr.key, itr.exp[param]).Eval(d.InputData) { + t.Errorf("OnTimeGreaterOrEqual(%q >= %q) should evaluate to %v", d.InputData.Get(param), itr.exp[param], d.Result) + } + } + } +} + +func TestOnTimeLessThan(t *testing.T) { + param := "timestamp" + testCases := []testcase{ + {param, map[string]string{param: "2020-04-07T20:05:00+05:30"}, + []Data{ + {map[string]string{param: "2020-04-07T20:05:00+05:30"}, false}, + {map[string]string{param: "2020-04-07T20:05:01+05:30"}, false}, + {map[string]string{param: "2020-04-07T20:04:00+05:30"}, true}, + }}} + + for _, itr := range testCases { + for _, d := range itr.data { + if !d.Result == TimeLessThan(itr.key, itr.exp[param]).Eval(d.InputData) { + t.Errorf("OnTimeLessThan(%q < %q) should evaluate to %v", d.InputData.Get(param), itr.exp[param], d.Result) + } + } + } +} + +func TestOnTimeLessOrEqual(t *testing.T) { + param := "timestamp" + testCases := []testcase{ + {param, map[string]string{param: "2020-04-07T20:05:00+05:30"}, + []Data{ + {map[string]string{param: "2020-04-07T20:05:00+05:30"}, true}, + {map[string]string{param: "2020-04-07T20:05:01+05:30"}, false}, + {map[string]string{param: "2020-04-07T20:04:00+05:30"}, true}, + }}} + + for _, itr := range testCases { + for _, d := range itr.data { + if !d.Result == TimeLessOrEqual(itr.key, itr.exp[param]).Eval(d.InputData) { + t.Errorf("OnTimeLessOrEqual(%q <= %q) should evaluate to %v", d.InputData.Get(param), itr.exp[param], d.Result) + } + } + } +} + +func TestOnTimeContains(t *testing.T) { + param := "timestamp" + testCases := []testcase{ + {param, map[string]string{param: "2020-04-07T20:05:00+05:30"}, + []Data{ + {map[string]string{param: "This 2020-04-07T20:05:00+05:30 is matching timestamp "}, true}, + {map[string]string{param: "2020-04-07T20:05:00+05:30"}, true}, + {map[string]string{param: "This 2020-04-07T20:04:00+05:30 is not matching timestamp"}, false}, + }}} + + for _, itr := range testCases { + for _, d := range itr.data { + if !d.Result == TimeContains(itr.key, itr.exp[param]).Eval(d.InputData) { + t.Errorf("OnTimeContains(%q : %q) should evaluate to %v", d.InputData.Get(param), itr.exp[param], d.Result) + } + } + } +}