Skip to content

Commit

Permalink
feat: Add True / False Bools + fix bugs, TODOs, new Stringer (#11)
Browse files Browse the repository at this point in the history
Add True / False Bools + fix bugs, TODOs, new Stringer
  • Loading branch information
seborama authored May 5, 2024
1 parent 3a5552c commit 2569144
Show file tree
Hide file tree
Showing 13 changed files with 1,048 additions and 46 deletions.
823 changes: 823 additions & 0 deletions .golangci.yml

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ Both examples will happily accept a `Value` of type `String` or `Number` and pro

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

## Strings

Strings must be enclosed in double-quotes (`"`) e.g. valid: `"this is a string"`, invalid: `this is a syntax error` (missing double-quotes).

Escapes are supported:
- `"this is \"also\" a valid string"`
- `"this is fine too\\"` (escapes cancel each other out)

## Bools

In additional to boolean expressions, sepcial contants `True` and `False` may be used.

Do not double-quote them, or they will become plain strings!

## Supported operations

* Operators: `+` `-` `*` `/` `%` `**` `<<` `>>` `<` `<=` `==` `!=` `>` `>=` `And` `&&` `Or` `||`
Expand Down
2 changes: 1 addition & 1 deletion function.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func PiLong(args ...Value) Value {
return NewUndefinedWithReasonf("pi() requires no argument, got %d", len(args))
}

pi, _ := NewNumberFromString(Pi51199)
pi, _ := NewNumberFromString(Pi51199) //nolint: errcheck

return pi
}
Expand Down
1 change: 1 addition & 0 deletions function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/seborama/gal/v8"

"github.com/stretchr/testify/assert"
)

Expand Down
4 changes: 4 additions & 0 deletions gal.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package gal

import "fmt"

type exprType int

