diff --git a/README.md b/README.md index 7b723aa..bf22112 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ func main() { `gal` comes with pre-defined type interfaces: Numberer, Booler, Stringer (and maybe more in the future). -They allow the general use of types. For instance, the String `"123"` can be converted to the Number `123`. +They allow the use of general types. For instance, the String `"123"` can be converted to the Number `123`. With `Numberer`, a user-defined function can transparently use String and Number when both hold a number representation. A user-defined function can do this: @@ -184,7 +184,7 @@ This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multi ## Objects -Objects are Go `struct`'s which **properties** act as **gal variables** and **methods** as **gal functions**. +Objects are Go `struct`'s which **properties** behave similarly to **gal variables** and **methods** to **gal functions**. Object definitions are passed as a `map[string]Object` using `WithObjects` when calling `Eval` from `Tree`. @@ -203,23 +203,47 @@ 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, - }, - }), - ) + 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 ``` +## Objects Dot accessors + +While user-defined Objects are generally Value-centric, `gal` supports accessing porperties and methods on Go objects too, using the `.` accessor. + +Example: + +`aCar.Stereo` returns a `CarStereo` struct. Its property `Brand` returns a `StereoBrand` that contains 2 properties `Name` and `Country`. None of these use `gal.Value` but the Dot accessor permits traversing them transparently. + +`gal` will convert basic Go types such as `int` or `bool` to their `gal.Value` equivalent. This helps, at the end of the chain, to continue with the evaluation of the expression. + +```go + expr := `aCar.Stereo.Brand.Name` +``` + +Dot is an accessor. It can be thought of as a symbol. It is not an operator! + +```go +// This is NOT a valid expression. While it may parse, it won't evaluate! +((aCar.Stereo).Brand).Country + +// And of course, gal will refuse to evaluate this expression: +((aCar.Stereo).Brand + 10).Country +``` + ## High level design Expressions are parsed in two stages: diff --git a/gal.go b/gal.go index f412d97..ceffbd2 100644 --- a/gal.go +++ b/gal.go @@ -1,7 +1,5 @@ package gal -import "fmt" - type exprType int const ( @@ -19,31 +17,6 @@ const ( objectAccessorByMethodType // represents an object accessor of a "left hand side" expression by method ) -type Value interface { - // Calculation - Add(Value) Value - Sub(Value) Value - Multiply(Value) Value - Divide(Value) Value - PowerOf(Value) Value - Mod(Value) Value - LShift(Value) Value - RShift(Value) Value - // Logical - LessThan(Value) Bool - LessThanOrEqual(Value) Bool - EqualTo(Value) Bool - NotEqualTo(Value) Bool - GreaterThan(Value) Bool - GreaterThanOrEqual(Value) Bool - And(Value) Bool - Or(Value) Bool - // Helpers - Stringer - fmt.Stringer - entry -} - // Example: Parse("blah").Eval(WithVariables(...), WithFunctions(...), WithObjects(...)) // This allows to parse an expression and then use the resulting Tree for multiple // evaluations with different variables provided. diff --git a/gal_test.go b/gal_test.go index 8e3acce..02ee562 100644 --- a/gal_test.go +++ b/gal_test.go @@ -587,7 +587,7 @@ func TestObjects_MethodReceiver(t *testing.T) { // 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: error: object method 'aCar.CurrentSpeed': unknown or non-callable member (check if it has a pointer receiver)", got.String()) + assert.Equal(t, "undefined: error: object 'aCar' method 'CurrentSpeed': unknown or non-callable member (check if it has a pointer receiver)", got.String()) } // TODO: this is an idea for a future feature. diff --git a/object.go b/object.go index 8048768..5d2dd75 100644 --- a/object.go +++ b/object.go @@ -93,6 +93,7 @@ func (o ObjectMethod) String() string { return fmt.Sprintf("%s.%s", o.ObjectName, o.MethodName) } +// ObjectGetProperty returns the value of the property with the given name from the object. func ObjectGetProperty(obj Object, name string) Value { if obj == nil { return NewUndefinedWithReasonf("object is nil for type '%T'", obj) @@ -153,6 +154,7 @@ func ObjectGetProperty(obj Object, name string) Value { return galValue } +// ObjectGetMethod returns a closure that can be called with the method's arguments. func ObjectGetMethod(obj Object, name string) (FunctionalValue, bool) { if obj == nil { return func(...Value) Value { diff --git a/static/bit.ly_3MDZ9QT.png b/static/bit.ly_3MDZ9QT.png deleted file mode 100644 index 4addb1d..0000000 Binary files a/static/bit.ly_3MDZ9QT.png and /dev/null differ diff --git a/tree.go b/tree.go index 872875c..77c0962 100644 --- a/tree.go +++ b/tree.go @@ -70,153 +70,6 @@ func (tree Tree) FullLen() int { return l } -// Variables holds the value of user-defined variables. -type Variables map[string]Value - -func (v Variables) Get(name string) (Value, bool) { - if v == nil { - return nil, false - } - obj, ok := v[name] - return obj, ok -} - -// Functions holds the definition of user-defined functions. -type Functions map[string]FunctionalValue - -func (f Functions) Get(name string) (FunctionalValue, bool) { - if f == nil { - return nil, false - } - obj, ok := f[name] - return obj, ok -} - -// Function returns the function definition of the function of the specified name. -// This method is used to look up object methods and user-defined functions. -// Built-in functions are not looked up here, they are pre-populated at -// parsing time by the TreeBuilder. -func (tc treeConfig) Function(name string) FunctionalValue { - splits := strings.Split(name, ".") - if len(splits) == 2 { - // look up the method in the user-provided objects - if obj, ok := tc.objects.Get(splits[0]); ok { - // we ignore "ok" here because ObjectGetMethod will populate it with an Undefined. - fv, _ := ObjectGetMethod(obj, splits[1]) - return fv - } - return func(...Value) Value { - return NewUndefinedWithReasonf("error: object reference '%s' is not valid: unknown object or unknown method", name) - } - } - - if len(splits) >= 2 { - return func(...Value) Value { - return NewUndefinedWithReasonf("syntax error: object reference '%s' is not valid: too many dot accessors: max 1 permitted", name) - } - } - - // look up the function in the user-defined functions - if val, ok := tc.functions.Get(name); ok { - return val - } - - return func(...Value) Value { - return NewUndefinedWithReasonf("error: unknown user-defined function '%s'", name) - } -} - -// TODO: should this return a Function rather? -func (tc treeConfig) ObjectMethod(objMethod ObjectMethod) FunctionalValue { - if obj, ok := tc.objects.Get(objMethod.ObjectName); ok { - if fv, ok := ObjectGetMethod(obj, objMethod.MethodName); ok { - return fv - } - return func(...Value) Value { - return NewUndefinedWithReasonf("error: object method '%s': unknown or non-callable member (check if it has a pointer receiver)", objMethod.String()) - } - } - - return func(...Value) Value { - return NewUndefinedWithReasonf("error: object method '%s': unknown object", objMethod.String()) - } -} - -// 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 - -// Get returns the Object of the specified name. -func (o Objects) Get(name string) (Object, bool) { - if o == nil { - return nil, false - } - obj, ok := o[name] - return obj, ok -} - -// TODO: move treeConfig to a separate file? -type treeConfig struct { - variables Variables - functions Functions - objects Objects -} - -// Variable returns the value of the variable specified by name. -// TODO: add support for arrays and maps via `[...]` -// ... NOTE: it may be more adequate to create a new `[]` operator. -// ... This would also permit its use on any Value, including those returned from function calls. -// ... We would likely need to create new types (unless MultiValue can work for this). -// ... An awkward and visually less elegant option would be builtin functions such as GetIndex() (for arrays) and GetKey (for maps). -// ................................................................... -// ................................................................... -// ... Perhaps this indicates that it's time to drop gal.Value ... -// ... and use native Go types and reflection?!?! ... -// ................................................................... -// ................................................................... -func (tc treeConfig) Variable(name string) Value { - if val, ok := tc.variables.Get(name); ok { - return val - } - - return NewUndefinedWithReasonf("error: unknown user-defined variable '%s'", name) -} - -func (tc treeConfig) ObjectProperty(objProp ObjectProperty) Value { - if obj, ok := tc.objects.Get(objProp.ObjectName); ok { - return ObjectGetProperty(obj, objProp.PropertyName) - } - return NewUndefinedWithReasonf("error: object property '%s': unknown object", objProp.String()) -} - -type treeOption func(*treeConfig) - -// WithVariables is a functional parameter for Tree evaluation. -// It provides user-defined variables. -func WithVariables(vars Variables) treeOption { - return func(cfg *treeConfig) { - cfg.variables = vars - } -} - -// WithFunctions is a functional parameter for Tree evaluation. -// It provides user-defined functions. -func WithFunctions(funcs Functions) treeOption { - return func(cfg *treeConfig) { - cfg.functions = funcs - } -} - -// 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. @@ -519,7 +372,7 @@ func objectAccessorEntryKindFn(val, e entry, cfg *treeConfig) entry { default: slog.Debug("Tree.Calc: objectAccessorEntryKind Dot[unknown]", "entry_string", oa.kind().String()) - return NewUndefinedWithReasonf("internal error: unknown objectAccessorEntryKind Dot kind: '%s'", e.kind().String()) + return NewUndefinedWithReasonf("internal error: unknown objectAccessorEntryKind Dot kind: '%T'", oa) } } @@ -601,7 +454,7 @@ func (tree Tree) cleansePlusMinusTreeStart() Tree { return append(Tree{NewNumberFromInt(-1), Multiply}, outTree[1:]...) } - panic("point never reached") + panic("this point should never be reached") } func (Tree) kind() entryKind { diff --git a/tree_builder.go b/tree_builder.go index 1129a1a..cacebd2 100644 --- a/tree_builder.go +++ b/tree_builder.go @@ -95,8 +95,8 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { tree = append(tree, v) } else { bodyFn := BuiltInFunction(fname) // will be nil if it isn't a built-in function (i.e. user-defined or object method) - // TODO: if bodyFn == nil, we have either a user-defined function or an user-defined object method. It may be worth - // ... creating a new type for this case. This could simplify the code by keeping each case separate and simpler. + // NOTE: if bodyFn == nil, we are likely dealing with user-defined function. These are dealt with at Evaluation time. + // NOTE: user-defined object methods are the remit of objectMethodType. tree = append(tree, NewFunction(fname, bodyFn, v.Split()...)) } @@ -237,11 +237,11 @@ func extractPart(expr string) (string, exprType, int, error) { } } - // read part - object accessor (Dot operator) + // read part - object Dot accessor // // NOTE: object accessors are second degree to variables and functions // The allow to continue traversing an object by property or method. - // The dot operator is used after any gal.entry that returns a value that can be treated as an object. + // The dot accessor is used after any gal.entry that returns a value that can be treated as an object. // For example "Pi().Add(10).Sub(5)" is a valid expression because "Pi()" returns a gal.Value and // hence a Go object (be it struct or interface). if expr[pos] == '.' { @@ -271,13 +271,13 @@ func extractPart(expr string) (string, exprType, int, error) { // read part - operator if s, l := readOperator(expr[pos:]); l != 0 { if s == "+" || s == "-" { - s, l = squashPlusMinusChain(expr[pos:]) // TODO: move this into readOperator()? + s, l = squashPlusMinusChain(expr[pos:]) // NOTE: shoud we move this into readOperator()? } return s, operatorType, pos + l, nil } // read part - number - // TODO: complex numbers are not supported - could be "native" or via function or perhaps even a specialised MultiValue? + // NOTE: 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 @@ -298,7 +298,7 @@ func readString(expr string) (string, int, error) { if r == '"' && (escapes == 0 || escapes&1 == 0) { break } - // TODO: perhaps we should collapse the `\`'s, here? + // NOTE: perhaps we should collapse the `\`'s, here? escapes = 0 } diff --git a/tree_config.go b/tree_config.go new file mode 100644 index 0000000..9508a92 --- /dev/null +++ b/tree_config.go @@ -0,0 +1,156 @@ +package gal + +import "strings" + +// Variables holds the value of user-defined variables. +type Variables map[string]Value + +func (v Variables) Get(name string) (Value, bool) { + if v == nil { + return nil, false + } + obj, ok := v[name] + return obj, ok +} + +// Functions holds the definition of user-defined functions. +type Functions map[string]FunctionalValue + +func (f Functions) Get(name string) (FunctionalValue, bool) { + if f == nil { + return nil, false + } + obj, ok := f[name] + return obj, ok +} + +// 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 + +// Get returns the Object of the specified name. +func (o Objects) Get(name string) (Object, bool) { + if o == nil { + return nil, false + } + obj, ok := o[name] + return obj, ok +} + +type treeConfig struct { + variables Variables + functions Functions + objects Objects +} + +// Variable returns the value of the variable specified by name. +// TODO: add support for arrays and maps via `[...]` +// ... NOTE: it may be more adequate to create a new `[]` operator. +// ... This would also permit its use on any Value, including those returned from function calls. +// ... We would likely need to create new types (unless MultiValue can work for this). +// ... An awkward and visually less elegant option would be builtin functions such as GetIndex() (for arrays) and GetKey (for maps). +// ................................................................... +// ................................................................... +// ... Perhaps this indicates that it's time to drop gal.Value ... +// ... and use native Go types and reflection?!?! ... +// ................................................................... +// ................................................................... +func (tc treeConfig) Variable(name string) Value { + if val, ok := tc.variables.Get(name); ok { + return val + } + + return NewUndefinedWithReasonf("error: unknown user-defined variable '%s'", name) +} + +func (tc treeConfig) ObjectProperty(objProp ObjectProperty) Value { + if obj, ok := tc.objects.Get(objProp.ObjectName); ok { + return ObjectGetProperty(obj, objProp.PropertyName) + } + return NewUndefinedWithReasonf("error: object property '%s': unknown object", objProp.String()) +} + +// Function returns the function definition of the function of the specified name. +// This method is used to look up object methods and user-defined functions. +// Built-in functions are not looked up here, they are pre-populated at +// parsing time by the TreeBuilder. +func (tc treeConfig) Function(name string) FunctionalValue { + splits := strings.Split(name, ".") // NOTE: strings.Split(name, ".") allocates a slice every call. strings.Count + strings.Index/LastIndex could avoid allocation in the common “no dot” path. + if len(splits) == 2 { + // look up the method in the user-provided objects + tc.objectMethod(splits[0], splits[1]) + if obj, ok := tc.objects.Get(splits[0]); ok { + // we ignore "ok" here because ObjectGetMethod will populate it with an Undefined. + fv, _ := ObjectGetMethod(obj, splits[1]) + return fv + } + return func(...Value) Value { + return NewUndefinedWithReasonf("error: object reference '%s' is not valid: unknown object or unknown method", name) + } + } + + if len(splits) >= 2 { + // for expressions like `obj.a.b`, the tree should use a Variable or a Function to access `a` and + // then a Dot[Variable] / Dot[Function] to access `b`. + return func(...Value) Value { + return NewUndefinedWithReasonf("syntax error: object reference '%s' is not valid: too many dot accessors: max 1 permitted", name) + } + } + + // look up the function in the user-defined functions + if val, ok := tc.functions.Get(name); ok { + return val + } + + return func(...Value) Value { + return NewUndefinedWithReasonf("error: unknown user-defined function '%s'", name) + } +} + +// TODO: should this return a Function rather? +func (tc treeConfig) ObjectMethod(objMethod ObjectMethod) FunctionalValue { + return tc.objectMethod(objMethod.ObjectName, objMethod.MethodName) +} + +func (tc treeConfig) objectMethod(objectName, methodName string) FunctionalValue { + if obj, ok := tc.objects.Get(objectName); ok { + if fv, ok := ObjectGetMethod(obj, methodName); ok { + return fv + } + return func(...Value) Value { + return NewUndefinedWithReasonf("error: object '%s' method '%s': unknown or non-callable member (check if it has a pointer receiver)", objectName, methodName) + } + } + + return func(...Value) Value { + return NewUndefinedWithReasonf("error: object '%s' method '%s': unknown object", objectName, methodName) + } +} + +type treeOption func(*treeConfig) + +// WithVariables is a functional parameter for Tree evaluation. +// It provides user-defined variables. +func WithVariables(vars Variables) treeOption { + return func(cfg *treeConfig) { + cfg.variables = vars + } +} + +// WithFunctions is a functional parameter for Tree evaluation. +// It provides user-defined functions. +func WithFunctions(funcs Functions) treeOption { + return func(cfg *treeConfig) { + cfg.functions = funcs + } +} + +// 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 + } +} diff --git a/value.go b/value.go index 8d462b3..b0519ec 100644 --- a/value.go +++ b/value.go @@ -1,15 +1,53 @@ package gal -// TODO: could this file be moved to a "value" package and be split into multiple files? +import "fmt" -import ( - "fmt" - "math/big" - "strings" +type Value interface { + valueCalculation + valueComparison + valueLogic + valueHelper + undefinedChecker +} + +type valueCalculation interface { + Add(Value) Value + Sub(Value) Value + Multiply(Value) Value + Divide(Value) Value + PowerOf(Value) Value + Mod(Value) Value + LShift(Value) Value + RShift(Value) Value +} + +type valueComparison interface { + LessThan(Value) Bool + LessThanOrEqual(Value) Bool + EqualTo(Value) Bool + NotEqualTo(Value) Bool + GreaterThan(Value) Bool + GreaterThanOrEqual(Value) Bool +} - "github.com/pkg/errors" - "github.com/shopspring/decimal" -) +type valueLogic interface { + And(Value) Bool + Or(Value) Bool +} + +type valueHelper interface { + Stringer + fmt.Stringer + entry +} + +type undefinedChecker interface { + // TODO: IsUndefined somewhat mimics a "Maybe" monad in functional programming: + // ... e.g. if a Bool has its Undefined value set, IsUndefined will return true. + // ... Instead of using the Bool, we should unwrap the Undefined and use it: this is not + // ... implemented yet! + IsUndefined() bool +} type Stringer interface { AsString() String // name is not String so to not clash with fmt.Stringer interface @@ -28,7 +66,7 @@ type Evaler interface { } func ToValue(value any) Value { - v, _ := toValue(value) + v, _ := toValue(value) // ignore "ok" because we are sure it is a valid Value, be it Undefined or not. return v } @@ -41,8 +79,11 @@ func toValue(value any) (Value, bool) { } func ToNumber(val Value) Number { - //nolint:errcheck // life's too short to check for type assertion success here - return val.(Numberer).Number() // may panic + n, ok := val.(Numberer) + if !ok { + return Number{Undefined: NewUndefinedWithReasonf("value type %T - cannot convert to Number", val)} + } + return n.Number() } func ToString(val Value) String { @@ -50,715 +91,9 @@ func ToString(val Value) String { } func ToBool(val Value) Bool { - //nolint:errcheck // life's too short to check for type assertion success here - return val.(Booler).Bool() // may panic -} - -// MultiValue is a container of zero or more Value's. -// 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. -// ... 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 { - Undefined - values []Value -} - -func NewMultiValue(values ...Value) MultiValue { - return MultiValue{values: values} -} - -func (MultiValue) kind() entryKind { - return valueEntryKind -} - -// 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 -} - -func (m MultiValue) String() string { - var vals []string - for _, val := range m.values { - vals = append(vals, val.String()) - } - return strings.Join(vals, `,`) -} - -func (m MultiValue) AsString() String { - return NewString(m.String()) -} - -func (m MultiValue) Get(i int) Value { - if i > len(m.values) { - return NewUndefinedWithReasonf("out of bounds: trying to get arg #%d on MultiValue that has %d arguments", i, len(m.values)) - } - - return m.values[i] -} - -func (m MultiValue) Size() int { - return len(m.values) -} - -type String struct { - Undefined - value string -} - -func NewString(s string) String { - return String{value: s} -} - -func (String) kind() entryKind { - return valueEntryKind -} - -// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package -func (s String) Equal(other String) bool { - return s.value == other.value -} - -func (s String) LessThan(other Value) Bool { - if v, ok := other.(Stringer); ok { - return NewBool(s.value < v.AsString().value) - } - - return False -} - -func (s String) LessThanOrEqual(other Value) Bool { - if v, ok := other.(Stringer); ok { - return NewBool(s.value <= v.AsString().value) - } - - return False -} - -func (s String) EqualTo(other Value) Bool { - if v, ok := other.(Stringer); ok { - return NewBool(s.value == v.AsString().value) // beware to compare what's comparable: do NOT use s.value == v.String() because String() may decorate the value (see String and MultiValue for example) - } - - return False -} - -func (s String) NotEqualTo(other Value) Bool { - return s.EqualTo(other).Not() -} - -func (s String) GreaterThan(other Value) Bool { - if v, ok := other.(Stringer); ok { - return NewBool(s.value > v.AsString().value) - } - - return False -} - -func (s String) GreaterThanOrEqual(other Value) Bool { - if v, ok := other.(Stringer); ok { - return NewBool(s.value >= v.AsString().value) - } - - return False -} - -func (s String) Add(other Value) Value { - if v, ok := other.(Stringer); ok { - return String{value: s.value + v.AsString().value} - } - - return NewUndefinedWithReasonf("cannot Add non-string to a string") -} - -func (s String) Multiply(other Value) Value { - if v, ok := other.(Numberer); ok { - return String{ - value: strings.Repeat(s.value, int(v.Number().value.IntPart())), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -// TODO: add test to confirm this is correct! -func (s String) LShift(other Value) Value { - if v, ok := other.(Numberer); ok { - if v.Number().value.IsNegative() { - return NewUndefinedWithReasonf("invalid negative left shift") - } - if !v.Number().value.IsInteger() { - return NewUndefinedWithReasonf("invalid non-integer left shift") - } - - return String{ - value: s.value[v.Number().value.IntPart():], - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -// TODO: add test to confirm this is correct! -func (s String) RShift(other Value) Value { - if v, ok := other.(Numberer); ok { - if v.Number().value.IsNegative() { - return NewUndefinedWithReasonf("invalid negative left shift") - } - if !v.Number().value.IsInteger() { - return NewUndefinedWithReasonf("invalid non-integer left shift") - } - - return String{ - value: s.value[:int64(len(s.value))-v.Number().value.IntPart()], - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (s String) String() string { - return `"` + s.value + `"` -} - -func (s String) RawString() string { - return s.value -} - -func (s String) AsString() String { - return s -} - -func (s String) Number() Number { - n, err := NewNumberFromString(s.value) // beware that `.String()` may decorate the value!! - if err != nil { - panic(err) // TODO :-/ - } - - return n -} - -func (s String) Eval() Value { - tree, err := NewTreeBuilder().FromExpr(s.value) - if err != nil { - return s - } - - return tree.Eval() -} - -type Number struct { - Undefined - value decimal.Decimal -} - -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} -} - -func NewNumberFromFloat(f float64) Number { - d := decimal.NewFromFloat(f) - - return Number{value: d} -} - -func NewNumberFromString(s string) (Number, error) { - d, err := decimal.NewFromString(s) - if err != nil { - return Number{}, errors.WithStack(err) - } - - return Number{value: d}, nil -} - -func (Number) kind() entryKind { - return valueEntryKind -} - -// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package -func (n Number) Equal(other Number) bool { - return n.value.Equal(other.value) -} - -func (n Number) Add(other Value) Value { - if v, ok := other.(Numberer); ok { - return Number{value: n.value.Add(v.Number().value)} - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) Sub(other Value) Value { - if v, ok := other.(Numberer); ok { - return Number{ - value: n.value.Sub(v.Number().value), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) Multiply(other Value) Value { - if v, ok := other.(Numberer); ok { - return Number{ - value: n.value.Mul(v.Number().value), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) Divide(other Value) Value { - if v, ok := other.(Numberer); ok { - return Number{ - value: n.value.Div(v.Number().value), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) PowerOf(other Value) Value { - if v, ok := other.(Numberer); ok { - return Number{ - value: n.value.Pow(v.Number().value), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) Mod(other Value) Value { - if v, ok := other.(Numberer); ok { - return Number{ - value: n.value.Mod(v.Number().value), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) IntPart() Value { - return Number{ - value: n.value.Truncate(0), - } -} - -func (n Number) LShift(other Value) Value { - if v, ok := other.(Numberer); ok { - if v.Number().value.IsNegative() { - return NewUndefinedWithReasonf("invalid negative left shift") - } - if !v.Number().value.IsInteger() { - return NewUndefinedWithReasonf("invalid non-integer left shift") - } - - return Number{ - value: n.value.Mul(decimal.NewFromInt(2).Pow(v.Number().value)).Floor(), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) RShift(other Value) Value { - if v, ok := other.(Numberer); ok { - if v.Number().value.IsNegative() { - return NewUndefinedWithReasonf("invalid negative right shift") - } - if !v.Number().value.IsInteger() { - return NewUndefinedWithReasonf("invalid non-integer right shift") - } - - return Number{ - value: n.value.Div(decimal.NewFromInt(2).Pow(v.Number().value)).Floor(), - } - } - - return NewUndefinedWithReasonf("NaN: %s", other.String()) -} - -func (n Number) Neg() Number { - return Number{ - value: n.value.Neg(), - } -} - -func (n Number) Sin() Number { - return Number{ - value: n.value.Sin(), - } -} - -func (n Number) Cos() Number { - return Number{ - value: n.value.Cos(), - } -} - -func (n Number) Sqrt() Value { - n, err := NewNumberFromString( - new(big.Float).Sqrt(n.value.BigFloat()).String(), - ) - if err != nil { - return NewUndefinedWithReasonf("Sqrt:%s", err.Error()) - } - - return n -} - -func (n Number) Tan() Number { - return Number{ - value: n.value.Tan(), - } -} - -func (n Number) Ln(precision int32) Value { - res, err := n.value.Ln(precision) - if err != nil { - return NewUndefinedWithReasonf("Ln:%s", err.Error()) - } - - return Number{ - value: res, - } -} - -func (n Number) Log(precision int32) Value { - res, err := n.value.Ln(precision + 1) - if err != nil { - return NewUndefinedWithReasonf("Log:%s", err.Error()) - } - - res10, err := decimal.New(10, 0).Ln(precision + 1) - if err != nil { - return NewUndefinedWithReasonf("Log:%s", err.Error()) - } - - return Number{ - value: res.Div(res10).Truncate(precision), - } -} - -func (n Number) Floor() Number { - return Number{ - value: n.value.Floor(), - } -} - -func (n Number) Trunc(precision int32) Number { - return Number{ - value: n.value.Truncate(precision), - } -} - -func (n Number) Factorial() Value { - if !n.value.IsInteger() || n.value.IsNegative() { - return NewUndefinedWithReasonf("Factorial: requires a positive integer, cannot accept %s", n.String()) - } - - res := decimal.NewFromInt(1) - - one := decimal.NewFromInt(1) - i := decimal.NewFromInt(2) - for i.LessThanOrEqual(n.value) { - res = res.Mul(i) - i = i.Add(one) - } - - return Number{ - value: res, - } -} - -func (n Number) LessThan(other Value) Bool { - if v, ok := other.(Numberer); ok { - return NewBool(n.value.LessThan(v.Number().value)) - } - - return False -} - -func (n Number) LessThanOrEqual(other Value) Bool { - if v, ok := other.(Numberer); ok { - return NewBool(n.value.LessThanOrEqual(v.Number().value)) - } - - return False -} - -func (n Number) EqualTo(other Value) Bool { - if v, ok := other.(Numberer); ok { - return NewBool(n.value.Equal(v.Number().value)) - } - - return False -} - -func (n Number) NotEqualTo(other Value) Bool { - return n.EqualTo(other).Not() -} - -func (n Number) GreaterThan(other Value) Bool { - if v, ok := other.(Numberer); ok { - return NewBool(n.value.GreaterThan(v.Number().value)) - } - - return False -} - -func (n Number) GreaterThanOrEqual(other Value) Bool { - if v, ok := other.(Numberer); ok { - return NewBool(n.value.GreaterThanOrEqual(v.Number().value)) - } - - return False -} - -func (n Number) String() string { - return n.value.String() -} - -func (n Number) Bool() Bool { - if n.value.IsZero() { - return False + b, ok := val.(Booler) + if !ok { + return Bool{Undefined: NewUndefinedWithReasonf("value type %T - cannot convert to Bool", val)} } - return True -} - -func (n Number) AsString() String { - return NewString(n.String()) -} - -func (n Number) Number() Number { - return n -} - -func (n Number) Float64() float64 { - return n.value.InexactFloat64() -} - -func (n Number) Int64() int64 { - return n.value.IntPart() -} - -type Bool struct { - Undefined - value bool -} - -func NewBool(b bool) Bool { - return Bool{value: b} -} - -// TODO: another option would be to return a Value and hence allow Undefined when neither True nor False is provided. -func NewBoolFromString(s string) (Bool, error) { - switch s { - case "True": - return True, nil - case "False": - return False, nil - default: - return False, errors.Errorf("'%s' cannot be converted to a Bool", s) - } -} - -func (Bool) kind() entryKind { - return valueEntryKind -} - -// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package -func (b Bool) Equal(other Bool) bool { - return b.value == other.value -} - -func (b Bool) EqualTo(other Value) Bool { - if v, ok := other.(Booler); ok { - return NewBool(b.value == v.Bool().value) - } - return False -} - -func (b Bool) NotEqualTo(other Value) Bool { - return b.EqualTo(other).Not() -} - -func (b Bool) Not() Bool { - return NewBool(!b.value) -} - -func (b Bool) And(other Value) Bool { - if v, ok := other.(Booler); ok { - return NewBool(b.value && v.Bool().value) - } - return False // TODO: should Bool be a Maybe? -} - -func (b Bool) Or(other Value) Bool { - if v, ok := other.(Booler); ok { - return NewBool(b.value || v.Bool().value) - } - return False // TODO: should Bool be a Maybe? -} - -func (b Bool) Bool() Bool { - return b -} - -func (b Bool) String() string { - if b.value { - return "True" - } - 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()) -} - -var ( - False = NewBool(false) - True = NewBool(true) -) - -// Undefined is a special gal.Value that indicates an undefined evaluation outcome. -// -// This can be as a first class citizen, when an error occurs -// (e.g. a '/' operator without the left hand side). -// -// All implementors of gal.Value also encapsulate an Undefined value. -// This ensures a default behaviour as defined by "Undefined" -// when none is available on the implementor. -// For instance, Bool does not support RShift() and does not implement it. -// However, since Bool encapsulates an Undefined value, it will return -// an Undefined value when RShift() is called on it. -type Undefined struct { - reason string // optional -} - -func NewUndefined() Undefined { - return Undefined{} -} - -func NewUndefinedWithReasonf(format string, a ...any) Undefined { - return Undefined{ - reason: fmt.Sprintf(format, a...), - } -} - -func (Undefined) kind() entryKind { - return unknownEntryKind -} - -// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package -func (u Undefined) Equal(other Undefined) bool { - return u.reason == other.reason -} - -func (u Undefined) EqualTo(other Value) Bool { - return False -} - -func (u Undefined) NotEqualTo(other Value) Bool { - return True -} - -func (u Undefined) GreaterThan(other Value) Bool { - return False -} - -func (u Undefined) GreaterThanOrEqual(other Value) Bool { - return False -} - -func (u Undefined) LessThan(other Value) Bool { - return False -} - -func (u Undefined) LessThanOrEqual(other Value) Bool { - return False -} - -func (Undefined) Add(Value) Value { - return Undefined{} -} - -func (Undefined) Sub(Value) Value { - return Undefined{} -} - -func (Undefined) Multiply(Value) Value { - return Undefined{} -} - -func (Undefined) Divide(Value) Value { - return Undefined{} -} - -func (Undefined) PowerOf(Value) Value { - return Undefined{} -} - -func (Undefined) Mod(Value) Value { - return Undefined{} -} - -func (Undefined) LShift(Value) Value { - return Undefined{} -} - -func (Undefined) RShift(Value) Value { - return Undefined{} -} - -func (Undefined) And(other Value) Bool { - // perhaps this should be a panic... Or else Bool should be a Maybe? - return False -} - -func (Undefined) Or(other Value) Bool { - // perhaps this should be a panic... Or else Bool should be a Maybe? - return False -} - -func (u Undefined) String() string { - if u.reason == "" { - return "undefined" - } - return "undefined: " + u.reason -} - -func (u Undefined) AsString() String { - return NewString(u.String()) + return b.Bool() } diff --git a/value_bool.go b/value_bool.go new file mode 100644 index 0000000..c14995b --- /dev/null +++ b/value_bool.go @@ -0,0 +1,90 @@ +package gal + +import "github.com/pkg/errors" + +type Bool struct { + Undefined + value bool +} + +func NewBool(b bool) Bool { + return Bool{value: b} +} + +// NOTE: another option would be to return: +// Bool{Undefined: NewUndefinedWithReasonf("cannot convert '%s' to Bool", s)} +func NewBoolFromString(s string) (Bool, error) { + switch s { + case "True": + return True, nil + case "False": + return False, nil + default: + return False, errors.Errorf("'%s' cannot be converted to a Bool", s) + } +} + +func (Bool) kind() entryKind { + return valueEntryKind +} + +// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package +func (b Bool) Equal(other Bool) bool { + return b.value == other.value +} + +func (b Bool) EqualTo(other Value) Bool { + if v, ok := other.(Booler); ok { + return NewBool(b.value == v.Bool().value) + } + return False +} + +func (b Bool) NotEqualTo(other Value) Bool { + return b.EqualTo(other).Not() +} + +func (b Bool) Not() Bool { + return NewBool(!b.value) +} + +func (b Bool) And(other Value) Bool { + if v, ok := other.(Booler); ok { + return NewBool(b.value && v.Bool().value) + } + return False // NOTE: should Bool be a Maybe? +} + +func (b Bool) Or(other Value) Bool { + if v, ok := other.(Booler); ok { + return NewBool(b.value || v.Bool().value) + } + return False // NOTE: should Bool be a Maybe? +} + +func (b Bool) Bool() Bool { + return b +} + +func (b Bool) String() string { + if b.value { + return "True" + } + 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()) +} + +var ( + False = NewBool(false) + True = NewBool(true) +) diff --git a/value_multivalue.go b/value_multivalue.go new file mode 100644 index 0000000..e066dc1 --- /dev/null +++ b/value_multivalue.go @@ -0,0 +1,66 @@ +package gal + +import "strings" + +// MultiValue is a container of zero or more Value's. +// 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. +// ... 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 { + Undefined + values []Value +} + +func NewMultiValue(values ...Value) MultiValue { + return MultiValue{values: values} +} + +func (MultiValue) kind() entryKind { + return valueEntryKind +} + +// 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].EqualTo(other.values[i]) == False { + return false + } + } + + return true +} + +func (m MultiValue) String() string { + var vals []string + for _, val := range m.values { + vals = append(vals, val.String()) + } + return strings.Join(vals, `,`) +} + +func (m MultiValue) AsString() String { + return NewString(m.String()) +} + +func (m MultiValue) Get(i int) Value { + if i >= len(m.values) { + return NewUndefinedWithReasonf("out of bounds: trying to get arg #%d on MultiValue that has %d arguments", i, len(m.values)) + } + + return m.values[i] +} + +func (m MultiValue) Size() int { + return len(m.values) +} diff --git a/value_number.go b/value_number.go new file mode 100644 index 0000000..c66d6d2 --- /dev/null +++ b/value_number.go @@ -0,0 +1,315 @@ +package gal + +import ( + "math/big" + + "github.com/pkg/errors" + "github.com/shopspring/decimal" +) + +type Number struct { + Undefined + value decimal.Decimal +} + +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} +} + +func NewNumberFromFloat(f float64) Number { + d := decimal.NewFromFloat(f) + + return Number{value: d} +} + +func NewNumberFromString(s string) (Number, error) { + d, err := decimal.NewFromString(s) + if err != nil { + return Number{}, errors.WithStack(err) + } + + return Number{value: d}, nil +} + +func (Number) kind() entryKind { + return valueEntryKind +} + +// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package +func (n Number) Equal(other Number) bool { + return n.value.Equal(other.value) +} + +func (n Number) Add(other Value) Value { + if v, ok := other.(Numberer); ok { + return Number{value: n.value.Add(v.Number().value)} + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) Sub(other Value) Value { + if v, ok := other.(Numberer); ok { + return Number{ + value: n.value.Sub(v.Number().value), + } + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) Multiply(other Value) Value { + if v, ok := other.(Numberer); ok { + return Number{ + value: n.value.Mul(v.Number().value), + } + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) Divide(other Value) Value { + if v, ok := other.(Numberer); ok { + if v.Number().value.IsZero() { + return NewUndefinedWithReasonf("division by zero") + } + return Number{value: n.value.Div(v.Number().value)} + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) PowerOf(other Value) Value { + if v, ok := other.(Numberer); ok { + return Number{ + value: n.value.Pow(v.Number().value), + } + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) Mod(other Value) Value { + if v, ok := other.(Numberer); ok && !v.Number().value.IsZero() { + return Number{ + value: n.value.Mod(v.Number().value), + } + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) IntPart() Value { + return Number{ + value: n.value.Truncate(0), + } +} + +func (n Number) LShift(other Value) Value { + if v, ok := other.(Numberer); ok { + if v.Number().value.IsNegative() { + return NewUndefinedWithReasonf("invalid negative left shift") + } + if !v.Number().value.IsInteger() { + return NewUndefinedWithReasonf("invalid non-integer left shift") + } + + return Number{ + value: n.value.Mul(decimal.NewFromInt(2).Pow(v.Number().value)).Floor(), + } + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) RShift(other Value) Value { + if v, ok := other.(Numberer); ok { + if v.Number().value.IsNegative() { + return NewUndefinedWithReasonf("invalid negative right shift") + } + if !v.Number().value.IsInteger() { + return NewUndefinedWithReasonf("invalid non-integer right shift") + } + + return Number{ + value: n.value.Div(decimal.NewFromInt(2).Pow(v.Number().value)).Floor(), + } + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +func (n Number) Neg() Number { + return Number{ + value: n.value.Neg(), + } +} + +func (n Number) Sin() Number { + return Number{ + value: n.value.Sin(), + } +} + +func (n Number) Cos() Number { + return Number{ + value: n.value.Cos(), + } +} + +func (n Number) Sqrt() Value { + if n.value.IsNegative() { + return NewUndefinedWithReasonf("square root of negative number: %s", n.String()) + } + n, err := NewNumberFromString( + new(big.Float).Sqrt(n.value.BigFloat()).String(), // NOTE: how about this? d.PowWithPrecision(decimal.New(5, -1), ppp) + ) + if err != nil { + return NewUndefinedWithReasonf("Sqrt:%s", err.Error()) + } + + return n +} + +func (n Number) Tan() Number { + return Number{ + value: n.value.Tan(), + } +} + +func (n Number) Ln(precision int32) Value { + res, err := n.value.Ln(precision) + if err != nil { + return NewUndefinedWithReasonf("Ln:%s", err.Error()) + } + + return Number{ + value: res, + } +} + +func (n Number) Log(precision int32) Value { + res, err := n.value.Ln(precision + 1) + if err != nil { + return NewUndefinedWithReasonf("Log:%s", err.Error()) + } + + res10, err := decimal.New(10, 0).Ln(precision + 1) + if err != nil { + return NewUndefinedWithReasonf("Log:%s", err.Error()) + } + + return Number{ + value: res.Div(res10).Truncate(precision), + } +} + +func (n Number) Floor() Number { + return Number{ + value: n.value.Floor(), + } +} + +func (n Number) Trunc(precision int32) Number { + return Number{ + value: n.value.Truncate(precision), + } +} + +func (n Number) Factorial() Value { + if !n.value.IsInteger() || n.value.IsNegative() { + return NewUndefinedWithReasonf("Factorial: requires a positive integer, cannot accept %s", n.String()) + } + + res := decimal.NewFromInt(1) + + one := decimal.NewFromInt(1) + i := decimal.NewFromInt(2) + for i.LessThanOrEqual(n.value) { + res = res.Mul(i) + i = i.Add(one) + } + + return Number{ + value: res, + } +} + +func (n Number) LessThan(other Value) Bool { + if v, ok := other.(Numberer); ok { + return NewBool(n.value.LessThan(v.Number().value)) + } + + return False +} + +func (n Number) LessThanOrEqual(other Value) Bool { + if v, ok := other.(Numberer); ok { + return NewBool(n.value.LessThanOrEqual(v.Number().value)) + } + + return False +} + +func (n Number) EqualTo(other Value) Bool { + if v, ok := other.(Numberer); ok { + return NewBool(n.value.Equal(v.Number().value)) + } + + return False +} + +func (n Number) NotEqualTo(other Value) Bool { + return n.EqualTo(other).Not() +} + +func (n Number) GreaterThan(other Value) Bool { + if v, ok := other.(Numberer); ok { + return NewBool(n.value.GreaterThan(v.Number().value)) + } + + return False +} + +func (n Number) GreaterThanOrEqual(other Value) Bool { + if v, ok := other.(Numberer); ok { + return NewBool(n.value.GreaterThanOrEqual(v.Number().value)) + } + + return False +} + +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()) +} + +func (n Number) Number() Number { + return n +} + +func (n Number) Float64() float64 { + return n.value.InexactFloat64() +} + +func (n Number) Int64() int64 { + return n.value.IntPart() +} diff --git a/value_number_test.go b/value_number_test.go new file mode 100644 index 0000000..f62ebcf --- /dev/null +++ b/value_number_test.go @@ -0,0 +1,1086 @@ +package gal + +import ( + "reflect" + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" +) + +func TestNewNumber(t *testing.T) { + got := NewNumber(5, -2) + assert.Equal(t, Number{Undefined: Undefined{}, value: decimal.New(5, -2)}, got) +} + +func TestNewNumberFromInt(t *testing.T) { + got := NewNumberFromInt(5) + assert.Equal(t, Number{Undefined: Undefined{}, value: decimal.New(5, 0)}, got) +} + +func TestNewNumberFromFloat(t *testing.T) { + got := NewNumberFromFloat(5.45678) + assert.Equal(t, Number{Undefined: Undefined{}, value: decimal.NewFromFloat(5.45678)}, got) +} + +func TestNewNumberFromString(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want Number + wantErr bool + }{ + { + name: "it creates a number from a string", + args: args{s: "5.45678"}, + want: Number{Undefined: Undefined{}, value: decimal.NewFromFloat(5.45678)}, + wantErr: false, + }, + { + name: "it returns an error when the string is not a number", + args: args{s: "not a number"}, + want: Number{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewNumberFromString(tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("NewNumberFromString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewNumberFromString() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_kind(t *testing.T) { + assert.Equal(t, valueEntryKind, Number{}.kind()) +} + +func TestNumber_Equal(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Number + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "equal", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(5, 0)}}, + want: true, + }, + { + name: "not equal", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(6, 0)}}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Equal(tt.args.other); got != tt.want { + t.Errorf("Number.Equal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Add(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + { + name: "add two positive numbers", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(3, 0)}}, + want: Number{value: decimal.New(8, 0)}, + }, + { + name: "add a positive and a negative number", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(2, 0)}, + }, + { + name: "add two negative numbers", + fields: fields{value: decimal.New(-5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(-8, 0)}, + }, + { + name: "add a number and non-Numberer", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: NewMultiValue()}, + want: NewUndefinedWithReasonf("NaN: %s", MultiValue{}.String()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Add(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Add() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Sub(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + { + name: "subtract two positive numbers", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(3, 0)}}, + want: Number{value: decimal.New(2, 0)}, + }, + { + name: "subtract a positive and a negative number", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(8, 0)}, + }, + { + name: "subtract two negative numbers", + fields: fields{value: decimal.New(-5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(-2, 0)}, + }, + { + name: "subtract a number and non-Numberer", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: NewMultiValue()}, + want: NewUndefinedWithReasonf("NaN: %s", MultiValue{}.String()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Sub(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Sub() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Multiply(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + { + name: "multiply two positive numbers", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(3, 0)}}, + want: Number{value: decimal.New(15, 0)}, + }, + { + name: "multiply a positive and a negative number", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(-15, 0)}, + }, + { + name: "multiply two negative numbers", + fields: fields{value: decimal.New(-5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(15, 0)}, + }, + { + name: "multiply a number and non-Numberer", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: NewMultiValue()}, + want: NewUndefinedWithReasonf("NaN: %s", MultiValue{}.String()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Multiply(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Multiply() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Divide(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + { + name: "divide two positive numbers", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(3, 0)}}, + want: Number{value: decimal.New(5, 0).Div(decimal.New(3, 0))}, + }, + { + name: "divide a positive and a negative number", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(5, 0).Div(decimal.New(-3, 0))}, + }, + { + name: "divide two negative numbers", + fields: fields{value: decimal.New(-5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(-5, 0).Div(decimal.New(-3, 0))}, + }, + { + name: "divide a number and non-Numberer", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: NewMultiValue()}, + want: NewUndefinedWithReasonf("NaN: %s", MultiValue{}.String()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Divide(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Divide() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_PowerOf(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + { + name: "power of two positive numbers", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(3, 0)}}, + want: Number{value: decimal.New(125, 0)}, + }, + { + name: "power of a positive and a negative number", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(8e13, -16)}, + }, + { + name: "power of two negative numbers", + fields: fields{value: decimal.New(-5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(-8e13, -16)}, + }, + { + name: "power of a number and non-Numberer", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: NewMultiValue()}, + want: NewUndefinedWithReasonf("NaN: %s", MultiValue{}.String()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.PowerOf(tt.args.other); !assert.Equal(t, tt.want, got) { + t.Errorf("Number.PowerOf() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Mod(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + { + name: "modulus of two positive numbers", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(3, 0)}}, + want: Number{value: decimal.New(2, 0)}, + }, + { + name: "modulus of a positive and a negative number", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(2, 0)}, + }, + { + name: "modulus of two negative numbers", + fields: fields{value: decimal.New(-5, 0)}, + args: args{other: Number{value: decimal.New(-3, 0)}}, + want: Number{value: decimal.New(-2, 0)}, + }, + { + name: "modulus of a number and non-Numberer", + fields: fields{value: decimal.New(5, 0)}, + args: args{other: NewMultiValue()}, + want: NewUndefinedWithReasonf("NaN: %s", MultiValue{}.String()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Mod(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Mod() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_IntPart(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.IntPart(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.IntPart() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_LShift(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.LShift(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.LShift() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_RShift(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.RShift(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.RShift() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Neg(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Neg(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Neg() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Sin(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Sin(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Sin() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Cos(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Cos(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Cos() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Sqrt(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Sqrt(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Sqrt() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Tan(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Tan(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Tan() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Ln(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + precision int32 + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Ln(tt.args.precision); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Ln() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Log(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + precision int32 + } + tests := []struct { + name string + fields fields + args args + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Log(tt.args.precision); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Log() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Floor(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Floor(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Floor() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Trunc(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + precision int32 + } + tests := []struct { + name string + fields fields + args args + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Trunc(tt.args.precision); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Trunc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Factorial(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Value + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Factorial(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Factorial() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_LessThan(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.LessThan(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.LessThan() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_LessThanOrEqual(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.LessThanOrEqual(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.LessThanOrEqual() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_EqualTo(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.EqualTo(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.EqualTo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_NotEqualTo(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.NotEqualTo(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.NotEqualTo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_GreaterThan(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.GreaterThan(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.GreaterThan() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_GreaterThanOrEqual(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + type args struct { + other Value + } + tests := []struct { + name string + fields fields + args args + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.GreaterThanOrEqual(tt.args.other); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.GreaterThanOrEqual() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_String(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.String(); got != tt.want { + t.Errorf("Number.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Bool(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Bool(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Bool() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_AsString(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want String + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.AsString(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.AsString() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Number(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want Number + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Number(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Number.Number() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Float64(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want float64 + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Float64(); got != tt.want { + t.Errorf("Number.Float64() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumber_Int64(t *testing.T) { + type fields struct { + Undefined Undefined + value decimal.Decimal + } + tests := []struct { + name string + fields fields + want int64 + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Number{ + Undefined: tt.fields.Undefined, + value: tt.fields.value, + } + if got := n.Int64(); got != tt.want { + t.Errorf("Number.Int64() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/value_string.go b/value_string.go new file mode 100644 index 0000000..3a91cd4 --- /dev/null +++ b/value_string.go @@ -0,0 +1,170 @@ +package gal + +import "strings" + +type String struct { + Undefined + value string +} + +func NewString(s string) String { + return String{value: s} +} + +func (String) kind() entryKind { + return valueEntryKind +} + +// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package +func (s String) Equal(other String) bool { + return s.value == other.value +} + +func (s String) LessThan(other Value) Bool { + if v, ok := other.(Stringer); ok { + return NewBool(s.value < v.AsString().value) + } + + return False +} + +func (s String) LessThanOrEqual(other Value) Bool { + if v, ok := other.(Stringer); ok { + return NewBool(s.value <= v.AsString().value) + } + + return False +} + +func (s String) EqualTo(other Value) Bool { + if v, ok := other.(Stringer); ok { + return NewBool(s.value == v.AsString().value) // beware to compare what's comparable: do NOT use s.value == v.String() because String() may decorate the value (see String and MultiValue for example) + } + + return False +} + +func (s String) NotEqualTo(other Value) Bool { + return s.EqualTo(other).Not() +} + +func (s String) GreaterThan(other Value) Bool { + if v, ok := other.(Stringer); ok { + return NewBool(s.value > v.AsString().value) + } + + return False +} + +func (s String) GreaterThanOrEqual(other Value) Bool { + if v, ok := other.(Stringer); ok { + return NewBool(s.value >= v.AsString().value) + } + + return False +} + +func (s String) Add(other Value) Value { + if v, ok := other.(Stringer); ok { + return String{value: s.value + v.AsString().value} + } + + return NewUndefinedWithReasonf("cannot Add non-string to a string") +} + +func (s String) Multiply(other Value) Value { + if v, ok := other.(Numberer); ok { + count := v.Number().value + if !count.IsInteger() || count.IsNegative() { + return NewUndefinedWithReasonf("String.Multiply: invalid repeat count: %s", count.String()) + } + n := count.IntPart() + if int64(int(n)) != n { // overflow check + return NewUndefinedWithReasonf("String.Multiply: repeat count overflows on this architecture") + } + return String{value: strings.Repeat(s.value, int(n))} + } + + return NewUndefinedWithReasonf("NaN: %s", other.String()) +} + +// TODO: add test to confirm this is correct! +func (s String) LShift(other Value) Value { + v, ok := other.(Numberer) + if !ok { + return NewUndefinedWithReasonf("NaN: %s", other.String()) + } + + if v.Number().value.IsNegative() { + return NewUndefinedWithReasonf("invalid negative left shift") + } + if !v.Number().value.IsInteger() { + return NewUndefinedWithReasonf("invalid non-integer left shift") + } + + idx64 := v.Number().value.IntPart() + if idx64 < 0 { + return NewUndefinedWithReasonf("left shift [%s]: out of range", other.String()) + } + if idx64 > int64(len(s.value)) { + return String{} + } + + return String{value: s.value[int(idx64):]} +} + +// TODO: add test to confirm this is correct! +func (s String) RShift(other Value) Value { + v, ok := other.(Numberer) + if !ok { + return NewUndefinedWithReasonf("NaN: %s", other.String()) + } + + if v.Number().value.IsNegative() { + return NewUndefinedWithReasonf("invalid negative right shift") + } + if !v.Number().value.IsInteger() { + return NewUndefinedWithReasonf("invalid non-integer right shift") + } + + shift := v.Number().value.IntPart() + if shift < 0 { + return NewUndefinedWithReasonf("right shift [%s]: out of range", other.String()) + } + limit := int64(len(s.value)) + if shift > limit { + return String{} + } + + return String{value: s.value[:int64(len(s.value))-v.Number().value.IntPart()]} +} + +func (s String) String() string { + return `"` + s.value + `"` +} + +func (s String) RawString() string { + return s.value +} + +func (s String) AsString() String { + return s +} + +func (s String) Number() Number { + n, err := NewNumberFromString(s.value) // beware that `.String()` may decorate the value!! + if err != nil { + return Number{Undefined: NewUndefinedWithReasonf("cannot convert %s to Number: %s", s.String(), err.Error())} + } + + return n +} + +func (s String) Eval() Value { + tree, err := NewTreeBuilder().FromExpr(s.value) + if err != nil { + return s + } + + return tree.Eval() +} diff --git a/value_undefined.go b/value_undefined.go new file mode 100644 index 0000000..5cedc8d --- /dev/null +++ b/value_undefined.go @@ -0,0 +1,119 @@ +package gal + +import "fmt" + +// Undefined is a special gal.Value that indicates an undefined evaluation outcome. +// +// This can be as a first class citizen, when an error occurs +// (e.g. a '/' operator without the left hand side). +// +// All implementors of gal.Value also encapsulate an Undefined value. +// This ensures a default behaviour as defined by "Undefined" +// when none is available on the implementor. +// For instance, Bool does not support RShift() and does not implement it. +// However, since Bool encapsulates an Undefined value, it will return +// an Undefined value when RShift() is called on it. +type Undefined struct { + reason string // optional +} + +func NewUndefined() Undefined { + return Undefined{} +} + +func NewUndefinedWithReasonf(format string, a ...any) Undefined { + return Undefined{ + reason: fmt.Sprintf(format, a...), + } +} + +func (Undefined) kind() entryKind { + return unknownEntryKind +} + +// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package +func (u Undefined) Equal(other Undefined) bool { + return u.reason == other.reason +} + +func (u Undefined) EqualTo(other Value) Bool { + return False +} + +func (u Undefined) NotEqualTo(other Value) Bool { + return True +} + +func (u Undefined) GreaterThan(other Value) Bool { + return False +} + +func (u Undefined) GreaterThanOrEqual(other Value) Bool { + return False +} + +func (u Undefined) LessThan(other Value) Bool { + return False +} + +func (u Undefined) LessThanOrEqual(other Value) Bool { + return False +} + +func (u Undefined) Add(Value) Value { + return u +} + +func (u Undefined) Sub(Value) Value { + return u +} + +func (u Undefined) Multiply(Value) Value { + return u +} + +func (u Undefined) Divide(Value) Value { + return u +} + +func (u Undefined) PowerOf(Value) Value { + return u +} + +func (u Undefined) Mod(Value) Value { + return u +} + +func (u Undefined) LShift(Value) Value { + return u +} + +func (u Undefined) RShift(Value) Value { + return u +} + +func (Undefined) And(other Value) Bool { + return Bool{Undefined: NewUndefinedWithReasonf("error: '%T/%s':'%s' cannot use And with Undefined", other, other.kind().String(), other.String())} +} + +func (Undefined) Or(other Value) Bool { + return Bool{Undefined: NewUndefinedWithReasonf("error: '%T/%s':'%s' cannot use Or with Undefined", other, other.kind().String(), other.String())} +} + +func (u Undefined) String() string { + if u.reason == "" { + return "undefined" + } + return "undefined: " + u.reason +} + +func (u Undefined) AsString() String { + return NewString(u.String()) +} + +// The purpose of this method is to allow the user to check if a Value is undefined. +// For instance, if a Value is Number but the Undefined property's reason is not empty, +// it means that the Value is not a valid Number. +func (u Undefined) IsUndefined() bool { + return u.reason == "" // NOTE: this is not quite accurate: an Undefined may not hold a reason +}