Skip to content

Commit 6780110

Browse files
committed
add support for MultiValue
1 parent cde7ec4 commit 6780110

10 files changed

+268
-62
lines changed

.github/FUNDING.yml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# These are supported funding model platforms
2+
3+
ko_fi: # Replace with a single Ko-fi username
4+
liberapay: # Replace with a single Liberapay username
5+
polar: # Replace with a single Polar username
6+
buy_me_a_coffee: seborama
7+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Go Eval
22

33
<p align="center">
4-
<a href="https://pkg.go.dev/github.com/seborama/gal/v7">
4+
<a href="https://pkg.go.dev/github.com/seborama/gal/v8">
55
<img src="https://img.shields.io/badge/godoc-reference-blue.svg" alt="gal">
66
</a>
77

8-
<a href="https://goreportcard.com/report/github.com/seborama/gal/v7">
9-
<img src="https://goreportcard.com/badge/github.com/seborama/gal/v7" alt="gal">
8+
<a href="https://goreportcard.com/report/github.com/seborama/gal/v8">
9+
<img src="https://goreportcard.com/badge/github.com/seborama/gal/v8" alt="gal">
1010
</a>
1111
</p>
1212

@@ -105,15 +105,17 @@ Numbers implement arbitrary precision fixed-point decimal arithmetic with [shops
105105
* Types: String, Number, Bool
106106
* Associativity with parentheses
107107
* Functions:
108-
* Pre-defined: pi, cos, floor, sin, sqrt, trunc, and more (see `function.go`: `Eval()`)
108+
* Built-in: pi, cos, floor, sin, sqrt, trunc, and more (see `function.go`: `Eval()`)
109109
* User-defined, injected via `WithFunctions()`
110110
* Variables, defined as `:variable_name:` and injected via `WithVariables()`
111111

112112
## Functions
113113

114114
Function names are case-insensitive.
115115

116-
A function can optionally accept one or more space-separated arguments, but it must return a single Value.
116+
A function can optionally accept one or more **space-separated** arguments, but it must return a single `Value`.
117+
118+
It should be noted that a `MultiValue` type is available that can hold multiple `Value`. A function can use `MultiValue` as its return type to effectively return multiple `Value`'s. Of course, as `MultiValue` is a `Value` type, functions can also accept it as part of their argument(s). Refer to the tests (`TestMultiValueFunctions`) for such examples.
117119

118120
User function definitions are passed as a `map[string]FunctionalValue` using `WithFunctions` when calling `Eval` from `Tree`.
119121

function.go

+6-11
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import (
1111
type FunctionalValue func(...Value) Value
1212

1313
func (fv FunctionalValue) String() string {
14-
// TODO: This is only to support the tests (see Function.Equal)).
15-
// This is not elegant but so far the only solution I have to compare
16-
// Functions in the tests.
1714
return fmt.Sprintf("FunctionalValue @%p", fv)
1815
}
1916

@@ -35,10 +32,8 @@ func (Function) kind() entryKind {
3532
return functionEntryKind
3633
}
3734

35+
// Equal satisfies the external Equaler interface such as in testify assertions and the cmp package
3836
func (f Function) Equal(other Function) bool {
39-
// TODO: This is only to support the tests.
40-
// This is not elegant but so far the only solution I have to compare
41-
// Functions in the tests.
4237
return f.Name == other.Name &&
4338
f.BodyFn.String() == other.BodyFn.String() &&
4439
cmp.Equal(f.Args, other.Args)
@@ -58,7 +53,7 @@ func (f Function) Eval(opts ...treeOption) Value {
5853
return f.BodyFn(args...)
5954
}
6055

61-
var preDefinedFunctions = map[string]FunctionalValue{
56+
var builtInFunctions = map[string]FunctionalValue{
6257
"pi": Pi,
6358
"factorial": Factorial,
6459
"cos": Cos,
@@ -69,14 +64,14 @@ var preDefinedFunctions = map[string]FunctionalValue{
6964
"trunc": Trunc,
7065
}
7166

72-
// PreDefinedFunction returns a pre-defined function body if known.
73-
// It returns `nil` when no pre-defined function exists by the specified name.
67+
// BuiltInFunction returns a built-in function body if known.
68+
// It returns `nil` when no built-in function exists by the specified name.
7469
// This signals the Evaluator to attempt to find a user defined function.
75-
func PreDefinedFunction(name string) FunctionalValue {
70+
func BuiltInFunction(name string) FunctionalValue {
7671
// note: for now function names are arbitrarily case-insensitive
7772
lowerName := strings.ToLower(name)
7873

79-
bodyFn, ok := preDefinedFunctions[lowerName]
74+
bodyFn, ok := builtInFunctions[lowerName]
8075
if ok {
8176
return bodyFn
8277
}

function_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"math"
55
"testing"
66

7-
"github.com/seborama/gal/v7"
7+
"github.com/seborama/gal/v8"
88
"github.com/stretchr/testify/assert"
99
)
1010

gal_test.go

+177-38
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package gal_test
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/google/go-cmp/cmp"
7-
"github.com/seborama/gal/v7"
8+
"github.com/seborama/gal/v8"
89
"github.com/stretchr/testify/assert"
910
)
1011

@@ -65,6 +66,56 @@ func TestTreeBuilder_FromExpr_UnknownVariable(t *testing.T) {
6566
}
6667
}
6768

69+
func TestEval_Boolean(t *testing.T) {
70+
expr := `2 > 1`
71+
val := gal.Parse(expr).Eval()
72+
assert.Equal(t, gal.True.String(), val.String())
73+
74+
expr = `2 > 2`
75+
val = gal.Parse(expr).Eval()
76+
assert.Equal(t, gal.False.String(), val.String())
77+
78+
expr = `2 >= 2`
79+
val = gal.Parse(expr).Eval()
80+
assert.Equal(t, gal.True.String(), val.String())
81+
82+
expr = `2 < 1`
83+
val = gal.Parse(expr).Eval()
84+
assert.Equal(t, gal.False.String(), val.String())
85+
86+
expr = `2 < 2`
87+
val = gal.Parse(expr).Eval()
88+
assert.Equal(t, gal.False.String(), val.String())
89+
90+
expr = `2 <= 2`
91+
val = gal.Parse(expr).Eval()
92+
assert.Equal(t, gal.True.String(), val.String())
93+
94+
expr = `2 != 2`
95+
val = gal.Parse(expr).Eval()
96+
assert.Equal(t, gal.False.String(), val.String())
97+
98+
expr = `1 != 2`
99+
val = gal.Parse(expr).Eval()
100+
assert.Equal(t, gal.True.String(), val.String())
101+
102+
expr = `3 != 2`
103+
val = gal.Parse(expr).Eval()
104+
assert.Equal(t, gal.True.String(), val.String())
105+
106+
expr = `2 == 2`
107+
val = gal.Parse(expr).Eval()
108+
assert.Equal(t, gal.True.String(), val.String())
109+
110+
expr = `1 == 2`
111+
val = gal.Parse(expr).Eval()
112+
assert.Equal(t, gal.False.String(), val.String())
113+
114+
expr = `3 == 2`
115+
val = gal.Parse(expr).Eval()
116+
assert.Equal(t, gal.False.String(), val.String())
117+
}
118+
68119
func TestWithVariablesAndFunctions(t *testing.T) {
69120
expr := `double(:val1:) + triple(:val2:)`
70121
parsedExpr := gal.Parse(expr)
@@ -143,52 +194,140 @@ func TestWithVariablesAndFunctions(t *testing.T) {
143194
}
144195
}
145196

146-
func TestEval_Boolean(t *testing.T) {
147-
expr := `2 > 1`
148-
val := gal.Parse(expr).Eval()
149-
assert.Equal(t, gal.True.String(), val.String())
197+
func TestNestedFunctions(t *testing.T) {
198+
expr := `double(triple(7))`
199+
parsedExpr := gal.Parse(expr)
150200

151-
expr = `2 > 2`
152-
val = gal.Parse(expr).Eval()
153-
assert.Equal(t, gal.False.String(), val.String())
201+
// step 1: define funcs and vars and Eval the expression
202+
funcs := gal.Functions{
203+
"double": func(args ...gal.Value) gal.Value {
204+
if len(args) != 1 {
205+
return gal.NewUndefinedWithReasonf("double() requires a single argument, got %d", len(args))
206+
}
154207

155-
expr = `2 >= 2`
156-
val = gal.Parse(expr).Eval()
157-
assert.Equal(t, gal.True.String(), val.String())
208+
value, ok := args[0].(gal.Numberer)
209+
if !ok {
210+
return gal.NewUndefinedWithReasonf("double(): syntax error - argument must be a number-like value, got '%v'", args[0])
211+
}
158212

159-
expr = `2 < 1`
160-
val = gal.Parse(expr).Eval()
161-
assert.Equal(t, gal.False.String(), val.String())
213+
return value.Number().Multiply(gal.NewNumber(2))
214+
},
215+
"triple": func(args ...gal.Value) gal.Value {
216+
if len(args) != 1 {
217+
return gal.NewUndefinedWithReasonf("triple() requires a single argument, got %d", len(args))
218+
}
162219

163-
expr = `2 < 2`
164-
val = gal.Parse(expr).Eval()
165-
assert.Equal(t, gal.False.String(), val.String())
220+
value, ok := args[0].(gal.Numberer)
221+
if !ok {
222+
return gal.NewUndefinedWithReasonf("triple(): syntax error - argument must be a number-like value, got '%v'", args[0])
223+
}
166224

167-
expr = `2 <= 2`
168-
val = gal.Parse(expr).Eval()
169-
assert.Equal(t, gal.True.String(), val.String())
225+
return value.Number().Multiply(gal.NewNumber(3))
226+
},
227+
}
170228

171-
expr = `2 != 2`
172-
val = gal.Parse(expr).Eval()
173-
assert.Equal(t, gal.False.String(), val.String())
229+
got := parsedExpr.Eval(
230+
gal.WithFunctions(funcs),
231+
)
232+
expected := gal.NewNumber(42)
233+
assert.Equal(t, expected.String(), got.String())
234+
}
174235

175-
expr = `1 != 2`
176-
val = gal.Parse(expr).Eval()
177-
assert.Equal(t, gal.True.String(), val.String())
236+
// If renaming this test, also update the README.md file, where it is mentioned.
237+
func TestMultiValueFunctions(t *testing.T) {
238+
expr := `sum(div(triple(7) double(4)))`
239+
parsedExpr := gal.Parse(expr)
178240

179-
expr = `3 != 2`
180-
val = gal.Parse(expr).Eval()
181-
assert.Equal(t, gal.True.String(), val.String())
241+
// step 1: define funcs and vars and Eval the expression
242+
funcs := gal.Functions{
243+
"double": func(args ...gal.Value) gal.Value {
244+
if len(args) != 1 {
245+
return gal.NewUndefinedWithReasonf("double() requires a single argument, got %d", len(args))
246+
}
182247

183-
expr = `2 == 2`
184-
val = gal.Parse(expr).Eval()
185-
assert.Equal(t, gal.True.String(), val.String())
248+
value, ok := args[0].(gal.Numberer)
249+
if !ok {
250+
return gal.NewUndefinedWithReasonf("double(): syntax error - argument must be a number-like value, got '%v'", args[0])
251+
}
186252

187-
expr = `1 == 2`
188-
val = gal.Parse(expr).Eval()
189-
assert.Equal(t, gal.False.String(), val.String())
253+
return value.Number().Multiply(gal.NewNumber(2))
254+
},
255+
"triple": func(args ...gal.Value) gal.Value {
256+
if len(args) != 1 {
257+
return gal.NewUndefinedWithReasonf("triple() requires a single argument, got %d", len(args))
258+
}
190259

191-
expr = `3 == 2`
192-
val = gal.Parse(expr).Eval()
193-
assert.Equal(t, gal.False.String(), val.String())
260+
value, ok := args[0].(gal.Numberer)
261+
if !ok {
262+
return gal.NewUndefinedWithReasonf("triple(): syntax error - argument must be a number-like value, got '%v'", args[0])
263+
}
264+
265+
return value.Number().Multiply(gal.NewNumber(3))
266+
},
267+
"div": func(args ...gal.Value) gal.Value {
268+
// returns the division of value1 by value2 as the interger portion and the remainder
269+
if len(args) != 2 {
270+
return gal.NewUndefinedWithReasonf("mult() requires two arguments, got %d", len(args))
271+
}
272+
273+
dividend := args[0].(gal.Numberer).Number()
274+
divisor := args[1].(gal.Numberer).Number()
275+
276+
quotient := dividend.Divide(divisor).(gal.Numberer).Number().IntPart()
277+
remainder := dividend.Number().Sub(quotient.(gal.Number).Multiply(divisor.Number()))
278+
return gal.NewMultiValue(quotient, remainder)
279+
},
280+
"sum": func(args ...gal.Value) gal.Value {
281+
// NOTE: we convert the args to a MultiValue to make this function "bilingual".
282+
// That way, it can receiv either two Numberer's or one single MultiValue that holds 2 Numberer's.
283+
var margs gal.MultiValue
284+
if len(args) == 1 {
285+
fmt.Println("DEBUG - a single MultiValue")
286+
margs = args[0].(gal.MultiValue) // not checking type satisfaction for simplicity
287+
}
288+
if len(args) == 2 {
289+
fmt.Println("DEBUG - two Value's")
290+
margs = gal.NewMultiValue(args...)
291+
}
292+
if margs.Size() != 2 {
293+
return gal.NewUndefinedWithReasonf("sum() requires either two Numberer-type Value's or one MultiValue holdings 2 Numberer's, as arguments, but got %d arguments", margs.Size())
294+
}
295+
296+
value1 := args[0].(gal.MultiValue).Get(0).(gal.Numberer)
297+
value2 := args[0].(gal.MultiValue).Get(1).(gal.Numberer)
298+
299+
return value1.Number().Add(value2.Number())
300+
},
301+
}
302+
303+
got := parsedExpr.Eval(
304+
gal.WithFunctions(funcs),
305+
)
306+
expected := gal.NewNumber(7)
307+
assert.Equal(t, expected.String(), got.String())
308+
}
309+
310+
func TestStringsWithSpaces(t *testing.T) {
311+
expr := `"ab cd" + "ef gh"`
312+
parsedExpr := gal.Parse(expr)
313+
314+
got := parsedExpr.Eval()
315+
assert.Equal(t, "ab cdef gh", got.String())
316+
}
317+
318+
func TestFunctionsAndStringsWithSpaces(t *testing.T) {
319+
expr := `f("ab cd") + f("ef gh")`
320+
parsedExpr := gal.Parse(expr)
321+
322+
got := parsedExpr.Eval(
323+
gal.WithFunctions(gal.Functions{
324+
"f": func(args ...gal.Value) gal.Value {
325+
if len(args) != 1 {
326+
return gal.NewUndefinedWithReasonf("f() requires a single argument, got %d", len(args))
327+
}
328+
return args[0]
329+
},
330+
}),
331+
)
332+
assert.Equal(t, "ab cdef gh", got.String())
194333
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/seborama/gal/v7
1+
module github.com/seborama/gal/v8
22

33
go 1.20
44

tree_builder.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,11 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) {
7474
return nil, err
7575
}
7676
if fname == "" {
77+
// parenthesis grouping, not a real function per-se.
78+
// conceptually, parenthesis grouping is a special case of anonymous identity function
7779
tree = append(tree, v)
7880
} else {
79-
bodyFn := PreDefinedFunction(fname)
81+
bodyFn := BuiltInFunction(fname)
8082
tree = append(tree, NewFunction(fname, bodyFn, v.Split()...))
8183
}
8284

@@ -166,7 +168,7 @@ func extractPart(expr string) (string, exprType, int, error) {
166168
}
167169

168170
// read part - number
169-
// TODO: complex numbers are not supported
171+
// TODO: complex numbers are not supported - could be "native" or via function or perhaps even a MultiValue?
170172
s, l, err := readNumber(expr[pos:])
171173
if err != nil {
172174
return "", unknownType, 0, err

tree_builder_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"testing"
55

66
"github.com/google/go-cmp/cmp"
7-
"github.com/seborama/gal/v7"
7+
"github.com/seborama/gal/v8"
88
"github.com/stretchr/testify/require"
99
)
1010

tree_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"testing"
55

66
"github.com/google/go-cmp/cmp"
7-
"github.com/seborama/gal/v7"
7+
"github.com/seborama/gal/v8"
88
"github.com/stretchr/testify/assert"
99
)
1010

0 commit comments

Comments
 (0)