const (
Expand All @@ -10,6 +12,7 @@ const (
stringType
variableType
functionType
boolType
)

type Value interface {
Expand All @@ -33,6 +36,7 @@ type Value interface {
Or(Value) Bool
// Helpers
Stringer
fmt.Stringer
entry
}

Expand Down
14 changes: 8 additions & 6 deletions gal_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package gal_test

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/seborama/gal/v8"
"github.com/stretchr/testify/assert"

"github.com/seborama/gal/v8"
)

func TestEval(t *testing.T) {
Expand Down Expand Up @@ -130,6 +130,10 @@ func TestEval_Boolean(t *testing.T) {
expr = `( 123 == 123 And 12 > 45 ) Or ( "b" != "b" )`
val = gal.Parse(expr).Eval()
assert.Equal(t, gal.False.String(), val.String())

expr = `True Or False`
val = gal.Parse(expr).Eval()
assert.Equal(t, gal.True.String(), val.String())
}

func TestWithVariablesAndFunctions(t *testing.T) {
Expand Down Expand Up @@ -298,11 +302,9 @@ func TestMultiValueFunctions(t *testing.T) {
// That way, it can receiv either two Numberer's or one single MultiValue that holds 2 Numberer's.
var margs gal.MultiValue
if len(args) == 1 {
fmt.Println("DEBUG - a single MultiValue")
margs = args[0].(gal.MultiValue) // not checking type satisfaction for simplicity
}
if len(args) == 2 {
fmt.Println("DEBUG - two Value's")
margs = gal.NewMultiValue(args...)
}
if margs.Size() != 2 {
Expand All @@ -328,7 +330,7 @@ func TestStringsWithSpaces(t *testing.T) {
parsedExpr := gal.Parse(expr)

got := parsedExpr.Eval()
assert.Equal(t, "ab cdef gh", got.String())
assert.Equal(t, `"ab cdef gh"`, got.String())
}

func TestFunctionsAndStringsWithSpaces(t *testing.T) {
Expand All @@ -345,5 +347,5 @@ func TestFunctionsAndStringsWithSpaces(t *testing.T) {
},
}),
)
assert.Equal(t, "ab cdef gh", got.String())
assert.Equal(t, `"ab cdef gh"`, got.String())
}
74 changes: 70 additions & 4 deletions tree.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
package gal

import (
"fmt"
"log/slog"
"strings"
)

type entryKind int

func (ek entryKind) String() string {
switch ek {
case unknownEntryKind:
return "unknownEntryKind"
case valueEntryKind:
return "valueEntryKind"
case operatorEntryKind:
return "operatorEntryKind"
case treeEntryKind:
return "treeEntryKind"
case functionEntryKind:
return "functionEntryKind"
case variableEntryKind:
return "variableEntryKind"
default:
return fmt.Sprintf("unknown:%d", ek)
}
}

const (
unknownEntryKind entryKind = iota
valueEntryKind
Expand Down Expand Up @@ -90,7 +115,7 @@ func WithFunctions(funcs Functions) treeOption {
// It accepts optional functional parameters to supply user-defined
// entities such as functions and variables.
func (tree Tree) Eval(opts ...treeOption) Value {
//config
// config
cfg := &treeConfig{}

for _, o := range opts {
Expand Down Expand Up @@ -141,22 +166,27 @@ func (tree Tree) Split() []Tree {
// For instance, a tree representing the expression '2 + 5 * 4 / 2' with an operator precedence
// of 'multiplicativeOperators' would read the Tree left to right and return a new Tree that
// represents: '2 + 10' where 10 was calculated (and reduced) from 5 * 4 = 20 / 2 = 10.
//
// nolint: gocognit,gocyclo,cyclop
func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *treeConfig) Tree {
var outTree Tree

var val entry
var op Operator = invalidOperator
var op Operator = invalidOperator //nolint: stylecheck

for i := 0; i < tree.TrunkLen(); i++ {
e := tree[i]
slog.Debug("Tree.Calc: entry in Tree", "i", i, "kind", e.kind().String())
if e == nil {
slog.Debug("Tree.Calc: nil entry in Tree")
return Tree{
NewUndefinedWithReasonf("syntax error: nil value at tree entry #%d - tree: %+v", i, tree),
}
}

switch e.kind() {
case valueEntryKind:
slog.Debug("Tree.Calc: valueEntryKind", "i", i, "Value", e.(Value).String())
if val == nil && op == invalidOperator {
val = e
continue
Expand All @@ -168,9 +198,12 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree
}
}

slog.Debug("Tree.Calc: valueEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "e", e.(Value).String())
val = calculate(val.(Value), op, e.(Value))
slog.Debug("Tree.Calc: valueEntryKind - calculate", "i", i, "result", val.(Value).String())

case treeEntryKind:
slog.Debug("Tree.Calc: treeEntryKind", "i", i)
if val == nil && op != invalidOperator {
return Tree{
NewUndefinedWithReasonf("syntax error: missing left hand side value for operator '%s'", op.String()),
Expand All @@ -184,9 +217,11 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree
}

val = calculate(val.(Value), op, rhsVal)
slog.Debug("Tree.Calc: treeEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "rhsVal", rhsVal.String(), "result", val.(Value).String())

case operatorEntryKind:
op = e.(Operator)
slog.Debug("Tree.Calc: operatorEntryKind", "i", i, "Value", e.(Operator).String())
op = e.(Operator) //nolint: errcheck
if isOperatorInPrecedenceGroup(op) {
// same operator precedence: keep operating linearly, do not build a tree
continue
Expand All @@ -199,7 +234,8 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree
op = invalidOperator

case functionEntryKind:
f := e.(Function)
slog.Debug("Tree.Calc: functionEntryKind", "i", i, "name", e.(Function).Name)
f := e.(Function) //nolint: errcheck
if f.BodyFn == nil {
f.BodyFn = cfg.functions.Function(f.Name)
}
Expand All @@ -210,22 +246,26 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree
}

val = calculate(val.(Value), op, rhsVal)
slog.Debug("Tree.Calc: functionEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "rhsVal", rhsVal.String(), "result", val.(Value).String())

case variableEntryKind:
slog.Debug("Tree.Calc: variableEntryKind", "i", i, "name", e.(Variable).Name)
varName := e.(Variable).Name
rhsVal, ok := cfg.Variable(varName)
if !ok {
return Tree{
NewUndefinedWithReasonf("syntax error: unknown variable name: '%s'", varName),
}
}
slog.Debug("Tree.Calc: variableEntryKind", "i", i, "value", rhsVal.String())

if val == nil {
val = rhsVal
continue
}

val = calculate(val.(Value), op, rhsVal)
slog.Debug("Tree.Calc: variableEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "rhsVal", rhsVal.String(), "result", val.(Value).String())

case unknownEntryKind:
return Tree{e}
Expand Down Expand Up @@ -274,6 +314,32 @@ func (Tree) kind() entryKind {
return treeEntryKind
}

func (tree Tree) String(indents ...string) string {
indent := strings.Join(indents, "")

res := ""
for _, e := range tree {
switch e.kind() {
case unknownEntryKind:
res += fmt.Sprintf(indent+"unknownEntryKind %T\n", e)
case valueEntryKind:
res += fmt.Sprintf(indent+"Value %T %s\n", e, e.(Value).String())
case operatorEntryKind:
res += fmt.Sprintf(indent+"Operator %s\n", e.(Operator).String())
case treeEntryKind:
res += fmt.Sprintf(indent+"Tree {\n%s}\n", e.(Tree).String(" "))
case functionEntryKind:
res += fmt.Sprintf(indent+"Function %s\n", e.(Function).Name)
case variableEntryKind:
res += fmt.Sprintf(indent+"Variable %s\n", e.(Variable).Name)
default:
res += fmt.Sprintf(indent+"undefined %T %s\n", e, e.kind().String())
}
}
return res
}

// nolint: gocognit,gocyclo,cyclop
func calculate(lhs Value, op Operator, rhs Value) Value {
var outVal Value

Expand Down
Loading

0 comments on commit 2569144

Please sign in to comment.