š evaluator - Awesome Go Library for Utilities
Evaluate an expression dynamically based on s-expression. It's simple and easy to extend.
Detailed Description of evaluator
It's very common to evaluate an expression dynamicly, so that's why we are here.
S-expression
We use s-epxression syntax to parse and evaluate.
In computing, s-expressions, sexprs or sexps (for "symbolic expression") are a notation for nested list (tree-structured) data, invented for and popularized by the programming language Lisp, which uses them for source code as well as data.
For example, for expression in common way:
ā
(
(gender = "female")
and
((age % 2) != 0)
)
it's coresponding format in s-expression is:
ā
(and
(= gender "female")
(!=
(% age 2)
0
)
)
Element types within expression
-
number
For convenience, we treat float64, int64 and so on as type of number. For example, float100.0
is equal to int100
, but not euqal to string"100"
-
string
character string quoted with`
,'
, or"
are treated as type ofstring
. You can convert typestring
to any other defined type you like by type convert functions which are mentioned later -
function or variable
character string without quotes are regarded as type offunction
orvariable
which depends on whether this function exists. For example in expression(age birthdate)
, bothage
andbirthdate
is unquoted.age
is type of function because we have registered a function namedage
, whilebirthdate
is type of variable for not found. The program will come to errors if there is neither parameter nor function namedbirthdate
when evaluating
How to
You can evaluate directly:
params := evaluator.MapParams{
"gender": "female",
}
res, err := evaluator.EvalBool(`(in gender ("female" "male"))`, params)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
# true
or you can reuse the Expression
to evaluate multiple times:
params := evaluator.MapParams{
"gender": "female",
}
exp, err := evaluator.New(`(in gender ("female" "male"))`)
if err != nil {
log.Fatal(err)
}
res, err := exp.EvalBool(params)
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
# true
And you can write expressions like this
-
(in gender ("male", "female"))
-
(between now (td_time "2017-01-02 12:00:00") (td_time "2017-12-02 12:00:00"))
-
(ne (mod (age birthdate) 7) 5)
-
or multiple-line for clarity
(and (ne os "ios") (eq gender "male") (beteen version (t_version "2.7.1") (t_version "2.9.1")) )
Functions
Implemented functions
operand | function | example | description |
---|---|---|---|
- | in | (in 1 (1 2)) | also suport array like (in (1) ((1))) |
- | between | (between age 18 20) | |
- | overlap | (overlap region (3142 1860)) | |
& | and | (and (eq gender "femal") (between age 18 20)) | |
` | ` | or | |
! | not | ||
= | eq | equal | |
!= | ne | not equal | |
> | gt | greater than | |
< | lt | less than | |
>= | ge | greater than or equal to | |
<= | le | less than or equal to | |
% | mod | ||
+ | - | plus | |
- | - | minus | |
* | - | multiply | |
/ | - | divide | |
- | t_version | convert type to version | |
- | t_time | (t_time "2006-01-02 15:04" "2017-09-09 12:00") | convert type to time, first param must be the layout for the time |
- | td_time | (td_time "2017:09:09 12:00:00) | convert type to time of default layout format 2006-01-02 15:04:05 |
_ | td_date | (in (td_date now) (td_date ("2017-01-02" "2017-02-01")) ) | convert type to time of default layout format 2006-01-02 |
p.s. either operand or function can be used in expression
How to use self-defined functions
Yes, you can write your own function by following thses steps:
- implement your function
- regist to functions
- enjoy it
here is an example:
package main
import (
"errors"
"log"
"time"
"github.com/nullne/evaluator"
"github.com/nullne/evaluator/function"
)
// define your own function and don't forget to register
func age(params ...interface{}) (interface{}, error) {
if len(params) != 1 {
return nil, errors.New("only one params accepted")
}
birth, ok := params[0].(string)
if !ok {
return nil, errors.New("birth format need to be string")
}
r, err := time.Parse("2006-01-02", birth)
if err != nil {
return nil, err
}
now := time.Now()
a := r.Year() - now.Year()
if r.Month() < now.Month() {
a--
} else if r.Month() == now.Month() {
if r.Day() < now.Day() {
a--
}
}
return a, nil
}
func main() {
if err := function.Regist("age", age); err != nil {
log.Print(err)
}
exp := `(not (between (age birthdate) 18 20))`
vvf := evaluator.MapParams{
"birthdate": "1980-02-01",
}
e, err := evaluator.New(exp)
if err != nil {
log.Print(err)
}
r, err := e.Eval(vvf)
if err != nil {
log.Print(err)
}
log.Printf("expression: `%s`, wanna: %+v, got: %+v\r", exp, true, r)
}
Params
Params
interface, which has a method namedGet
to get all params neededMapParams
a simple implementedParams
inmap
Bench
BenchmarkEqualString-8 3000000 473 ns/op
BenchmarkInString-8 2000000 916 ns/op
BenchmarkBetweenInt-8 3000000 467 ns/op
BenchmarkBetweenTime-8 1000000 2089 ns/op
BenchmarkOverlapInt-8 500000 2966 ns/op
BenchmarkTypeTime-8 2000000 638 ns/op
BenchmarkTypeVersion-8 3000000 539 ns/op
p.s. on MacBook Pro (Retina, 15-inch, Mid 2015), Memory: 16 GB 1600 MHz DDR3, Processor: 2.2 GHz Intel Core i7