Skip to content

Bizywizy compare arrays (rebase) #627

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

Closed
wants to merge 15 commits into from
Closed
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 README.md
Original file line number Diff line number Diff line change
@@ -162,6 +162,7 @@ func main() {
* [Visually.io](https://visually.io) employs Expr as a business rule engine for its personalization targeting algorithm.
* [Akvorado](https://github.com/akvorado/akvorado) utilizes Expr to classify exporters and interfaces in network flows.
* [keda.sh](https://keda.sh) uses Expr to allow customization of its Kubernetes-based event-driven autoscaling.
* [Span Digital](https://spandigital.com/) uses Expr in it's Knowledge Management products.

[Add your company too](https://github.com/expr-lang/expr/edit/master/README.md)

8 changes: 7 additions & 1 deletion ast/print.go
Original file line number Diff line number Diff line change
@@ -202,5 +202,11 @@ func (n *MapNode) String() string {
}

func (n *PairNode) String() string {
return fmt.Sprintf("%s: %s", n.Key.String(), n.Value.String())
if str, ok := n.Key.(*StringNode); ok {
if utils.IsValidIdentifier(str.Value) {
return fmt.Sprintf("%s: %s", str.Value, n.Value.String())
}
return fmt.Sprintf("%q: %s", str.String(), n.Value.String())
}
return fmt.Sprintf("(%s): %s", n.Key.String(), n.Value.String())
}
5 changes: 3 additions & 2 deletions ast/print_test.go
Original file line number Diff line number Diff line change
@@ -55,8 +55,8 @@ func TestPrint(t *testing.T) {
{`func(a)`, `func(a)`},
{`func(a, b)`, `func(a, b)`},
{`{}`, `{}`},
{`{a: b}`, `{"a": b}`},
{`{a: b, c: d}`, `{"a": b, "c": d}`},
{`{a: b}`, `{a: b}`},
{`{a: b, c: d}`, `{a: b, c: d}`},
{`[]`, `[]`},
{`[a]`, `[a]`},
{`[a, b]`, `[a, b]`},
@@ -71,6 +71,7 @@ func TestPrint(t *testing.T) {
{`a[1:]`, `a[1:]`},
{`a[:]`, `a[:]`},
{`(nil ?? 1) > 0`, `(nil ?? 1) > 0`},
{`{("a" + "b"): 42}`, `{("a" + "b"): 42}`},
}

for _, tt := range tests {
201 changes: 29 additions & 172 deletions builtin/builtin.go
Original file line number Diff line number Diff line change
@@ -135,42 +135,21 @@ var Builtins = []*Function{
Name: "ceil",
Fast: Ceil,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Interface:
return floatType, nil
}
return anyType, fmt.Errorf("invalid argument for ceil (type %s)", args[0])
return validateRoundFunc("ceil", args)
},
},
{
Name: "floor",
Fast: Floor,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Interface:
return floatType, nil
}
return anyType, fmt.Errorf("invalid argument for floor (type %s)", args[0])
return validateRoundFunc("floor", args)
},
},
{
Name: "round",
Fast: Round,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Interface:
return floatType, nil
}
return anyType, fmt.Errorf("invalid argument for floor (type %s)", args[0])
return validateRoundFunc("round", args)
},
},
{
@@ -392,185 +371,63 @@ var Builtins = []*Function{
},
{
Name: "max",
Func: Max,
Func: func(args ...any) (any, error) {
return minMax("max", runtime.Less, args...)
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
switch len(args) {
case 0:
return anyType, fmt.Errorf("not enough arguments to call max")
case 1:
if kindName := kind(args[0]); kindName == reflect.Array || kindName == reflect.Slice {
return anyType, nil
}
fallthrough
default:
for _, arg := range args {
switch kind(arg) {
case reflect.Interface, reflect.Array, reflect.Slice:
return anyType, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
default:
return anyType, fmt.Errorf("invalid argument for max (type %s)", arg)
}
}
return args[0], nil
}
return validateAggregateFunc("max", args)
},
},
{
Name: "min",
Func: Min,
Func: func(args ...any) (any, error) {
return minMax("min", runtime.More, args...)
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
switch len(args) {
case 0:
return anyType, fmt.Errorf("not enough arguments to call min")
case 1:
if kindName := kind(args[0]); kindName == reflect.Array || kindName == reflect.Slice {
return anyType, nil
}
fallthrough
default:
for _, arg := range args {
switch kind(arg) {
case reflect.Interface, reflect.Array, reflect.Slice:
return anyType, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
default:
return anyType, fmt.Errorf("invalid argument for min (type %s)", arg)
}
}
return args[0], nil

}
return validateAggregateFunc("min", args)
},
},
{
Name: "sum",
Func: func(args ...any) (any, error) {
if len(args) != 1 {
return nil, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
v := reflect.ValueOf(args[0])
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return nil, fmt.Errorf("cannot sum %s", v.Kind())
}
sum := int64(0)
i := 0
for ; i < v.Len(); i++ {
it := deref.Value(v.Index(i))
if it.CanInt() {
sum += it.Int()
} else if it.CanFloat() {
goto float
} else {
return nil, fmt.Errorf("cannot sum %s", it.Kind())
}
}
return int(sum), nil
float:
fSum := float64(sum)
for ; i < v.Len(); i++ {
it := deref.Value(v.Index(i))
if it.CanInt() {
fSum += float64(it.Int())
} else if it.CanFloat() {
fSum += it.Float()
} else {
return nil, fmt.Errorf("cannot sum %s", it.Kind())
}
}
return fSum, nil
},
Func: sum,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Interface, reflect.Slice, reflect.Array:
default:
return anyType, fmt.Errorf("cannot sum %s", args[0])
}
return anyType, nil
return validateAggregateFunc("sum", args)
},
},
{
Name: "mean",
Func: func(args ...any) (any, error) {
if len(args) != 1 {
return nil, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
v := reflect.ValueOf(args[0])
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return nil, fmt.Errorf("cannot mean %s", v.Kind())
count, sum, err := mean(args...)
if err != nil {
return nil, err
}
if v.Len() == 0 {
if count == 0 {
return 0.0, nil
}
sum := float64(0)
i := 0
for ; i < v.Len(); i++ {
it := deref.Value(v.Index(i))
if it.CanInt() {
sum += float64(it.Int())
} else if it.CanFloat() {
sum += it.Float()
} else {
return nil, fmt.Errorf("cannot mean %s", it.Kind())
}
}
return sum / float64(i), nil
return sum / float64(count), nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Interface, reflect.Slice, reflect.Array:
default:
return anyType, fmt.Errorf("cannot avg %s", args[0])
}
return floatType, nil
return validateAggregateFunc("mean", args)
},
},
{
Name: "median",
Func: func(args ...any) (any, error) {
if len(args) != 1 {
return nil, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
v := reflect.ValueOf(args[0])
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return nil, fmt.Errorf("cannot median %s", v.Kind())
}
if v.Len() == 0 {
return 0.0, nil
values, err := median(args...)
if err != nil {
return nil, err
}
s := make([]float64, v.Len())
for i := 0; i < v.Len(); i++ {
it := deref.Value(v.Index(i))
if it.CanInt() {
s[i] = float64(it.Int())
} else if it.CanFloat() {
s[i] = it.Float()
} else {
return nil, fmt.Errorf("cannot median %s", it.Kind())
if n := len(values); n > 0 {
sort.Float64s(values)
if n%2 == 1 {
return values[n/2], nil
}
return (values[n/2-1] + values[n/2]) / 2, nil
}
sort.Float64s(s)
if len(s)%2 == 0 {
return (s[len(s)/2-1] + s[len(s)/2]) / 2, nil
}
return s[len(s)/2], nil
return 0.0, nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Interface, reflect.Slice, reflect.Array:
default:
return anyType, fmt.Errorf("cannot median %s", args[0])
}
return floatType, nil
return validateAggregateFunc("median", args)
},
},
{
27 changes: 27 additions & 0 deletions builtin/builtin_test.go
Original file line number Diff line number Diff line change
@@ -85,19 +85,29 @@ func TestBuiltin(t *testing.T) {
{`min(1.5, 2.5, 3.5)`, 1.5},
{`min([1, 2, 3])`, 1},
{`min([1.5, 2.5, 3.5])`, 1.5},
{`min(-1, [1.5, 2.5, 3.5])`, -1},
{`sum(1..9)`, 45},
{`sum([.5, 1.5, 2.5])`, 4.5},
{`sum([])`, 0},
{`sum([1, 2, 3.0, 4])`, 10.0},
{`sum(10, [1, 2, 3], 1..9)`, 61},
{`sum(-10, [1, 2, 3, 4])`, 0},
{`sum(-10.9, [1, 2, 3, 4, 9])`, 8.1},
{`mean(1..9)`, 5.0},
{`mean([.5, 1.5, 2.5])`, 1.5},
{`mean([])`, 0.0},
{`mean([1, 2, 3.0, 4])`, 2.5},
{`mean(10, [1, 2, 3], 1..9)`, 4.6923076923076925},
{`mean(-10, [1, 2, 3, 4])`, 0.0},
{`mean(10.9, 1..9)`, 5.59},
{`median(1..9)`, 5.0},
{`median([.5, 1.5, 2.5])`, 1.5},
{`median([])`, 0.0},
{`median([1, 2, 3])`, 2.0},
{`median([1, 2, 3, 4])`, 2.5},
{`median(10, [1, 2, 3], 1..9)`, 4.0},
{`median(-10, [1, 2, 3, 4])`, 2.0},
{`median(1..5, 4.9)`, 3.5},
{`toJSON({foo: 1, bar: 2})`, "{\n \"bar\": 2,\n \"foo\": 1\n}"},
{`fromJSON("[1, 2, 3]")`, []any{1.0, 2.0, 3.0}},
{`toBase64("hello")`, "aGVsbG8="},
@@ -207,6 +217,9 @@ func TestBuiltin_errors(t *testing.T) {
{`min()`, `not enough arguments to call min`},
{`min(1, "2")`, `invalid argument for min (type string)`},
{`min([1, "2"])`, `invalid argument for min (type string)`},
{`median(1..9, "t")`, "invalid argument for median (type string)"},
{`mean("s", 1..9)`, "invalid argument for mean (type string)"},
{`sum("s", "h")`, "invalid argument for sum (type string)"},
{`duration("error")`, `invalid duration`},
{`date("error")`, `invalid date`},
{`get()`, `invalid number of arguments (expected 2, got 0)`},
@@ -599,3 +612,17 @@ func TestBuiltin_bitOpsFunc(t *testing.T) {
})
}
}

type customInt int

func Test_int_unwraps_underlying_value(t *testing.T) {
env := map[string]any{
"customInt": customInt(42),
}
program, err := expr.Compile(`int(customInt) == 42`, expr.Env(env))
require.NoError(t, err)

out, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, true, out)
}
Loading