Skip to content

Commit

Permalink
minor refactoring, add oneliners
Browse files Browse the repository at this point in the history
  • Loading branch information
bhmj committed Feb 23, 2022
1 parent 5947144 commit ca43924
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 35 deletions.
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ help:
echo " <command> is"
echo ""
echo " configure - install tools and dependencies (gocyclo and golangci-lint)"
echo " build - build jsonslice CLI"
echo " run - run jsonslice CLI"
echo " build - build xpression CLI"
echo " run - run xpression CLI"
echo " lint - run linters"
echo " test - run tests"
echo " cover - generate coverage report"
Expand All @@ -33,7 +33,9 @@ run:
CGO_ENABLED=0 go run -ldflags "$(LDFLAGS)" -trimpath $(SRC)

lint:
echo "------ golangci-lint"
golangci-lint run
echo "------ gocyclo"
gocyclo -over 18 .

test:
Expand Down
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## What is it?

This project is a renewed version of expression parser/evaluator used in [jsonslice](https://github.com/bhmj/jsonslice). It is still work in progress so use it with caution.
This project is a renewed version of expression parser/evaluator used in [jsonslice](https://github.com/bhmj/jsonslice). It allows you to evaluate simple arithmetic expressions with variable support.

## Check it out

Expand Down Expand Up @@ -31,8 +31,7 @@ Expression examples:
```Go
// simple expression evaluation (error handling skipped)
tokens, _ := xpression.Parse([]byte(`5 - 3 * (6-12)`))
result, _ := xpression.Evaluate(tokens, nil)
result, _ := xpression.Eval([]byte(`5 - 3 * (6-12)`))
switch result.Type {
case xpression.NumberOperand:
fmt.Println(result.Number)
Expand All @@ -42,15 +41,18 @@ Expression examples:
// external data in expression (aka variables)
foobar := 123
varFunc := func(name []byte, result *xpression.Operator) error {
varFunc := func(name []byte, result *xpression.Operand) error {
mapper := map[string]*int{
`foobar`: &foobar,
}
xpression.SetNumber(float64(*mapper[string(name)]))
ref := mapper[string(name)]
if ref == nil {
return errors.New("unknown variable")
}
result.SetNumber(float64(*ref))
return nil
}
tokens, _ = xpression.Parse([]byte(`27 / foobar`))
result, _ = xpression.Evaluate(tokens, varFunc)
result, _ = xpression.EvalVar([]byte(`27 / foobar`), varFunc)
fmt.Println(result.Number)
```
[Run in Go Playground](https://play.golang.com/p/QRWqM25sX6_P)
Expand Down Expand Up @@ -111,6 +113,7 @@ ok github.com/Knetic/govaluate 9.810s

## Changelog

**0.9.2** (2022-02-23) -- Minor code refactoring. One-liner functions added (Ev al, EvalVar).
**0.9.1** (2022-01-02) -- Variable bounds refined.
**0.9.0** (2021-11-19) -- Memory allocation reduced. Speed optimization.
**0.8.0** (2021-11-11) -- hex numbers support. Production ready.
Expand All @@ -133,9 +136,11 @@ ok github.com/Knetic/govaluate 9.810s
- [x] expression evaluation
- [x] parser test coverage
- [x] evaluator test coverage
- [x] add external reference type (node reference in jsonslice)
- [x] add external reference type aka variable (node reference in jsonslice)
- [x] optimize memory allocations
- [ ] Unicode support!
- [ ] Unicode support
- [ ] add math functions support (?)
- [ ] add math/big support (?)

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion cmd/xpression/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func main() {
if len(os.Args) < 2 {
fmt.Printf("Expression evaluator.\nUsage: %[1]s <expression>", filepath.Base(os.Args[0]))
fmt.Printf("Expression evaluator.\nUsage: %[1]s <expression>\n", filepath.Base(os.Args[0]))
return
}

Expand Down
19 changes: 19 additions & 0 deletions eval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package xpression

// Eval evaluates expression and returns the result. No external variables used. See EvalVar for more.
func Eval(expression []byte) (*Operand, error) {
tokens, err := Parse(expression)
if err != nil {
return nil, err
}
return Evaluate(tokens, nil)
}

// EvalVar evaluates expression and returns the result. External variables can be used via varFunc.
func EvalVar(expression []byte, varFunc VariableFunc) (*Operand, error) {
tokens, err := Parse(expression)
if err != nil {
return nil, err
}
return Evaluate(tokens, varFunc)
}
20 changes: 10 additions & 10 deletions evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var (
}
)

type VariableFunc func([]byte, *Operand) (error)
type VariableFunc func([]byte, *Operand) error

// Evaluate evaluates the previously parsed expression
func Evaluate(tokens []*Token, varFunc VariableFunc) (*Operand, error) {
Expand All @@ -66,14 +66,14 @@ func Evaluate(tokens []*Token, varFunc VariableFunc) (*Operand, error) {

const (
tokenOperand int = 0
tokenResult int = 1
tokensRest int = 2
tokenResult int = 1
tokensRest int = 2
)

// evaluate evaluates expression stored in `tokens` in prefix notation (NPN).
// Usually it takes an operator from the head of the list and then takes 1 or 2 operands from the list,
// depending of the operator type (unary or binary).
// The extreme case is when there is only one operand in the list.
// The edge case is when there is only one operand in the list.
// This function calls itself recursively to evalute operands if needed.
func evaluate(tokens []*Token, varFunc VariableFunc) (*Operand, []*Token, error) {
if len(tokens) == 0 {
Expand All @@ -91,9 +91,9 @@ func evaluate(tokens []*Token, varFunc VariableFunc) (*Operand, []*Token, error)
}

var (
err error
left *Operand
right *Operand
err error
left *Operand
right *Operand
result *Operand
)
result = &tokens[tokenResult].Operand
Expand Down Expand Up @@ -276,15 +276,15 @@ func doCompareNumber(op Operator, left float64, right float64, result *Operand)
}

func opEquality(op Operator) bool {
return op==opEqual || op==opStrictEqual || op==opLE || op==opGE
return op == opEqual || op == opStrictEqual || op == opLE || op == opGE
}

func opLessNotEqual(op Operator) bool {
return op==opL || op==opLE || op==opNotEqual || op==opStrictNotEqual
return op == opL || op == opLE || op == opNotEqual || op == opStrictNotEqual
}

func opGreaterNotEqual(op Operator) bool {
return op==opG || op==opGE || op==opNotEqual || op==opStrictNotEqual
return op == opG || op == opGE || op == opNotEqual || op == opStrictNotEqual
}

func sameInfinities(left, right float64) bool {
Expand Down
15 changes: 13 additions & 2 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ import (
)

func Parse(path []byte) ([]*Token, error) {

tokens, err := lexer(path)
if err != nil {
return nil, err
}

return parser(tokens)
}

func lexer(path []byte) ([]*Token, error) {
path = path[:trimSpaces(path)]
l := len(path)
i := 0

// lexer
tokens := make([]*Token, 0)
var tok *Token
var err error
Expand All @@ -24,8 +33,10 @@ func Parse(path []byte) ([]*Token, error) {
tokens = append(tokens, tok)
}
}
return tokens, nil
}

//parser
func parser(tokens []*Token) ([]*Token, error) {
opStack := new(tokenStack)
result := new(tokenStack)
for _, token := range reverse(tokens) {
Expand Down
24 changes: 13 additions & 11 deletions tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,13 @@ func readHex(input []byte) (float64, error) {
return float64(signed), nil
}

// readVar reads variable matching the following "regex":
// ([^operatorBound]+(\[[^\[]+]\])*)+
// readVar reads variable matching the following "regex": ([^operatorBound]+(\[[^\[]+]\])*)+
// which means:
// 1) a string not containing operatorBound symbols
// 2) followed by optional sequence of one or more square brackets with any symbols between them
// 3) possibly repeated again starting from
// 1) a string not containing operatorBound symbols;
// 2) followed by optional sequence of one or more square brackets with any symbols between them;
// 3) possibly repeated again starting from 1;
// The variable can start with the following characters: a-z, A-Z, $, @.
// Examples of valid variables: "@var", "@.var", "var", "var[1]", "@[1]", "var['foo']", "var[1+2]"
func readVar(path []byte, i int) (int, *Token, error) {
var err error
l := len(path)
Expand Down Expand Up @@ -167,17 +168,19 @@ func skipNumber(input []byte, i int) (int, int, error) {
if input[i] == '0' && i < l-1 && input[i+1] == 'x' {
return skipHex(input, i+2)
}
skipDigits := func() {
for ; i < l && input[i] >= '0' && input[i] <= '9'; i++ {
}
}
// numbers: -2 0.3 .3 1e2 -0.1e-2
// [-][0[.[0]]][e[-]0]
if i < l && input[i] == '-' {
i++
}
for ; i < l && input[i] >= '0' && input[i] <= '9'; i++ {
}
skipDigits()
for ; i < l && input[i] == '.'; i++ {
}
for ; i < l && input[i] >= '0' && input[i] <= '9'; i++ {
}
skipDigits()
if i < l && (input[i] == 'E' || input[i] == 'e') {
i++
} else {
Expand All @@ -186,8 +189,7 @@ func skipNumber(input []byte, i int) (int, int, error) {
if i < l && (input[i] == '+' || input[i] == '-') {
i++
}
for ; i < l && (input[i] >= '0' && input[i] <= '9'); i++ {
}
skipDigits()
return i, numFloat, nil
}

Expand Down

0 comments on commit ca43924

Please sign in to comment.