diff --git a/.golangci.yml b/.golangci.yml index 961a584..63562c3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -207,7 +207,7 @@ linters-settings: gocognit: # Minimal code complexity to report # Default: 30 (but we recommend 10-20) - min-complexity: 15 + min-complexity: 20 gocritic: # Which checks should be enabled; can't be combined with 'disabled-checks'. @@ -300,7 +300,7 @@ linters-settings: gocyclo: # Minimal code complexity to report. # Default: 30 (but we recommend 10-20) - min-complexity: 15 + min-complexity: 20 godot: # Comments to be checked: `declarations`, `toplevel`, or `all`. diff --git a/README.md b/README.md index 2743c19..d22c0f9 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Escapes are supported: ## Bools -In addition to boolean expressions, sepcial contants `True` and `False` may be used. +In addition to boolean expressions, special contants `True` and `False` may be used. Do not double-quote them, or they will become plain strings! @@ -148,7 +148,7 @@ This is container `Value`. It can contain zero or any number of `Value`'s. Curre * Go classifies bit shift operators with the higher `*`. * `&&` is synonymous of `And`. * `||` is synonymous of `Or`. - * Worded operators such as `And` and `Or` are **case-sensitive** and must be followed by a blank character. `True Or (False)` is a Bool expression with the `Or` operator but `True Or(False)` is an invalid expression attempting to call a user-defined function called `Or()`. + * Worded operators such as `And` and `Or` are **case-sensitive** and must be followed by a blank character. `True Or (False)` is a Bool expression with the `Or` operator but `True Or(False)` is an expression attempting to call a user-defined function called `Or()`. * Types: String, Number, Bool, MultiValue * Associativity with parentheses: `(` and `)` * Functions: @@ -158,6 +158,8 @@ This is container `Value`. It can contain zero or any number of `Value`'s. Curre ## Functions +(See also Objects) + A function is defined as a Go type: `type FunctionalValue func(...Value) Value` Function names are case-insensitive. @@ -172,12 +174,52 @@ This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multi ## Variables +(See also Objects) + Variable names are case-sensitive. Values are passed as a `map[string]Value` using `WithVariables` when calling `Eval` from `Tree`. This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multiple times with different variable values. +## Objects + +Objects are Go `struct`'s which **properties** act as **gal variables** and **methods** as **gal functions**. + +Object definitions are passed as a `map[string]Object` using `WithObjects` when calling `Eval` from `Tree`. + +This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multiple times with different instances of an object. + +Object methods generally follow the same rules as **gal Functions**: +- methods can optionally accept one or more arguments +- methods must return a single value (which can be a `MultiValue` to emulate multiple return values) +- arguments and return value are preferred to be of `gal.Value` type. + However, **gal** will attempt to convert Go types to `gal.Value` types on best endeavour: + - A method signature of `MyMethod(arg1 int64) bool` will translate the supplied `gal.Value`'s and attempt to map them to `int64` and `bool` using `gal.Numberer` and `gal.Booler`. + - Type conversion may lead to a panic when the type cannot be interpreted. + +Example: + +`type Car struct` has several properties and methods - one of which is `func (c *Car) CurrentSpeed() gal.Value`. + +```go + expr := `aCar.MaxSpeed - aCar.CurrentSpeed()` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + // result: 150 == 250 - 100 + +``` + ## High level design Expressions are parsed in two stages: diff --git a/function.go b/function.go index b20a779..27c7478 100644 --- a/function.go +++ b/function.go @@ -71,6 +71,10 @@ var builtInFunctions = map[string]FunctionalValue{ // It returns `nil` when no built-in function exists by the specified name. // This signals the Evaluator to attempt to find a user defined function. func BuiltInFunction(name string) FunctionalValue { + if builtInFunctions == nil { + return nil + } + // note: for now function names are arbitrarily case-insensitive lowerName := strings.ToLower(name) @@ -82,15 +86,6 @@ func BuiltInFunction(name string) FunctionalValue { return nil } -// UserDefinedFunction is a helper function that returns the definition of the -// provided function name from the supplied userFunctions. -func UserDefinedFunction(name string, userFunctions Functions) FunctionalValue { - // note: for now function names are arbitrarily case-insensitive - lowerName := strings.ToLower(name) - - return userFunctions.Function(lowerName) -} - // Pi returns the Value of math.Pi. func Pi(args ...Value) Value { if len(args) != 0 { diff --git a/function_test.go b/function_test.go index c556cfd..9a64db7 100644 --- a/function_test.go +++ b/function_test.go @@ -21,16 +21,16 @@ func TestPiLong(t *testing.T) { } func TestFactorial(t *testing.T) { - val := gal.Factorial(gal.NewNumber(0)) - assert.Equal(t, gal.NewNumber(1).String(), val.String()) + val := gal.Factorial(gal.NewNumberFromInt(0)) + assert.Equal(t, gal.NewNumberFromInt(1).String(), val.String()) - val = gal.Factorial(gal.NewNumber(1)) - assert.Equal(t, gal.NewNumber(1).String(), val.String()) + val = gal.Factorial(gal.NewNumberFromInt(1)) + assert.Equal(t, gal.NewNumberFromInt(1).String(), val.String()) - val = gal.Factorial(gal.NewNumber(10)) - assert.Equal(t, gal.NewNumber(3_628_800).String(), val.String()) + val = gal.Factorial(gal.NewNumberFromInt(10)) + assert.Equal(t, gal.NewNumberFromInt(3_628_800).String(), val.String()) - val = gal.Factorial(gal.NewNumber(-10)) + val = gal.Factorial(gal.NewNumberFromInt(-10)) assert.Equal(t, "undefined: Factorial: requires a positive integer, cannot accept -10", val.String()) } @@ -60,21 +60,21 @@ func TestFloor(t *testing.T) { } func TestLn(t *testing.T) { - val := gal.Ln(gal.NewNumber(123456), gal.NewNumber(5)) + val := gal.Ln(gal.NewNumberFromInt(123456), gal.NewNumberFromInt(5)) assert.Equal(t, "11.72364", val.String()) - val = gal.Ln(gal.NewNumber(-123456), gal.NewNumber(5)) + val = gal.Ln(gal.NewNumberFromInt(-123456), gal.NewNumberFromInt(5)) assert.Equal(t, "undefined: Ln:cannot calculate natural logarithm for negative decimals", val.String()) } func TestLog(t *testing.T) { - val := gal.Log(gal.NewNumber(123456), gal.NewNumber(5)) + val := gal.Log(gal.NewNumberFromInt(123456), gal.NewNumberFromInt(5)) assert.Equal(t, "5.09151", val.String()) - val = gal.Log(gal.NewNumber(-123456), gal.NewNumber(5)) + val = gal.Log(gal.NewNumberFromInt(-123456), gal.NewNumberFromInt(5)) assert.Equal(t, "undefined: Log:cannot calculate natural logarithm for negative decimals", val.String()) - val = gal.Log(gal.NewNumber(10_000_000), gal.NewNumber(0)) + val = gal.Log(gal.NewNumberFromInt(10_000_000), gal.NewNumberFromInt(0)) assert.Equal(t, "7", val.String()) } diff --git a/gal_test.go b/gal_test.go index 5071e7e..0c6059f 100644 --- a/gal_test.go +++ b/gal_test.go @@ -12,43 +12,43 @@ import ( func TestEval(t *testing.T) { expr := `-1 + 2 * 3 / 2 + 3 ** 2 -8` val := gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber(3).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt(3).String(), val.String()) expr = `-"123"+"100"` val = gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber(-23).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt(-23).String(), val.String()) expr = `1-2+7<<4+5` val = gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber((1-2+7)<<(4+5)).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt((1-2+7)<<(4+5)).String(), val.String()) expr = `-1-2-7<<4+5` val = gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber((-1-2-7)<<(4+5)).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt((-1-2-7)<<(4+5)).String(), val.String()) expr = `-100*2*7+1>>2+3` val = gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber((-100*2*7+1)>>(2+3)).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt((-100*2*7+1)>>(2+3)).String(), val.String()) expr = `100*2*7+1>>2+3` val = gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber((100*2*7+1)>>(2+3)).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt((100*2*7+1)>>(2+3)).String(), val.String()) expr = `2+Factorial(4)-5` val = gal.Parse(expr).Eval() - assert.Equal(t, gal.NewNumber(21).String(), val.String()) + assert.Equal(t, gal.NewNumberFromInt(21).String(), val.String()) } func TestTreeBuilder_FromExpr_Variables(t *testing.T) { vars := gal.Variables{ - ":var1:": gal.NewNumber(4), // TODO: remove the need to surround with `:`? - ":var2:": gal.NewNumber(3), + ":var1:": gal.NewNumberFromInt(4), // TODO: remove the need to surround with `:`? + ":var2:": gal.NewNumberFromInt(3), } expr := `2 + :var1: * :var2: - 5` got := gal.Parse(expr).Eval(gal.WithVariables(vars)) - expected := gal.NewNumber(9) + expected := gal.NewNumberFromInt(9) if !cmp.Equal(expected, got) { t.Error(cmp.Diff(expected, got)) @@ -162,7 +162,7 @@ func TestWithVariablesAndFunctions(t *testing.T) { return gal.NewUndefinedWithReasonf("double(): syntax error - argument must be a number-like value, got '%v'", args[0]) } - return value.Number().Multiply(gal.NewNumber(2)) + return value.Number().Multiply(gal.NewNumberFromInt(2)) }, "triple": func(args ...gal.Value) gal.Value { if len(args) != 1 { @@ -174,20 +174,20 @@ func TestWithVariablesAndFunctions(t *testing.T) { return gal.NewUndefinedWithReasonf("triple(): syntax error - argument must be a number-like value, got '%v'", args[0]) } - return value.Number().Multiply(gal.NewNumber(3)) + return value.Number().Multiply(gal.NewNumberFromInt(3)) }, } vars := gal.Variables{ - ":val1:": gal.NewNumber(4), - ":val2:": gal.NewNumber(5), + ":val1:": gal.NewNumberFromInt(4), + ":val2:": gal.NewNumberFromInt(5), } got := parsedExpr.Eval( gal.WithVariables(vars), gal.WithFunctions(funcs), ) - expected := gal.NewNumber(23) + expected := gal.NewNumberFromInt(23) if !cmp.Equal(expected, got) { t.Error(cmp.Diff(expected, got)) @@ -199,25 +199,25 @@ func TestWithVariablesAndFunctions(t *testing.T) { "double": func(args ...gal.Value) gal.Value { // should first validate argument count here value := args[0].(gal.Numberer) // should check type assertion is ok here - return value.Number().Divide(gal.NewNumber(2)) + return value.Number().Divide(gal.NewNumberFromInt(2)) }, "triple": func(args ...gal.Value) gal.Value { // should first validate argument count here value := args[0].(gal.Numberer) // should check type assertion is ok here - return value.Number().Divide(gal.NewNumber(3)) + return value.Number().Divide(gal.NewNumberFromInt(3)) }, } vars = gal.Variables{ - ":val1:": gal.NewNumber(2), - ":val2:": gal.NewNumber(6), + ":val1:": gal.NewNumberFromInt(2), + ":val2:": gal.NewNumberFromInt(6), } got = parsedExpr.Eval( gal.WithVariables(vars), gal.WithFunctions(funcs), ) - expected = gal.NewNumber(3) + expected = gal.NewNumberFromInt(3) if !cmp.Equal(expected, got) { t.Error(cmp.Diff(expected, got)) @@ -240,7 +240,7 @@ func TestNestedFunctions(t *testing.T) { return gal.NewUndefinedWithReasonf("double(): syntax error - argument must be a number-like value, got '%v'", args[0]) } - return value.Number().Multiply(gal.NewNumber(2)) + return value.Number().Multiply(gal.NewNumberFromInt(2)) }, "triple": func(args ...gal.Value) gal.Value { if len(args) != 1 { @@ -252,14 +252,14 @@ func TestNestedFunctions(t *testing.T) { return gal.NewUndefinedWithReasonf("triple(): syntax error - argument must be a number-like value, got '%v'", args[0]) } - return value.Number().Multiply(gal.NewNumber(3)) + return value.Number().Multiply(gal.NewNumberFromInt(3)) }, } got := parsedExpr.Eval( gal.WithFunctions(funcs), ) - expected := gal.NewNumber(42) + expected := gal.NewNumberFromInt(42) assert.Equal(t, expected.String(), got.String()) } @@ -279,7 +279,7 @@ func TestMultiValueFunctions(t *testing.T) { return gal.NewUndefinedWithReasonf("double(): syntax error - argument must be a number-like value, got '%v'", args[0]) } - return value.Number().Multiply(gal.NewNumber(2)) + return value.Number().Multiply(gal.NewNumberFromInt(2)) }, "triple": func(args ...gal.Value) gal.Value { if len(args) != 1 { @@ -291,7 +291,7 @@ func TestMultiValueFunctions(t *testing.T) { return gal.NewUndefinedWithReasonf("triple(): syntax error - argument must be a number-like value, got '%v'", args[0]) } - return value.Number().Multiply(gal.NewNumber(3)) + return value.Number().Multiply(gal.NewNumberFromInt(3)) }, "div": func(args ...gal.Value) gal.Value { // returns the division of value1 by value2 as the interger portion and the remainder @@ -330,7 +330,7 @@ func TestMultiValueFunctions(t *testing.T) { got := gal.Parse(expr).Eval( gal.WithFunctions(funcs), ) - expected := gal.NewNumber(7) + expected := gal.NewNumberFromInt(7) assert.Equal(t, expected.String(), got.String()) } @@ -358,3 +358,91 @@ func TestFunctionsAndStringsWithSpaces(t *testing.T) { ) assert.Equal(t, `"ab cdef gh"`, got.String()) } + +func TestObjects_Properties(t *testing.T) { + expr := `aCar.MaxSpeed - aCar.Speed` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "150", got.String()) +} + +func TestObjects_Methods(t *testing.T) { + expr := `aCar.MaxSpeed - aCar.CurrentSpeed()` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": &Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "150", got.String()) +} + +func TestObjects_Methods_WithSubTree(t *testing.T) { + expr := `2 * (aCar.MaxSpeed - aCar.CurrentSpeed())` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": &Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "300", got.String()) +} + +func TestObjects_Methods_WithArgsSubTree(t *testing.T) { + expr := `2 * (aCar.MaxSpeed - aCar.TillMaxSpeed(aCar.CurrentSpeed()))` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": &Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "200", got.String()) +} + +func TestObjects_MethodReceiver(t *testing.T) { + expr := `aCar.MaxSpeed - aCar.CurrentSpeed()` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + // Note: in this test, WithObjects is called with a `Car`, not a `*Car`. + // However, Car.CurrentSpeed has a *Car receiver, hence from a Go perspective, the method + // exists on *Car but it does NOT exist on Car! + assert.Equal(t, "undefined: type 'gal_test.Car' does not have a method 'CurrentSpeed' (check if it has a pointer receiver)", got.String()) +} diff --git a/go.mod b/go.mod index 4b46a2c..221e560 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ require github.com/shopspring/decimal v1.4.0 require ( github.com/google/go-cmp v0.6.0 github.com/pkg/errors v0.9.1 + github.com/samber/lo v1.39.0 github.com/stretchr/testify v1.7.1 ) require ( github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6d574b9..f309c6d 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/object.go b/object.go new file mode 100644 index 0000000..00b2bc5 --- /dev/null +++ b/object.go @@ -0,0 +1,173 @@ +package gal + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/pkg/errors" + "github.com/samber/lo" +) + +// TODO: implement support for nested structs? +func ObjectGetProperty(obj Object, name string) (Value, bool) { //nolint: gocognit, gocyclo, cyclop + if obj == nil { + return NewUndefinedWithReasonf("object is nil"), false + } + + // Use the reflect.ValueOf function to get the value of the struct + v := reflect.ValueOf(obj) + if v.IsZero() || !v.IsValid() { + return NewUndefinedWithReasonf("object is nil, not a Go value or invalid"), false + } + + // Use reflect.TypeOf to get the type of the struct + t := reflect.TypeOf(obj) + + if v.Kind() == reflect.Ptr { + v = v.Elem() + if v.IsZero() || !v.IsValid() { + return NewUndefinedWithReasonf("object interface is nil, not a Go value or invalid"), false + } + + t = t.Elem() + if v.IsZero() || !v.IsValid() { + return NewUndefinedWithReasonf("object interface is nil, not a Go value or invalid"), false + } + } + + // TODO: we only support `struct` for now. Perhaps simple types (int, float, etc) are a worthwhile enhancement? + if t.Kind() != reflect.Struct { + return NewUndefinedWithReasonf("object is '%s' but only 'struct' and '*struct' are currently supported", t.Kind()), false + } + + vValue := v.FieldByName(name) + if !vValue.IsValid() { + return NewUndefinedWithReasonf("property '%T:%s' does not exist on object", obj, name), false + } + + galValue, err := goAnyToGalType(vValue.Interface()) + if err != nil { + return NewUndefinedWithReasonf("object::%T:%s - %s", obj, name, err.Error()), false + } + return galValue, true +} + +func ObjectGetMethod(obj Object, name string) (FunctionalValue, bool) { //nolint: cyclop + if obj == nil { + return func(...Value) Value { + return NewUndefinedWithReasonf("object is nil for type '%T'", obj) + }, false + } + + value := reflect.ValueOf(obj) + if value.IsZero() || !value.IsValid() { + return func(...Value) Value { + return NewUndefinedWithReasonf("object is nil for type '%T' or does not have a method '%s'", obj, name) + }, false + } + + methodValue := value.MethodByName(name) + if !methodValue.IsValid() { + return func(...Value) Value { + return NewUndefinedWithReasonf("type '%T' does not have a method '%s' (check if it has a pointer receiver)", obj, name) + }, false + } + + methodType := methodValue.Type() + numParams := methodType.NumIn() + + var fn FunctionalValue = func(args ...Value) (retValue Value) { + if len(args) != numParams { + return NewUndefinedWithReasonf("invalid function call - object::%T:%s - wants %d args, received %d instead", obj, name, numParams, len(args)) + } + + callArgs := lo.Map(args, func(item Value, index int) reflect.Value { + paramType := methodType.In(index) + + switch paramType.Kind() { + // TODO: continue with more "case"'s + case reflect.Int: + return reflect.ValueOf(int(item.(Numberer).Number().Int64())) + case reflect.Int32: + return reflect.ValueOf(int32(item.(Numberer).Number().Int64())) + case reflect.Int64: + return reflect.ValueOf(item.(Numberer).Number().Int64()) + case reflect.Uint: + return reflect.ValueOf(uint(item.(Numberer).Number().Int64())) + case reflect.Uint32: + return reflect.ValueOf(uint32(item.(Numberer).Number().Int64())) + case reflect.Uint64: + n, err := strconv.ParseUint(item.(Stringer).AsString().RawString(), 10, 64) + if err != nil { + panic(err) // no other safe way + } + return reflect.ValueOf(n) + case reflect.Float32: + return reflect.ValueOf(float32(item.(Numberer).Number().Float64())) + case reflect.Float64: + return reflect.ValueOf(item.(Numberer).Number().Float64()) + case reflect.String: + return reflect.ValueOf(item.(Stringer).AsString().RawString()) + case reflect.Bool: + return reflect.ValueOf(item.(Booler).Bool().value) + default: + return reflect.ValueOf(item) + } + }) + + defer func() { + if r := recover(); r != nil { + retValue = NewUndefinedWithReasonf("invalid function call - object::%T:%s - invalid argument type passed to function - %v", obj, name, r) + return + } + }() + + out := methodValue.Call(callArgs) + if len(out) != 1 { + return NewUndefinedWithReasonf("invalid function call - object::%T:%s - must return 1 value, returned %d instead", obj, name, len(out)) + } + + retValue, err := goAnyToGalType(out[0].Interface()) + if err != nil { + return NewUndefinedWithReasonf("object::%T:%s - %s", obj, name, err.Error()) + } + return + } + + return fn, true +} + +// attempt to convert a Go 'any' type to an equivalent gal.Value +func goAnyToGalType(value any) (Value, error) { + switch typedValue := value.(type) { + case Value: + return typedValue, nil + case int: + return NewNumberFromInt(int64(typedValue)), nil + case int32: + return NewNumberFromInt(int64(typedValue)), nil + case int64: + return NewNumberFromInt(typedValue), nil + case uint: + return NewNumberFromInt(int64(typedValue)), nil + case uint32: + return NewNumberFromInt(int64(typedValue)), nil + case uint64: + n, err := NewNumberFromString(fmt.Sprintf("%d", typedValue)) + if err != nil { + return nil, errors.Errorf("value uint64(%d) cannot be converted to a Number", typedValue) + } + return n, nil + case float32: // this will commonly suffer from floating point issues + return NewNumberFromFloat(float64(typedValue)), nil + case float64: + return NewNumberFromFloat(typedValue), nil + case string: + return NewString(typedValue), nil + case bool: + return NewBool(typedValue), nil + default: + return nil, errors.Errorf("type '%T' cannot be mapped to gal.Value", typedValue) + } +} diff --git a/object_test.go b/object_test.go new file mode 100644 index 0000000..3d6602f --- /dev/null +++ b/object_test.go @@ -0,0 +1,164 @@ +package gal_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/seborama/gal/v8" +) + +type Car struct { + Make string + Mileage gal.Number + Speed float32 + MaxSpeed int64 + ComplexProperty complex128 +} + +func (c *Car) Ignite() gal.Value { + return gal.True +} + +func (c Car) Shutdown() gal.Value { //nolint: gocritic + return gal.True +} + +func (c *Car) CurrentSpeed() gal.Value { + return gal.NewNumberFromFloat(float64(c.Speed)) +} + +func (c *Car) CurrentSpeed2() float32 { + return c.Speed +} + +func (c *Car) SetSpeed(speed gal.Number) { + c.Speed = float32(speed.Float64()) +} + +func (c *Car) SetSpeed2(speed gal.Number) gal.Bool { + c.Speed = float32(speed.Float64()) + return gal.True +} + +func (c *Car) SetSpeed3(speed float32) gal.Bool { + c.Speed = speed + return gal.True +} + +func (c *Car) TillMaxSpeed(speed gal.Number) gal.Number { + return gal.NewNumberFromInt(c.MaxSpeed).Add(speed.Neg()).(gal.Numberer).Number() +} + +type fancyType struct { + Speed float32 +} + +func (c *Car) SetSpeed4(speed fancyType) gal.Bool { + c.Speed = speed.Speed + return gal.True +} + +func (c *Car) String() string { + return fmt.Sprint("Car", c.Make, c.Mileage.String(), c.Speed, c.Make) +} + +func TestObjectGetProperty(t *testing.T) { + myCar := &Car{ + Make: "Lotus", + Mileage: gal.NewNumberFromInt(100), + Speed: 50.345, + MaxSpeed: 250, + } + + var nilCar *Car + + val, ok := gal.ObjectGetProperty(nilCar, "Mileage") + require.False(t, ok) + assert.Equal(t, "undefined: object is nil, not a Go value or invalid", val.String()) + + val, ok = gal.ObjectGetProperty(myCar, "Make") + require.True(t, ok) + assert.Equal(t, "Lotus", val.(gal.String).RawString()) + + val, ok = gal.ObjectGetProperty(myCar, "Mileage") + require.True(t, ok) + assert.Equal(t, gal.NewNumberFromInt(100), val) + + // some bothersome floating point issues... + val, ok = gal.ObjectGetProperty(*myCar, "Speed") + require.True(t, ok) + assert.Equal(t, gal.NewNumberFromFloat(50.345).Trunc(1).String(), val.(gal.Number).Trunc(1).String()) + + val, ok = gal.ObjectGetProperty(complex(10, 3), "Blah") + require.False(t, ok) + assert.Equal(t, "undefined: object is 'complex128' but only 'struct' and '*struct' are currently supported", val.String()) + + val, ok = gal.ObjectGetProperty(myCar, "ComplexProperty") + require.False(t, ok) + assert.Equal(t, "undefined: object::*gal_test.Car:ComplexProperty - type 'complex128' cannot be mapped to gal.Value", val.String()) + + val, ok = gal.ObjectGetProperty(myCar, "MaxSpeed") + require.True(t, ok) + assert.Equal(t, gal.NewNumberFromInt(250), val) +} + +func TestObjectGetMethod(t *testing.T) { + myCar := &Car{ + Make: "Lotus", + Mileage: gal.NewNumberFromInt(100), + Speed: 50.345, + MaxSpeed: 250, + } + + var nilCar *Car + + val, ok := gal.ObjectGetMethod(nilCar, "Ignite") + require.False(t, ok) + assert.Equal(t, "undefined: object is nil for type '*gal_test.Car' or does not have a method 'Ignite'", val().String()) + + val, ok = gal.ObjectGetMethod(myCar, "DoesNotExist!") + require.False(t, ok) + assert.Equal(t, "undefined: type '*gal_test.Car' does not have a method 'DoesNotExist!' (check if it has a pointer receiver)", val().String()) + + val, ok = gal.ObjectGetMethod(myCar, "Ignite") + require.True(t, ok) + assert.Equal(t, gal.True, val()) + + val, ok = gal.ObjectGetMethod(myCar, "CurrentSpeed") + require.True(t, ok) + assert.Equal(t, "50.345", val().(gal.Numberer).Number().Trunc(3).String()) + + val, ok = gal.ObjectGetMethod(myCar, "CurrentSpeed2") + require.True(t, ok) + assert.Equal(t, "50.345", val().(gal.Numberer).Number().Trunc(3).String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed") + require.True(t, ok) + got := val(gal.NewNumberFromFloat(76), gal.True, gal.False) + assert.Equal(t, "undefined: invalid function call - object::*gal_test.Car:SetSpeed - wants 1 args, received 3 instead", got.String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed") + require.True(t, ok) + got = val(gal.NewNumberFromFloat(86)) + assert.Equal(t, "undefined: invalid function call - object::*gal_test.Car:SetSpeed - must return 1 value, returned 0 instead", got.String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed2") + require.True(t, ok) + got = val(gal.NewNumberFromFloat(96)) + assert.Equal(t, gal.True, got) + assert.Equal(t, "96", myCar.CurrentSpeed().String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed3") + require.True(t, ok) + got = val(gal.NewNumberFromFloat(106)) + assert.Equal(t, gal.True, got) + assert.Equal(t, "106", myCar.CurrentSpeed().String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed4") + require.True(t, ok) + got = val(gal.NewString("blah")) + assert.Equal(t, "undefined: invalid function call - object::*gal_test.Car:SetSpeed4 - invalid argument type passed to function - reflect: Call using gal.String as type gal_test.fancyType", got.String()) +} diff --git a/tree.go b/tree.go index 5be8742..0374eff 100644 --- a/tree.go +++ b/tree.go @@ -66,31 +66,66 @@ type Variables map[string]Value type Functions map[string]FunctionalValue // Function returns the function definition of the function of the specified name. -func (f Functions) Function(name string) FunctionalValue { - if f == nil { +func (tc treeConfig) Function(name string) FunctionalValue { + splits := strings.Split(name, ".") + if len(splits) > 1 { + // TODO: add recursive handling i.e. obj1.obj2.func1()? + if tc.objects != nil { + obj, ok := tc.objects[splits[0]] + if ok { + fv, _ := ObjectGetMethod(obj, splits[1]) + return fv + } + } + return nil + } + + if tc.functions == nil { return nil } - if val, ok := f[name]; ok { + if val, ok := tc.functions[name]; ok { return val } return nil } +// Object holds user-defined objects that can carry properties and functions that may be +// referenced within a gal expression during evaluation. +type Object any + +// Objects is a collection of Object's in the form of a map which keys are the name of the +// object and values are the actual Object's. +type Objects map[string]Object + type treeConfig struct { variables Variables functions Functions + objects Objects } // Variable returns the value of the variable specified by name. func (tc treeConfig) Variable(name string) (Value, bool) { - if tc.variables == nil { + splits := strings.Split(name, ".") + if len(splits) > 1 { + // TODO: add recursive handling i.e. obj.prop1.prop2? how about obj.func1().prop? + if tc.objects != nil { + obj, ok := tc.objects[splits[0]] + if ok { + return ObjectGetProperty(obj, splits[1]) + } + } return nil, false } - val, ok := tc.variables[name] - return val, ok + if tc.variables != nil { + val, ok := tc.variables[name] + if ok { + return val, ok + } + } + return nil, false } type treeOption func(*treeConfig) @@ -111,6 +146,16 @@ func WithFunctions(funcs Functions) treeOption { } } +// WithObjects is a functional parameter for Tree evaluation. +// It provides user-defined Objects. +// These objects can carry both properties and methods that can be accessed +// by gal in place of variables and functions. +func WithObjects(objects Objects) treeOption { + return func(cfg *treeConfig) { + cfg.objects = objects + } +} + // Eval evaluates this tree and returns its value. // It accepts optional functional parameters to supply user-defined // entities such as functions and variables. @@ -142,6 +187,10 @@ func (tree Tree) Eval(opts ...treeOption) Value { // Split divides a Tree trunk at points where two consecutive entries are present without // an operator in between. func (tree Tree) Split() []Tree { + if len(tree) == 0 { + return []Tree{} + } + var forest []Tree partStart := 0 @@ -216,7 +265,7 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree } } - rhsVal := e.(Tree).Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables)) + rhsVal := e.(Tree).Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables), WithObjects(cfg.objects)) if v, ok := rhsVal.(Undefined); ok { slog.Debug("Tree.Calc: val is Undefined", "i", i, "val", v.String()) return Tree{v} @@ -248,10 +297,10 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree slog.Debug("Tree.Calc: functionEntryKind", "i", i, "name", e.(Function).Name) f := e.(Function) //nolint: errcheck if f.BodyFn == nil { - f.BodyFn = cfg.functions.Function(f.Name) + f.BodyFn = cfg.Function(f.Name) } - rhsVal := f.Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables)) + rhsVal := f.Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables), WithObjects(cfg.objects)) if v, ok := rhsVal.(Undefined); ok { slog.Debug("Tree.Calc: val is Undefined", "i", i, "val", v.String()) return Tree{v} @@ -323,7 +372,7 @@ func (tree Tree) cleansePlusMinusTreeStart() Tree { case Plus: return outTree[1:] case Minus: - return append(Tree{NewNumber(-1), Multiply}, outTree[1:]...) + return append(Tree{NewNumberFromInt(-1), Multiply}, outTree[1:]...) } panic("point never reached") diff --git a/tree_builder.go b/tree_builder.go index 93611fa..b7946ce 100644 --- a/tree_builder.go +++ b/tree_builder.go @@ -84,7 +84,7 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { } case functionType: - fname, l, _ := readFunctionName(part) //nolint: errcheck // ignore err: we already parsed the function name when in extractPart() + fname, l, _ := readNamedExpressionType(part) //nolint: errcheck // ignore err: we already parsed the function name when in extractPart() v, err := tb.FromExpr(part[l+1 : len(part)-1]) // exclude leading '(' and trailing ')' if err != nil { return nil, err @@ -119,7 +119,7 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { case Plus: return tree[1:], nil case Minus: - return append(Tree{NewNumber(-1), Multiply}, tree[1:]...), nil + return append(Tree{NewNumberFromInt(-1), Multiply}, tree[1:]...), nil } } @@ -155,6 +155,9 @@ func extractPart(expr string) (string, exprType, int, error) { } // read part - "boolean" + // TODO: we can probably merge this with the logic that goes for functions + // ...and call it something like "textual expression type" which can be + // ...functions, variables, object properties, object functions or constants { // make s, l and ok local scope s, l, ok := readBool(expr[pos:]) if ok { @@ -172,16 +175,25 @@ func extractPart(expr string) (string, exprType, int, error) { return s, variableType, pos + l, nil } - // read part - function(...) + // read part - function(...) / constant / (brackets...) / object.property / object.function() // conceptually, parenthesis grouping is a special case of anonymous identity function + // NOTE: name expression types that contain a '.' are reserved for Object's only. if expr[pos] == '(' || (expr[pos] >= 'a' && expr[pos] <= 'z') || (expr[pos] >= 'A' && expr[pos] <= 'Z') { - fname, lf, err := readFunctionName(expr[pos:]) + fname, lf, err := readNamedExpressionType(expr[pos:]) switch { case errors.Is(err, errFunctionNameWithoutParens): + if strings.Contains(fname, ".") { + // object property found: act like a variable + // TODO: could create a new objectPropertyType + return fname, variableType, pos + lf, nil + } // allow to continue so we can check alphanumerical operator names such as "And", "Or", etc + // TODO: before we try for alphanum operators, we will need to check if we have a defined constant + // e.g. Phi (golden ratio), etc user-defined or built-in (True, False) case err != nil: return "", unknownType, 0, err default: + // TODO: if name contains `.` it's an object function - could create a new objectFunctionType fargs, la, err := readFunctionArguments(expr[pos+lf:]) if err != nil { return "", unknownType, 0, err @@ -199,7 +211,7 @@ func extractPart(expr string) (string, exprType, int, error) { } // read part - number - // TODO: complex numbers are not supported - could be "native" or via function or perhaps even a MultiValue? + // TODO: complex numbers are not supported - could be "native" or via function or perhaps even a specialised MultiValue? s, l, err := readNumber(expr[pos:]) if err != nil { return "", unknownType, 0, err @@ -220,7 +232,7 @@ func readString(expr string) (string, int, error) { if r == '"' && (escapes == 0 || escapes&1 == 0) { break } - // TODO: perhaps we should collapse the `\`, here? + // TODO: perhaps we should collapse the `\`'s, here? escapes = 0 } @@ -285,7 +297,7 @@ readString: var errFunctionNameWithoutParens = errors.New("function with Parenthesis") -func readFunctionName(expr string) (string, int, error) { +func readNamedExpressionType(expr string) (string, int, error) { to := 0 // this could be an anonymous identity function (i.e. simple case of parenthesis grouping) for _, r := range expr { @@ -293,12 +305,12 @@ func readFunctionName(expr string) (string, int, error) { return expr[:to], to, nil } if isBlankSpace(r) { - return expr[:to], to, errFunctionNameWithoutParens + break } to++ } - return "", 0, errors.Errorf("syntax error: missing '(' for function name '%s'", expr[:to]) + return expr[:to], to, errFunctionNameWithoutParens } func readFunctionArguments(expr string) (string, int, error) { diff --git a/tree_builder_test.go b/tree_builder_test.go index 47788a6..06e79e6 100644 --- a/tree_builder_test.go +++ b/tree_builder_test.go @@ -1,7 +1,6 @@ package gal_test import ( - "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -16,21 +15,21 @@ func TestTreeBuilder_FromExpr_VariousOperators(t *testing.T) { require.NoError(t, err) expectedTree := gal.Tree{ - gal.NewNumber(-1), + gal.NewNumberFromInt(-1), gal.Multiply, - gal.NewNumber(10), + gal.NewNumberFromInt(10), gal.Plus, - gal.NewNumber(2), + gal.NewNumberFromInt(2), gal.Multiply, - gal.NewNumber(7), + gal.NewNumberFromInt(7), gal.Divide, - gal.NewNumber(2), + gal.NewNumberFromInt(2), gal.Plus, - gal.NewNumber(5), + gal.NewNumberFromInt(5), gal.Power, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Minus, - gal.NewNumber(8), + gal.NewNumberFromInt(8), } if !cmp.Equal(expectedTree, tree) { @@ -42,30 +41,29 @@ func TestTreeBuilder_FromExpr_PlusMinus_String(t *testing.T) { expr := `"-3 + -4" + -3 --4 / ( 1 + 2+3+4) +tan(10)` tree, err := gal.NewTreeBuilder().FromExpr(expr) require.NoError(t, err) - fmt.Println(tree.Eval()) expectedTree := gal.Tree{ gal.NewString(`-3 + -4`), gal.Minus, - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Plus, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Divide, gal.Tree{ - gal.NewNumber(1), + gal.NewNumberFromInt(1), gal.Plus, - gal.NewNumber(2), + gal.NewNumberFromInt(2), gal.Plus, - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Plus, - gal.NewNumber(4), + gal.NewNumberFromInt(4), }, gal.Plus, gal.NewFunction( "tan", gal.Tan, gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), }, ), } @@ -79,7 +77,7 @@ func TestTreeBuilder_FromExpr_Functions(t *testing.T) { expr := `trunc(tan(10 + sin(cos(3 + f(1+2 3 ")4((")))) 6)` funcs := gal.Functions{ - "f": func(...gal.Value) gal.Value { return gal.NewNumber(123) }, + "f": func(...gal.Value) gal.Value { return gal.NewNumberFromInt(123) }, } got := gal.Parse(expr) @@ -93,7 +91,7 @@ func TestTreeBuilder_FromExpr_Functions(t *testing.T) { "tan", gal.Tan, gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), gal.Plus, gal.NewFunction( "sin", @@ -103,18 +101,18 @@ func TestTreeBuilder_FromExpr_Functions(t *testing.T) { "cos", gal.Cos, gal.Tree{ - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Plus, gal.NewFunction( "f", nil, gal.Tree{ - gal.NewNumber(1), + gal.NewNumberFromInt(1), gal.Plus, - gal.NewNumber(2), + gal.NewNumberFromInt(2), }, gal.Tree{ - gal.NewNumber(3), + gal.NewNumberFromInt(3), }, gal.Tree{ gal.NewString(")4(("), @@ -128,7 +126,7 @@ func TestTreeBuilder_FromExpr_Functions(t *testing.T) { ), }, gal.Tree{ - gal.NewNumber(6), + gal.NewNumberFromInt(6), }, ), } diff --git a/tree_test.go b/tree_test.go index 929fc52..0ffc646 100644 --- a/tree_test.go +++ b/tree_test.go @@ -21,13 +21,13 @@ func TestTree_FullLen(t *testing.T) { "simple tree": { tree: gal.Tree{ gal.Plus, - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), }, wantLen: 2, }, "semi-complex tree": { tree: gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), gal.Plus, gal.Tree{}, }, @@ -35,22 +35,22 @@ func TestTree_FullLen(t *testing.T) { }, "complex tree": { tree: gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), gal.Plus, gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), gal.Plus, gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), gal.Plus, gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), gal.Plus, gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), gal.Plus, gal.Tree{ - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), }, }, }, @@ -80,134 +80,134 @@ func TestTree_Eval_Expressions(t *testing.T) { "starts with *": { tree: gal.Tree{ gal.Multiply, - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), }, want: gal.NewUndefinedWithReasonf("syntax error: missing left hand side value for operator '*'"), }, "starts with + -4": { tree: gal.Tree{ gal.Plus, - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), }, - want: gal.NewNumber(-4), + want: gal.NewNumberFromInt(-4), }, "starts with - -4": { tree: gal.Tree{ gal.Minus, - gal.NewNumber(-4), + gal.NewNumberFromInt(-4), }, - want: gal.NewNumber(4), + want: gal.NewNumberFromInt(4), }, "chained * and /": { tree: gal.Tree{ - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Multiply, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Divide, - gal.NewNumber(2), + gal.NewNumberFromInt(2), gal.Divide, - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Multiply, - gal.NewNumber(4), + gal.NewNumberFromInt(4), }, - want: gal.NewNumber(8), + want: gal.NewNumberFromInt(8), }, "chained and tree'ed * and /": { tree: gal.Tree{ gal.Tree{ gal.Tree{ gal.Tree{ - gal.NewNumber(3), + gal.NewNumberFromInt(3), }, }, gal.Multiply, gal.Tree{ - gal.NewNumber(4), + gal.NewNumberFromInt(4), }, }, gal.Divide, gal.Tree{ - gal.NewNumber(2), + gal.NewNumberFromInt(2), }, gal.Divide, gal.Tree{ - gal.NewNumber(3), + gal.NewNumberFromInt(3), }, gal.Multiply, gal.Tree{ - gal.NewNumber(4), + gal.NewNumberFromInt(4), }, }, - want: gal.NewNumber(8), + want: gal.NewNumberFromInt(8), }, "rich tree": { tree: gal.Tree{ - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Minus, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Multiply, gal.Tree{ gal.Minus, - gal.NewNumber(2), + gal.NewNumberFromInt(2), }, gal.Minus, - gal.NewNumber(5), + gal.NewNumberFromInt(5), }, - want: gal.NewNumber(6), + want: gal.NewNumberFromInt(6), }, "multiple levels of decreasing operator precedence": { tree: gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), gal.Power, - gal.NewNumber(2), + gal.NewNumberFromInt(2), gal.Multiply, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Plus, - gal.NewNumber(3), + gal.NewNumberFromInt(3), }, - want: gal.NewNumber(403), + want: gal.NewNumberFromInt(403), }, "multiple levels of operator precedence": { tree: gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), gal.Plus, - gal.NewNumber(5), + gal.NewNumberFromInt(5), gal.Multiply, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Power, - gal.NewNumber(3), + gal.NewNumberFromInt(3), gal.Multiply, - gal.NewNumber(2), + gal.NewNumberFromInt(2), gal.Plus, - gal.NewNumber(6), + gal.NewNumberFromInt(6), gal.Multiply, - gal.NewNumber(7), + gal.NewNumberFromInt(7), }, - want: gal.NewNumber(692), + want: gal.NewNumberFromInt(692), }, "rich sub-trees": { tree: gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), gal.Plus, gal.Tree{ - gal.NewNumber(5), + gal.NewNumberFromInt(5), gal.Multiply, gal.Tree{ gal.Minus, - gal.NewNumber(4), + gal.NewNumberFromInt(4), gal.Modulus, gal.Tree{ gal.Minus, - gal.NewNumber(3), + gal.NewNumberFromInt(3), }, }, }, }, - want: gal.NewNumber(5), + want: gal.NewNumberFromInt(5), }, "function": { tree: gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), gal.Plus, gal.NewFunction( "trunc", @@ -217,12 +217,12 @@ func TestTree_Eval_Expressions(t *testing.T) { "sqrt", gal.Sqrt, gal.Tree{ - gal.NewNumber(10), + gal.NewNumberFromInt(10), }, ), }, gal.Tree{ - gal.NewNumber(6), + gal.NewNumberFromInt(6), }, ), }, diff --git a/value.go b/value.go index ce87af4..2832929 100644 --- a/value.go +++ b/value.go @@ -25,20 +25,38 @@ type Evaler interface { Eval() Value } -// TODO: we may also want to create ToString() and ToBool() +func ToValue(value any) Value { + v, _ := toValue(value) + return v +} + +func toValue(value any) (Value, bool) { + v, err := goAnyToGalType(value) + if err != nil { + return NewUndefinedWithReasonf("value type %T - %s", value, err.Error()), false + } + return v, true +} + func ToNumber(val Value) Number { - // TODO: we could also try to convert Bool to Number since we could add NewNumberFromBool() to deal with Bool's return val.(Numberer).Number() // may panic } +func ToString(val Value) String { + return val.AsString() +} + +func ToBool(val Value) Bool { + return val.(Booler).Bool() // may panic +} + // MultiValue is a container of zero or more Value's. -// TODO: impose a minimum of 1 value? // For the time being, it is only usable and useful with functions. // Functions can accept a MultiValue, and also return a MultiValue. // This allows a function to effectively return multiple values as a MultiValue. // TODO: we could add a syntax to instantiate a MultiValue within an expression. -// TODO: ... perhaps along the lines of [[v1 v2 ...]] or simply a built-in function such as -// TODO: ... MultiValue(...) - nothing stops the user from creating their own for now :-) +// ... perhaps along the lines of [[v1 v2 ...]] or simply a built-in function such as +// ... MultiValue(...) - nothing stops the user from creating their own for now :-) // // TODO: implement other methods such as Add, LessThan, etc (if meaningful) type MultiValue struct { @@ -57,12 +75,17 @@ func (MultiValue) kind() entryKind { // Equal satisfies the external Equaler interface such as in testify assertions and the cmp package // Note that the current implementation defines equality as values matching and in order they appear. func (m MultiValue) Equal(other MultiValue) bool { + if m.Size() != other.Size() { + return false + } + for i := range m.values { // TODO: add test to confirm this is correct! if m.values[i].NotEqualTo(other.values[i]) == False { return false } } + return true } @@ -182,6 +205,10 @@ func (s String) String() string { return `"` + s.value + `"` } +func (s String) RawString() string { + return s.value +} + func (s String) AsString() String { return s } @@ -209,7 +236,13 @@ type Number struct { value decimal.Decimal } -func NewNumber(i int64) Number { +func NewNumber(i int64, exp int32) Number { + d := decimal.New(i, exp) + + return Number{value: d} +} + +func NewNumberFromInt(i int64) Number { d := decimal.NewFromInt(i) return Number{value: d} @@ -478,6 +511,13 @@ func (n Number) String() string { return n.value.String() } +func (n Number) Bool() Bool { + if n.value.IsZero() { + return False + } + return True +} + func (n Number) AsString() String { return NewString(n.String()) } @@ -564,6 +604,13 @@ func (b Bool) String() string { return "False" } +func (b Bool) Number() Number { + if b.value { + return NewNumberFromInt(1) + } + return NewNumberFromInt(0) +} + func (b Bool) AsString() String { return NewString(b.String()) } diff --git a/value_test.go b/value_test.go index ed2aee4..90f6d69 100644 --- a/value_test.go +++ b/value_test.go @@ -7,6 +7,6 @@ import ( ) func TestMultiValueString(t *testing.T) { - v := NewMultiValue(NewNumber(123), NewString("abc"), NewBool(true)) + v := NewMultiValue(NewNumberFromInt(123), NewString("abc"), NewBool(true)) assert.Equal(t, `123,"abc",True`, v.String()) }