Skip to content

Commit 3a5552c

Browse files
committed
add logical operators "And", &&, "Or", ||
1 parent edb13cb commit 3a5552c

7 files changed

+151
-33
lines changed

README.md

+36-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
A simple but powerful expression parser and evaluator in Go.
1414

15-
This is a research project.
15+
This project started as a personal research.
1616

1717
## Examples
1818

@@ -86,34 +86,65 @@ func main() {
8686
}
8787
```
8888

89+
## Type interfaces
90+
91+
`gal` comes with pre-defined type interfaces: Numberer, Booler, Stringer (and maybe more in the future).
92+
93+
They allow the general use of types. For instance, the String `"123"` can be converted to the Number `123`.
94+
With `Numberer`, a user-defined function can transparently use String and Number when both hold a number representation.
95+
96+
A user-defined function can do this:
97+
98+
```go
99+
n := args[0].(gal.Numberer).Number()
100+
```
101+
102+
or, for additional type safety:
103+
104+
```go
105+
if value, ok := args[0].(gal.Numberer); !ok {
106+
return gal.NewUndefinedWithReasonf("NaN '%s'", args[0])
107+
}
108+
n := value.Number()
109+
/* ... */
110+
```
111+
112+
Both examples will happily accept a `Value` of type `String` or `Number` and process it as if it were a `Number`.
113+
89114
## Numbers
90115

91116
Numbers implement arbitrary precision fixed-point decimal arithmetic with [shopspring/decimal](https://github.com/shopspring/decimal).
92117

93118
## Supported operations
94119

95-
* Operators: `+` `-` `*` `/` `%` `**` `<<` `>>` `<` `<=` `==` `!=` `>` `>=`
120+
* Operators: `+` `-` `*` `/` `%` `**` `<<` `>>` `<` `<=` `==` `!=` `>` `>=` `And` `&&` `Or` `||`
96121
* [Precedence](https://en.wikipedia.org/wiki/Order_of_operations#Programming_languages), highest to lowest:
97122
* `**`
98123
* `*` `/` `%`
99124
* `+` `-`
100125
* `<<` `>>`
101126
* `<` `<=` `==` `!=` `>` `>=`
102-
* Note: Go classifies bit shift operators with the higher `*`.
127+
* `And` `&&` `Or` `||`
128+
* Notes:
129+
* Go classifies bit shift operators with the higher `*`.
130+
* `&&` is synonymous of `And`.
131+
* `||` is synonymous of `Or`.
103132
* Types: String, Number, Bool
104-
* Associativity with parentheses
133+
* Associativity with parentheses: `(` and `)`
105134
* Functions:
106135
* Built-in: pi, cos, floor, sin, sqrt, trunc, and more (see `function.go`: `Eval()`)
107136
* User-defined, injected via `WithFunctions()`
108137
* Variables, defined as `:variable_name:` and injected via `WithVariables()`
109138

110139
## Functions
111140

141+
A function is defined as a Go type: `type FunctionalValue func(...Value) Value`
142+
112143
Function names are case-insensitive.
113144

114145
A function can optionally accept one or more **space-separated** arguments, but it must return a single `Value`.
115146

116-
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.
147+
It should be noted that a `MultiValue` type is available that can hold multiple `Value` elements. 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 test `TestMultiValueFunctions`, for an example.
117148

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

gal.go

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
)
1414

1515
type Value interface {
16+
// Calculation
1617
Add(Value) Value
1718
Sub(Value) Value
1819
Multiply(Value) Value
@@ -21,12 +22,16 @@ type Value interface {
2122
Mod(Value) Value
2223
LShift(Value) Value
2324
RShift(Value) Value
25+
// Logical
2426
LessThan(Value) Bool
2527
LessThanOrEqual(Value) Bool
2628
EqualTo(Value) Bool
2729
NotEqualTo(Value) Bool
2830
GreaterThan(Value) Bool
2931
GreaterThanOrEqual(Value) Bool
32+
And(Value) Bool
33+
Or(Value) Bool
34+
// Helpers
3035
Stringer
3136
entry
3237
}

gal_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@ func TestEval_Boolean(t *testing.T) {
114114
expr = `3 == 2`
115115
val = gal.Parse(expr).Eval()
116116
assert.Equal(t, gal.False.String(), val.String())
117+
118+
expr = `( 123 == 123 && 12 <= 45 ) Or ( "a" != "b" )`
119+
val = gal.Parse(expr).Eval()
120+
assert.Equal(t, gal.True.String(), val.String())
121+
122+
expr = `( 123 == 123 && 12 <= 45 ) Or ( "b" != "b" )`
123+
val = gal.Parse(expr).Eval()
124+
assert.Equal(t, gal.True.String(), val.String())
125+
126+
expr = `( 123 == 123 && 12 > 45 ) Or ( "b" == "b" )`
127+
val = gal.Parse(expr).Eval()
128+
assert.Equal(t, gal.True.String(), val.String())
129+
130+
expr = `( 123 == 123 And 12 > 45 ) Or ( "b" != "b" )`
131+
val = gal.Parse(expr).Eval()
132+
assert.Equal(t, gal.False.String(), val.String())
117133
}
118134

119135
func TestWithVariablesAndFunctions(t *testing.T) {

operator.go

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const (
2626
NotEqualTo Operator = "!="
2727
GreaterThan Operator = ">"
2828
GreaterThanOrEqual Operator = ">="
29+
And Operator = "And" // TODO: case sentive for now
30+
And2 Operator = "&&"
31+
Or Operator = "Or" // TODO: case sentive for now
32+
Or2 Operator = "||"
2933
)
3034

3135
func powerOperators(o Operator) bool {
@@ -49,3 +53,8 @@ func comparativeOperators(o Operator) bool {
4953
o == LessThan || o == LessThanOrEqual ||
5054
o == EqualTo || o == NotEqualTo
5155
}
56+
57+
func logicalOperators(o Operator) bool {
58+
return o == And || o == And2 ||
59+
o == Or || o == Or2
60+
}

tree.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ func (tree Tree) Eval(opts ...treeOption) Value {
106106
Calc(multiplicativeOperators, cfg).
107107
Calc(additiveOperators, cfg).
108108
Calc(bitwiseShiftOperators, cfg).
109-
Calc(comparativeOperators, cfg)
109+
Calc(comparativeOperators, cfg).
110+
Calc(logicalOperators, cfg)
110111

111112
// TODO: refactor this
112113
// perhaps add Tree.Value() which tests that only one entry is left and that it is a Value
@@ -187,6 +188,7 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree
187188
case operatorEntryKind:
188189
op = e.(Operator)
189190
if isOperatorInPrecedenceGroup(op) {
191+
// same operator precedence: keep operating linearly, do not build a tree
190192
continue
191193
}
192194
if val != nil {
@@ -318,6 +320,12 @@ func calculate(lhs Value, op Operator, rhs Value) Value {
318320
case GreaterThanOrEqual:
319321
outVal = lhs.GreaterThanOrEqual(rhs)
320322

323+
case And, And2:
324+
outVal = lhs.And(rhs)
325+
326+
case Or, Or2:
327+
outVal = lhs.Or(rhs)
328+
321329
default:
322330
return NewUndefinedWithReasonf("unimplemented operator: '%s'", op.String())
323331
}

tree_builder.go

+48-27
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,20 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) {
6363
tree = append(tree, LShift)
6464
case RShift.String():
6565
tree = append(tree, RShift)
66+
case And.String():
67+
tree = append(tree, And)
68+
case And2.String():
69+
tree = append(tree, And2) // NOTE: re-route to And?
70+
case Or.String():
71+
tree = append(tree, Or)
72+
case Or2.String():
73+
tree = append(tree, Or2) // NOTE: re-route to Or?
6674
default:
6775
return nil, errors.Errorf("unknown operator: '%s'", part)
6876
}
6977

7078
case functionType:
71-
fname, l, _ := readFunctionName(part)
79+
fname, l, _ := readFunctionName(part) // ignore err: we already parsed the function name when in extractPart()
7280
v, err := tb.FromExpr(part[l+1 : len(part)-1]) // exclude leading '(' and trailing ')'
7381
if err != nil {
7482
return nil, err
@@ -97,7 +105,7 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) {
97105
idx += length
98106
}
99107

100-
// adjust trees that start with "Plus" or "Minus" followed by a "numberer"
108+
// adjust trees that start with "Plus" or "Minus" followed by a "Numberer"
101109
if tree.TrunkLen() >= 2 {
102110
switch tree[0] {
103111
case Plus:
@@ -149,14 +157,18 @@ func extractPart(expr string) (string, exprType, int, error) {
149157
// conceptually, parenthesis grouping is a special case of anonymous identity function
150158
if expr[pos] == '(' || (expr[pos] >= 'a' && expr[pos] <= 'z') || (expr[pos] >= 'A' && expr[pos] <= 'Z') {
151159
fname, lf, err := readFunctionName(expr[pos:])
152-
if err != nil {
153-
return "", unknownType, 0, err
154-
}
155-
fargs, la, err := readFunctionArguments(expr[pos+lf:])
156-
if err != nil {
160+
switch {
161+
case errors.Is(err, errFunctionNameWithoutParens):
162+
// allow to continue so we can check alphanumerical operator names such as "And", "Or", etc
163+
case err != nil:
157164
return "", unknownType, 0, err
165+
default:
166+
fargs, la, err := readFunctionArguments(expr[pos+lf:])
167+
if err != nil {
168+
return "", unknownType, 0, err
169+
}
170+
return fname + fargs, functionType, pos + lf + la, nil
158171
}
159-
return fname + fargs, functionType, pos + lf + la, nil
160172
}
161173

162174
// read part - operator
@@ -220,6 +232,8 @@ func readVariable(expr string) (string, int, error) {
220232
return expr[:to], to, nil
221233
}
222234

235+
var errFunctionNameWithoutParens = errors.New("function with Parenthesis")
236+
223237
func readFunctionName(expr string) (string, int, error) {
224238
to := 0 // this could be an anonymous identity function (i.e. simple case of parenthesis grouping)
225239

@@ -228,7 +242,7 @@ func readFunctionName(expr string) (string, int, error) {
228242
return expr[:to], to, nil
229243
}
230244
if isBlankSpace(r) {
231-
return "", 0, errors.Errorf("syntax error: invalid character '%c' for function name '%s'", r, expr[:to])
245+
return expr[:to], to, errFunctionNameWithoutParens
232246
}
233247
to += 1
234248
}
@@ -324,28 +338,35 @@ func isBlankSpace(r rune) bool {
324338
}
325339

326340
func readOperator(s string) (string, int) {
327-
if strings.HasPrefix(s, Power.String()) ||
328-
strings.HasPrefix(s, LShift.String()) ||
329-
strings.HasPrefix(s, RShift.String()) ||
330-
strings.HasPrefix(s, EqualTo.String()) ||
331-
strings.HasPrefix(s, NotEqualTo.String()) ||
332-
strings.HasPrefix(s, GreaterThanOrEqual.String()) ||
333-
strings.HasPrefix(s, LessThanOrEqual.String()) {
341+
switch {
342+
case strings.HasPrefix(s, And.String()):
343+
return s[:3], 3
344+
345+
case strings.HasPrefix(s, Power.String()),
346+
strings.HasPrefix(s, LShift.String()),
347+
strings.HasPrefix(s, RShift.String()),
348+
strings.HasPrefix(s, EqualTo.String()),
349+
strings.HasPrefix(s, NotEqualTo.String()),
350+
strings.HasPrefix(s, GreaterThanOrEqual.String()),
351+
strings.HasPrefix(s, LessThanOrEqual.String()),
352+
strings.HasPrefix(s, And2.String()),
353+
strings.HasPrefix(s, Or.String()),
354+
strings.HasPrefix(s, Or2.String()):
334355
return s[:2], 2
335-
}
336356

337-
if strings.HasPrefix(s, Plus.String()) ||
338-
strings.HasPrefix(s, Minus.String()) ||
339-
strings.HasPrefix(s, Divide.String()) ||
340-
strings.HasPrefix(s, Multiply.String()) ||
341-
strings.HasPrefix(s, Power.String()) ||
342-
strings.HasPrefix(s, Modulus.String()) ||
343-
strings.HasPrefix(s, GreaterThan.String()) ||
344-
strings.HasPrefix(s, LessThan.String()) {
357+
case strings.HasPrefix(s, Plus.String()),
358+
strings.HasPrefix(s, Minus.String()),
359+
strings.HasPrefix(s, Divide.String()),
360+
strings.HasPrefix(s, Multiply.String()),
361+
strings.HasPrefix(s, Power.String()),
362+
strings.HasPrefix(s, Modulus.String()),
363+
strings.HasPrefix(s, GreaterThan.String()),
364+
strings.HasPrefix(s, LessThan.String()):
345365
return s[:1], 1
346-
}
347366

348-
return "", 0
367+
default:
368+
return "", 0
369+
}
349370
}
350371

351372
func isOperator(s string) bool {

value.go

+28
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,24 @@ func (b Bool) Not() Bool {
482482
return NewBool(!b.value)
483483
}
484484

485+
func (b Bool) And(other Value) Bool {
486+
if v, ok := other.(Booler); ok {
487+
return NewBool(b.value && v.Bool().value)
488+
}
489+
return False // TODO: should Bool be a Maybe?
490+
}
491+
492+
func (b Bool) Or(other Value) Bool {
493+
if v, ok := other.(Booler); ok {
494+
return NewBool(b.value || v.Bool().value)
495+
}
496+
return False // TODO: should Bool be a Maybe?
497+
}
498+
499+
func (b Bool) Bool() Bool {
500+
return b
501+
}
502+
485503
func (b Bool) String() string { // TODO: return String rather than string?
486504
if b.value {
487505
return "true"
@@ -571,6 +589,16 @@ func (Undefined) RShift(Value) Value {
571589
return Undefined{}
572590
}
573591

592+
func (Undefined) And(other Value) Bool {
593+
// perhaps this should be a panic... Or else Bool should be a Maybe?
594+
return False
595+
}
596+
597+
func (Undefined) Or(other Value) Bool {
598+
// perhaps this should be a panic... Or else Bool should be a Maybe?
599+
return False
600+
}
601+
574602
func (u Undefined) String() string {
575603
if u.reason == "" {
576604
return "undefined"

0 commit comments

Comments
 (0)