From d4bfe7d7243c77c27e4e7074b627f79e8908e822 Mon Sep 17 00:00:00 2001 From: seborama Date: Sun, 27 Apr 2025 23:03:08 +0100 Subject: [PATCH 1/2] Re-org files, update doc --- README.md | 28 +- gal.go | 27 -- gal_test.go | 2 +- object.go | 2 + static/bit.ly_3MDZ9QT.png | Bin 5444 -> 0 bytes tree.go | 151 +------- tree_builder.go | 14 +- tree_config.go | 156 ++++++++ value.go | 773 +++----------------------------------- value_bool.go | 90 +++++ value_multivalue.go | 66 ++++ value_number.go | 311 +++++++++++++++ value_string.go | 162 ++++++++ value_undefined.go | 116 ++++++ 14 files changed, 989 insertions(+), 909 deletions(-) delete mode 100644 static/bit.ly_3MDZ9QT.png create mode 100644 tree_config.go create mode 100644 value_bool.go create mode 100644 value_multivalue.go create mode 100644 value_number.go create mode 100644 value_string.go create mode 100644 value_undefined.go diff --git a/README.md b/README.md index 7b723aa..6e27fbd 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`. @@ -220,6 +220,30 @@ Example: ``` +## 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 4addb1d0be2e61ae5bbdd1480658d0b7665d0428..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5444 zcmcIoc|4R~+rJ5sY*9%iMIl*#WZ&0PG+DCmyNt4%3?oz$5+SnhW6wU*Fe*#d>^uF) zIwtGTVDy}R&tLEJc>jCf`}3L4nft!axz9P*_qx8<_gsdOFZpcojN*)d!74tAqa5|f>7ZQv$tB) zH5=E*cssW#$;4O`10zXegK!R{Y5Dn}zu3)=*gp+GPUn9C=fP{;F5Y^ zJ-mc6=#l2lq&;GjvUVJJ!B;CJDQumEv1nb|0{-*NH|irSB6h-tCud>0&3svn%d@PU8O_x?a|T)%G~cg zfh8*b3lUwKg2>)C?FhGuu@Pn&N(!wCBZQLrPpqnjYM_l#qS6lHo#OH7)lElIzjE0K z�@2(__YW06aCLA9Z*6N63D%R8*7@7pH1! zYMPLic2`zbHX$iVJN2eV_Q#J3W{p7&tCm#pnJg-y?E6reMT44iZ+hO(pYWBnwStI< zh}@1X1kTXVP~+;=tA@tLTB0AYZ9pG7A+?WqPCxa!+z-c)0(^kKAgxFSEP*%LDq#dBlmj^TyYx@j;w)J}*+G zJ^RZnu}h_O(;udiGG4g&_#||6bS$O$PiKvdnT`!nMSr7yZK7W)V)aqy^y$-*H*Y#E z`?1%+r{v_k7f#|1Hal;<4LgG(vqGl`MJ-?B`CVrkkVNhl2lgK&${=bwY61%^JM?}`lEg%qg8p*`1rWXNSP%=QYch`dX0Kv*SKBgxuaVP z_V~9~!znbjbN-a3rsiyW9B+10leNYbw(bc_6BAY>5}BQqrTTEPUi|O5MXv@8UEL_h z#YotED)Ml2IY=EEm4uit$|c-OLn>tymY1Jxw)s|pImyiYlCr1`R2=bsP5pzYmV#@M!;hTz~P>+S9B z_Fwc7tZMINy|o;ioZQMhwX~jba&h%rDE<5EH2Cf9>h`Z+`z-4E`|pb@Dhkid?Z?K& zvBB=$%WRkqRZ-dM<)A^4fB5^l7v-uRzcb8`*D{TJB9&nRoCt#;b)R8=u4!ueZej0% zk&&rcQ`<62#NTaNezcNCo8l~}vNDlwesgv8u)H>_VjaG0*)J9%NlQO})|SBOp$L0k zEOp9tGUsvp!NJFc{17A(H{WIztHXDtqq9?rs0)XeC1+&RE!~E=(18YMiCx&IP18E6%8Hg5!sI9UKOSi`>=M z7fMV8!3U76#ZqZ2NW&*HpFX8dNlw0J(Yv?>6XVQ1Eo7W*meIDM#jT~vsFDp(C<-P6 zV2W_KDRJxIE7{H&$V4z%`}}<+oLUxZO>J#QnTUHWhEst1^78D-oG<9!DXhOBKfgAq ze_H)HbD#DlWNv*=n!JBuadFh%-kyt{-P1|9bg3ma@#V{xE`;II{!epL{xe3Dr{z|- z6c8OwE796%n?6p0!VKpwvN%+27~)9c*@qy?(Q8}#6!b+$e!cgsfKMKGu9JhZYif*I z+iv6+6r3jPd>`uTvvpH^B{$Pm7JHH&MYCeBL8*HU&(~Kx`{XXNG~76OedY$U7IPGp0;Z7eHj(C{LVQ& z68o{dTr_L$bhMVMi%a`vuWNUTl%a)1NqtWQq_qU|u2S!bM1r z<0KuRYr(QA`!?p5`G$su-(-JPRjTONScAa8hLxSRwzj<0x^bllwaXYkVoCz$@iVAm z6pu%($hn0G7Z(@1_NFUV1BODEl;i^DHmh=CB92vQ*Q_3Ap{W@slsCcKE2-@NUdErn zB1q`j&91IHCupc&5_0qM`T&DnB8SxMe&wL25%}vbgU1VV>+2P(ZXW>2wLERkoZbAB zB``j&YL3p1jzwiTHx);2=q8EefyXVr>+E<0Jrq?$p$qx7i{s{w)%Lw;N~N;BdrUn+ zX16*pP=eecI z_J>r35wWX4cZEY0$>SW~9S+q*MMThLyj8TBwQFvj%>lJ5$G+oUxOH`6;vdx zph>KMEFB%4%cDoHLBezm4o+9#W+798uL}rd9vmEmYx8r{KmW`y#MBkW3m1=VQ}_1v zew-vEY>?ylruVb;hfBe~Le0$0msiKCUN$vTf|ZSojQRk;%U}63?i2>QHEq-S~kv-{(#ZS)1$HyC5 zS?OI0TBo9rMyxT@#W(x3J(5{pJmocB8ujlN!FOxHJ)77Z?r zNR6d9ZZgZ&op|B@5EC!f14W^up^pC6E{`#lr9Ppr!ox%MZ`rl#tGe43x1?-CGTV$k$-gpk+Q2QzE%a_pK9 zIoLSv=*vHCX`=koM216d66Sa4Dcfy)8%P3pqpcL)_O9O)(GvmO(z^+}b99UdZQo-I~ zXlQ77Y=KcmIc!1-tAtq*am?vD#={kjRys8a67b<+u;s)EI|O*v?$7ob{(fXqVpemT zBuiK6`2)oUHM~gh0?vC=<%{NYDk%i&jjOB*t3~++QC+%E`$Y&diYzngSfR_X5l|$Xqp+RfL@4pZ9C8 zFROI?$`%@H3Tawt{PE+*i+AtN-JN%3fbvA&zX=Vc8v3BEt^M0H#K}p^!P&XEaSz*m23X1@plWaLX8Zivm7gUUlEJ5NlVH2M-Dm z8?kM3Sr`mPgZUyS4HZwHcduf>a_1C~p&7`XcPj|9)>U0yU3PS_S$(~QNJm!}-<9lv z0{sD?IgL6M9({*OO{=bD?V(h2tL6Em@_+>MlU1V^mzL%qF0(Ag=nD!8qA65B(qdAU z5f+U>rj?UG++7Z5rBEoJ2qg3{ue`i@S9kX<#Y?Yd6A-OW&!8NZmzM|o`?KW~Wllxj z1Ns2@o3i_O1m;eao1gE7@Kd?r)=dVyxd$A}wJqi%+W+wK6*o9Me|y^}hnbg;p33If zM4jh|=s$P*P}u=9M_@!`XuT{OgU*wzi>r`}@8_i#x=L!O_v= z?O^1=M%(tzgy;DXJddnIus`b_WOz|2$fOc(Ui&cchho+}9p8*v_!QUHr&NgWl^9!s;pqnN0o!A`<8Xn3)-uH_i7@Ago-` z20+M;V*Sfyf)DC{fmOE5cP43CTW84lt|Szq(fyL|w3p}ou$l%2@xn@Har5nF)^vh( zG~OrVr&gASOU)DByvddeT-YNK0F$NObIyQCNJuP2i8f|RPX~NX*o?-IlEz~9Tj!da z#e^P|4N7|d1fr$@xxaI(co9(Q^5%SJK9byQ;zA`*VZf*o3r;ZCZ`{bW5px;42`CjD zsbK2r$AqmLCkH*UXl*Xgpr{MzAhy0<<==N&T3RHfq#XBZfe0+j%ZsoIoIkC*!$f6s z;eaOK4sN?&cW_`Je$eC75Dhie%Floe`Kg;-;ufbT%Cg89|YidbYeH>+};JC}7&V7pi90PuZ~ z(j}0l&6XC5$%#mvn%=uGRXx#fyOGuFK9rYOG<;Id)CtL@iqsj_h#{YCb+(9gcccu)$2Pb_^ zdmSI$CfD<_LNcfRv&i&M{l%d>)W2gOF?x1eervQy>BwpjC#?>uWPj(pQOv-TP7?Q} zs@MW6Ke@vr`^Iq6ejk?i@GEbG`HM(A;rLugl%ojtI;kCHzp5bt6g%SX+vF+ThdVAW zN?^wdDkh|8k2bdXmG{$9j1YtRQD6FhuBK5~x=r|_?lQmF2c^ZUKAI*z4z@mya`s-1 zpn)XBBqW5zZVHQw8H&ruNy*BI-xLxPlM@r0db)P{e+=;ObZ~VF{ND$Z%seOp1E5=) LdKzVFHc$TrLI?P| 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..13cf8dd 100644 --- a/value.go +++ b/value.go @@ -1,15 +1,45 @@ package gal -// TODO: could this file be moved to a "value" package and be split into multiple files? - -import ( - "fmt" - "math/big" - "strings" - - "github.com/pkg/errors" - "github.com/shopspring/decimal" -) +import "fmt" + +type Value interface { + valueCalculation + valueLogic + valueHelper +} + +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 valueLogic interface { + LessThan(Value) Bool + LessThanOrEqual(Value) Bool + EqualTo(Value) Bool + NotEqualTo(Value) Bool + GreaterThan(Value) Bool + GreaterThanOrEqual(Value) Bool + And(Value) Bool + Or(Value) Bool +} + +type valueHelper interface { + Stringer + fmt.Stringer + entry + // 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 +58,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 +71,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 +83,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(), + b, ok := val.(Booler) + if !ok { + return Bool{Undefined: NewUndefinedWithReasonf("value type %T - cannot convert to Bool", val)} } -} - -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 - } - 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..af2b3e1 --- /dev/null +++ b/value_number.go @@ -0,0 +1,311 @@ +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 { + 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 + } + 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_string.go b/value_string.go new file mode 100644 index 0000000..40948ea --- /dev/null +++ b/value_string.go @@ -0,0 +1,162 @@ +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 { + 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") + } + + 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):]} + } + + 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") + } + + idx64 := v.Number().value.IntPart() + if idx64 < 0 { + return NewUndefinedWithReasonf("right shift [%s]: out of range", other.String()) + } + if idx64 > int64(len(s.value)) { + return String{} + } + + 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 { + 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..9df9320 --- /dev/null +++ b/value_undefined.go @@ -0,0 +1,116 @@ +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 (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 { + 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()) +} + +func (u Undefined) IsUndefined() bool { + return u.reason == "" // NOTE: this is not quite accurate: an Undefined may not hold a reason +} From c2ce23517866adb211e46506d1c9437d7c1246d1 Mon Sep 17 00:00:00 2001 From: seborama Date: Mon, 28 Apr 2025 23:03:29 +0100 Subject: [PATCH 2/2] linting and bug fixes --- README.md | 28 +- value.go | 10 +- value_number.go | 12 +- value_number_test.go | 1086 ++++++++++++++++++++++++++++++++++++++++++ value_string.go | 80 ++-- value_undefined.go | 35 +- 6 files changed, 1180 insertions(+), 71 deletions(-) create mode 100644 value_number_test.go diff --git a/README.md b/README.md index 6e27fbd..bf22112 100644 --- a/README.md +++ b/README.md @@ -203,19 +203,19 @@ 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 ``` @@ -231,7 +231,7 @@ Example: `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` + expr := `aCar.Stereo.Brand.Name` ``` Dot is an accessor. It can be thought of as a symbol. It is not an operator! diff --git a/value.go b/value.go index 13cf8dd..b0519ec 100644 --- a/value.go +++ b/value.go @@ -4,8 +4,10 @@ import "fmt" type Value interface { valueCalculation + valueComparison valueLogic valueHelper + undefinedChecker } type valueCalculation interface { @@ -19,13 +21,16 @@ type valueCalculation interface { RShift(Value) Value } -type valueLogic interface { +type valueComparison interface { LessThan(Value) Bool LessThanOrEqual(Value) Bool EqualTo(Value) Bool NotEqualTo(Value) Bool GreaterThan(Value) Bool GreaterThanOrEqual(Value) Bool +} + +type valueLogic interface { And(Value) Bool Or(Value) Bool } @@ -34,6 +39,9 @@ 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 diff --git a/value_number.go b/value_number.go index af2b3e1..c66d6d2 100644 --- a/value_number.go +++ b/value_number.go @@ -78,9 +78,10 @@ func (n Number) Multiply(other Value) Value { func (n Number) Divide(other Value) Value { if v, ok := other.(Numberer); ok { - return Number{ - value: n.value.Div(v.Number().value), + 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()) @@ -97,7 +98,7 @@ func (n Number) PowerOf(other Value) Value { } func (n Number) Mod(other Value) Value { - if v, ok := other.(Numberer); ok { + if v, ok := other.(Numberer); ok && !v.Number().value.IsZero() { return Number{ value: n.value.Mod(v.Number().value), } @@ -165,8 +166,11 @@ func (n Number) Cos() Number { } 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(), + 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()) 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 index 40948ea..3a91cd4 100644 --- a/value_string.go +++ b/value_string.go @@ -74,9 +74,15 @@ func (s String) Add(other Value) Value { 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())), + 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()) @@ -84,51 +90,53 @@ func (s String) Multiply(other Value) Value { // 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") - } + v, ok := other.(Numberer) + if !ok { + return NewUndefinedWithReasonf("NaN: %s", other.String()) + } - 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):]} + if v.Number().value.IsNegative() { + return NewUndefinedWithReasonf("invalid negative left shift") + } + if !v.Number().value.IsInteger() { + return NewUndefinedWithReasonf("invalid non-integer left shift") } - return NewUndefinedWithReasonf("NaN: %s", other.String()) + 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 { - 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") - } + v, ok := other.(Numberer) + if !ok { + return NewUndefinedWithReasonf("NaN: %s", other.String()) + } - idx64 := v.Number().value.IntPart() - if idx64 < 0 { - return NewUndefinedWithReasonf("right shift [%s]: out of range", other.String()) - } - if idx64 > int64(len(s.value)) { - return String{} - } + if v.Number().value.IsNegative() { + return NewUndefinedWithReasonf("invalid negative right shift") + } + if !v.Number().value.IsInteger() { + return NewUndefinedWithReasonf("invalid non-integer right shift") + } - return String{ - value: s.value[:int64(len(s.value))-v.Number().value.IntPart()], - } + 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 NewUndefinedWithReasonf("NaN: %s", other.String()) + return String{value: s.value[:int64(len(s.value))-v.Number().value.IntPart()]} } func (s String) String() string { diff --git a/value_undefined.go b/value_undefined.go index 9df9320..5cedc8d 100644 --- a/value_undefined.go +++ b/value_undefined.go @@ -60,36 +60,36 @@ func (u Undefined) LessThanOrEqual(other Value) Bool { return False } -func (Undefined) Add(Value) Value { - return Undefined{} +func (u Undefined) Add(Value) Value { + return u } -func (Undefined) Sub(Value) Value { - return Undefined{} +func (u Undefined) Sub(Value) Value { + return u } -func (Undefined) Multiply(Value) Value { - return Undefined{} +func (u Undefined) Multiply(Value) Value { + return u } -func (Undefined) Divide(Value) Value { - return Undefined{} +func (u Undefined) Divide(Value) Value { + return u } -func (Undefined) PowerOf(Value) Value { - return Undefined{} +func (u Undefined) PowerOf(Value) Value { + return u } -func (Undefined) Mod(Value) Value { - return Undefined{} +func (u Undefined) Mod(Value) Value { + return u } -func (Undefined) LShift(Value) Value { - return Undefined{} +func (u Undefined) LShift(Value) Value { + return u } -func (Undefined) RShift(Value) Value { - return Undefined{} +func (u Undefined) RShift(Value) Value { + return u } func (Undefined) And(other Value) Bool { @@ -111,6 +111,9 @@ 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 }