From 24ca753ba3ab0f0d4fdc413c467bc304da06744f Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Thu, 13 Feb 2020 22:55:13 +0300 Subject: [PATCH] initial commit --- .codecov.yml | 14 +++ .travis.yml | 23 +++++ .vscode/launch.json | 17 ++++ LICENSE | 20 ++++ README.md | 22 ++++ calculator.go | 142 ++++++++++++++++++++++++++ calculator_test.go | 95 +++++++++++++++++ checklicense.sh | 17 ++++ defaults.go | 72 +++++++++++++ doc.go | 38 +++++++ examples/logic.go | 57 +++++++++++ examples/math.go | 42 ++++++++ function.go | 33 ++++++ go.mod | 3 + operator.go | 44 ++++++++ tokenizer.go | 242 ++++++++++++++++++++++++++++++++++++++++++++ tokenizer_test.go | 98 ++++++++++++++++++ types.go | 55 ++++++++++ 18 files changed, 1034 insertions(+) create mode 100644 .codecov.yml create mode 100644 .travis.yml create mode 100644 .vscode/launch.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 calculator.go create mode 100644 calculator_test.go create mode 100755 checklicense.sh create mode 100644 defaults.go create mode 100644 doc.go create mode 100644 examples/logic.go create mode 100644 examples/math.go create mode 100644 function.go create mode 100644 go.mod create mode 100644 operator.go create mode 100644 tokenizer.go create mode 100644 tokenizer_test.go create mode 100644 types.go diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..7ba991d --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,14 @@ +coverage: + range: 80..100 + round: down + precision: 2 + + status: + project: # measuring the overall project coverage + default: # context, you can create multiple ones with custom titles + enabled: yes # must be yes|true to enable this status + target: 95% # specify the target coverage for each commit status + # option: "auto" (must increase from parent commit or pull request base) + # option: "X%" a static target percentage to hit + if_not_found: success # if parent is not found report status as success, error, or failure + if_ci_failed: error # if ci fails report status as success, error, or failure \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..13bc8d2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: go +sudo: false + +go_import_path: github.com/neonxp/GoMathExecutor +env: + global: + - TEST_TIMEOUT_SCALE=10 + - GO111MODULE=on + +matrix: + include: + - go: 1.12.x + - go: 1.13.x + env: LINT=1 + +script: + - test -z "$LINT" || make lint + - make test + - make bench + +after_success: + - make cover + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c082e4a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}", + "env": {}, + "args": [] + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..055889b --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ + +Copyright (c) 2020 Alexander Kiryukhin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f834764 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# GoMathExecutor [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] + +Package GoMathExecutor provides simple expression executor. + +## Installation + +`go get github.com/neonxp/GoMathExecutor` + +## Usage + +``` +calc := executor.NewCalc() +calc.AddOperators(executor.MathOperators) // Loads default MathOperators (see: defaults.go) +calc.Prepare("2+2*2") // Prepare expression +calc.Execute(nil) // == 6, nil +calc.Prepare("x * (y+z)") // Prepare another expression with variables +calc.Execute(map[string]float64{ + "x": 3, + "y": 2, + "z": 1, +}) // == 9, nil +``` diff --git a/calculator.go b/calculator.go new file mode 100644 index 0000000..21bb769 --- /dev/null +++ b/calculator.go @@ -0,0 +1,142 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +import ( + "errors" + "fmt" +) + +// Calc calculates expressions +type Calc struct { + preparedTokens []*token + functions map[string]*Function + operators map[string]*Operator +} + +// NewCalc instantinates new calculator +func NewCalc() *Calc { + c := &Calc{ + functions: map[string]*Function{}, + operators: map[string]*Operator{}, + } + return c +} + +// Prepare expression before execution +func (c *Calc) Prepare(expression string) error { + t := newTokenizer(expression, c.operators) + if err := t.tokenize(); err != nil { + return err + } + tkns, err := t.toRPN() + if err != nil { + return err + } + c.preparedTokens = tkns + return nil +} + +// Execute prepared expression with variables at `vars` argument +func (c *Calc) Execute(vars map[string]float64) (float64, error) { + if len(c.preparedTokens) == 0 { + return 0, errors.New("must prepare expression") + } + if vars == nil { + vars = map[string]float64{} + } + var stack []float64 + for _, tkn := range c.preparedTokens { + switch tkn.Type { + case literalType: + stack = append(stack, tkn.FValue) + case operatorType: + sz := len(stack) + if sz < 2 { + return 0, errors.New("empty stack") + } + var args []float64 + args, stack = stack[sz-2:], stack[:sz-2] + + if op, ok := c.operators[tkn.SValue]; ok { + res, err := op.Fn(args[0], args[1]) + if err != nil { + return 0, err + } + stack = append(stack, res) + } else { + return 0, fmt.Errorf("unknown operator '%s'", tkn.SValue) + } + case functionType: + fn, exists := c.functions[tkn.SValue] + if !exists { + return 0, fmt.Errorf("unknown function '%s'", tkn.SValue) + } + sz := len(stack) + if sz < fn.Places { + return 0, errors.New("not enough args") + } + var args []float64 + args, stack = stack[sz-fn.Places:], stack[:sz-fn.Places] + res, err := fn.Fn(args...) + if err != nil { + return 0, err + } + stack = append(stack, res) + case variableType: + res, exists := vars[tkn.SValue] + if !exists { + return 0, fmt.Errorf("unknown variable '%s'", tkn.SValue) + } + stack = append(stack, res) + default: + return 0, fmt.Errorf("unknown token %d, %s, %f", tkn.Type, tkn.SValue, tkn.FValue) + } + } + if len(stack) != 1 { + return 0, errors.New("invalid expression") + } + return stack[0], nil +} + +// AddFunction adds custom function +func (c *Calc) AddFunction(cf *Function) { + c.functions[cf.Name] = cf +} + +// AddOperator adds custom operator +func (c *Calc) AddOperator(op *Operator) { + c.operators[op.Op] = op +} + +// AddFunctions xadds many custom functions +func (c *Calc) AddFunctions(funcs []*Function) { + for _, fn := range funcs { + c.AddFunction(fn) + } +} + +// AddOperators adds many custom operators +func (c *Calc) AddOperators(operators []*Operator) { + for _, op := range operators { + c.AddOperator(op) + } +} diff --git a/calculator_test.go b/calculator_test.go new file mode 100644 index 0000000..89eeea1 --- /dev/null +++ b/calculator_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +import "testing" + +func TestCalc(t *testing.T) { + funcs := []*Function{ + NewFunction("negative", func(args ...float64) (f float64, err error) { + return -args[0], nil + }, 1), + NewFunction("sum", func(args ...float64) (f float64, err error) { + return args[0] + args[1], nil + }, 2), + } + operators := []*Operator{ + NewOperator("==", 1, LeftAssoc, func(a, b float64) (float64, error) { + if a == b { + return 1, nil + } + return 0, nil + }), + } + tests := []struct { + name string + expression string + expected float64 + vars map[string]float64 + funcs []*Function + operators []*Operator + }{ + {"simple", "((15/(7-(1+1)))*-3)-(-2+(1+1))", ((15.0 / (7.0 - (1.0 + 1.0))) * -3.0) - (-2.0 + (1.0 + 1.0)), nil, nil, nil}, + {"variables", "a+b*c", 14.0, map[string]float64{"a": 2.0, "b": 3.0, "c": 4.0}, nil, nil}, + {"functions 1 arg", "negative(10)", -10.0, nil, funcs, nil}, + {"functions 2 arg", "negative(sum(10, 20)+20)", -50.0, nil, funcs, nil}, + {"custom operator", "10 == 10", 1, nil, nil, operators}, + {"custom operator 2", "10 == 12", 0, nil, nil, operators}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := NewCalc() + c.AddOperators(MathOperators) + if test.funcs != nil { + c.AddFunctions(test.funcs) + } + if test.operators != nil { + c.AddOperators(test.operators) + } + if err := c.Prepare(test.expression); err != nil { + t.Error(err) + } + actual, err := c.Execute(test.vars) + if err != nil { + t.Error(err) + } + if actual != test.expected { + t.Errorf("Expected %f, actual %f", test.expected, actual) + } + }) + } +} + +func TestCalc2(t *testing.T) { + c := NewCalc() + c.AddOperators(MathOperators) + if err := c.Prepare("((15/(7-(1+1)))*-3)-(-2+(1+1))"); err != nil { + t.Error(err) + } + actual, err := c.Execute(nil) + if err != nil { + t.Error(err) + } + expected := ((15.0 / (7.0 - (1.0 + 1.0))) * -3.0) - (-2.0 + (1.0 + 1.0)) + if actual != expected { + t.Errorf("Expected %f, actual %f", expected, actual) + } +} diff --git a/checklicense.sh b/checklicense.sh new file mode 100755 index 0000000..587c36c --- /dev/null +++ b/checklicense.sh @@ -0,0 +1,17 @@ +#!/bin/bash -e + +ERROR_COUNT=0 +while read -r file +do + case "$(head -1 "${file}")" in + *"Copyright (c) "*" Alexander Kiryukhin ") + # everything's cool + ;; + *) + echo "$file is missing license header." + (( ERROR_COUNT++ )) + ;; + esac +done < <(git ls-files "*\.go") + +exit $ERROR_COUNT \ No newline at end of file diff --git a/defaults.go b/defaults.go new file mode 100644 index 0000000..b450e62 --- /dev/null +++ b/defaults.go @@ -0,0 +1,72 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +import "math" + +// MathOperators is default set for math expressions +var MathOperators = []*Operator{ + {Op: "+", Assoc: LeftAssoc, Priority: 10, Fn: func(a float64, b float64) (float64, error) { return a + b, nil }}, + {Op: "-", Assoc: LeftAssoc, Priority: 10, Fn: func(a float64, b float64) (float64, error) { return a - b, nil }}, + {Op: "*", Assoc: LeftAssoc, Priority: 20, Fn: func(a float64, b float64) (float64, error) { return a * b, nil }}, + {Op: "/", Assoc: LeftAssoc, Priority: 20, Fn: func(a float64, b float64) (float64, error) { return a / b, nil }}, + {Op: "^", Assoc: RightAssoc, Priority: 30, Fn: func(a, b float64) (float64, error) { return math.Pow(a, b), nil }}, +} + +// LogicOperators is default set for logic expressions +var LogicOperators = []*Operator{ + {Op: "==", Assoc: LeftAssoc, Priority: 0, Fn: func(a float64, b float64) (float64, error) { + if a == b { + return 1, nil + } + return 0, nil + }}, + {Op: "!=", Assoc: LeftAssoc, Priority: 0, Fn: func(a float64, b float64) (float64, error) { + if a != b { + return 1, nil + } + return 0, nil + }}, + {Op: ">", Assoc: LeftAssoc, Priority: 0, Fn: func(a float64, b float64) (float64, error) { + if a > b { + return 1, nil + } + return 0, nil + }}, + {Op: "<", Assoc: LeftAssoc, Priority: 0, Fn: func(a float64, b float64) (float64, error) { + if a < b { + return 1, nil + } + return 0, nil + }}, + {Op: ">=", Assoc: LeftAssoc, Priority: 0, Fn: func(a float64, b float64) (float64, error) { + if a >= b { + return 1, nil + } + return 0, nil + }}, + {Op: "<=", Assoc: LeftAssoc, Priority: 0, Fn: func(a float64, b float64) (float64, error) { + if a <= b { + return 1, nil + } + return 0, nil + }}, +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..b75a931 --- /dev/null +++ b/doc.go @@ -0,0 +1,38 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Package GoMathExecutor provides simple expression executor. +// +// Usage: +// +// ``` +// calc := executor.NewCalc() +// calc.AddOperators(executor.MathOperators) // Loads default MathOperators (see: defaults.go) +// calc.Prepare("2+2*2") // Prepare expression +// calc.Execute(nil) // == 6, nil +// calc.Prepare("x * (y+z)") // Prepare another expression with variables +// calc.Execute(map[string]float64{ +// "x": 3, +// "y": 2, +// "z": 1, +// }) // == 9, nil +// ``` + +package executor // import "github.com/neonxp/GoMathExecutor" \ No newline at end of file diff --git a/examples/logic.go b/examples/logic.go new file mode 100644 index 0000000..08669e1 --- /dev/null +++ b/examples/logic.go @@ -0,0 +1,57 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// +build ignore + +package main + +import ( + "log" + + executor "github.com/neonxp/GoMathExecutor" +) + +func main() { + c := executor.NewCalc() + c.AddOperators(executor.MathOperators) + c.AddOperators(executor.LogicOperators) + c.Prepare("x == (y+z)") + log.Println(c.Execute(map[string]float64{ + "x": 10, + "y": 2, + "z": 8, + })) + log.Println(c.Execute(map[string]float64{ + "x": 10, + "y": 2, + "z": 10, + })) + c.Prepare("x != (y+z)") + log.Println(c.Execute(map[string]float64{ + "x": 10, + "y": 2, + "z": 8, + })) + log.Println(c.Execute(map[string]float64{ + "x": 10, + "y": 2, + "z": 10, + })) +} diff --git a/examples/math.go b/examples/math.go new file mode 100644 index 0000000..e649755 --- /dev/null +++ b/examples/math.go @@ -0,0 +1,42 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// +build ignore + +package main + +import ( + "log" + + executor "github.com/neonxp/GoMathExecutor" +) + +func main() { + c := executor.NewCalc() + c.AddOperators(executor.MathOperators) + c.Prepare("2+2*2") + log.Println(c.Execute(nil)) + c.Prepare("x * (y+z)") + log.Println(c.Execute(map[string]float64{ + "x": 3, + "y": 2, + "z": 1, + })) +} diff --git a/function.go b/function.go new file mode 100644 index 0000000..d1d5cc0 --- /dev/null +++ b/function.go @@ -0,0 +1,33 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +// Function represents custom functions +type Function struct { + Name string + Fn func(args ...float64) (float64, error) + Places int +} + +// NewFunction creates Function instance +func NewFunction(name string, fn func(args ...float64) (float64, error), places int) *Function { + return &Function{Name: name, Fn: fn, Places: places} +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..30bd888 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/neonxp/GoMathExecutor + +go 1.13 diff --git a/operator.go b/operator.go new file mode 100644 index 0000000..7c5f194 --- /dev/null +++ b/operator.go @@ -0,0 +1,44 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +// Operator implements math operators +type Operator struct { + Op string + Priority int + Assoc Assoc + Fn func(a float64, b float64) (float64, error) +} + +// NewOperator returns new instance of Operator +func NewOperator(op string, priority int, assoc Assoc, fn func(a float64, b float64) (float64, error)) *Operator { + return &Operator{Op: op, Priority: priority, Assoc: assoc, Fn: fn} +} + +// Assoc right or left association of operator +type Assoc int + +// LeftAssoc for left associated operators +// RighAssoc for right associated operators +const ( + LeftAssoc Assoc = iota + RightAssoc +) diff --git a/tokenizer.go b/tokenizer.go new file mode 100644 index 0000000..df4b08f --- /dev/null +++ b/tokenizer.go @@ -0,0 +1,242 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +import ( + "fmt" + "strconv" +) + +type tokenizer struct { + str string + numberBuffer string + strBuffer string + allowNegative bool + tkns []*token + operators map[string]*Operator +} + +func newTokenizer(str string, operators map[string]*Operator) *tokenizer { + return &tokenizer{str: str, numberBuffer: "", strBuffer: "", allowNegative: true, tkns: []*token{}, operators: operators} +} + +func (t *tokenizer) emptyNumberBufferAsLiteral() error { + if t.numberBuffer != "" { + f, err := strconv.ParseFloat(t.numberBuffer, 64) + if err != nil { + return fmt.Errorf("invalid number %s", t.numberBuffer) + } + t.tkns = append(t.tkns, newToken(literalType, "", f)) + } + t.numberBuffer = "" + return nil +} + +func (t *tokenizer) emptyStrBufferAsVariable() { + if t.strBuffer != "" { + t.tkns = append(t.tkns, newToken(variableType, t.strBuffer, 0)) + t.strBuffer = "" + } +} + +func (t *tokenizer) tokenize() error { + for _, ch := range t.str { + if ch == ' ' { + continue + } + ch := byte(ch) + switch true { + case isAlpha(ch): + if t.numberBuffer != "" { + if err := t.emptyNumberBufferAsLiteral(); err != nil { + return err + } + t.tkns = append(t.tkns, newToken(operatorType, "*", 0)) + t.numberBuffer = "" + } + t.allowNegative = false + t.strBuffer += string(ch) + case isNumber(ch): + t.numberBuffer += string(ch) + t.allowNegative = false + case isDot(ch): + t.numberBuffer += string(ch) + t.allowNegative = false + case isLP(ch): + if t.strBuffer != "" { + t.tkns = append(t.tkns, newToken(functionType, t.strBuffer, 0)) + t.strBuffer = "" + } else if t.numberBuffer != "" { + if err := t.emptyNumberBufferAsLiteral(); err != nil { + return err + } + t.tkns = append(t.tkns, newToken(operatorType, "*", 0)) + t.numberBuffer = "" + } + t.allowNegative = true + t.tkns = append(t.tkns, newToken(leftParenthesisType, "", 0)) + case isRP(ch): + if err := t.emptyNumberBufferAsLiteral(); err != nil { + return err + } + t.emptyStrBufferAsVariable() + t.allowNegative = false + t.tkns = append(t.tkns, newToken(rightParenthesisType, "", 0)) + case isComma(ch): + if err := t.emptyNumberBufferAsLiteral(); err != nil { + return err + } + t.emptyStrBufferAsVariable() + t.tkns = append(t.tkns, newToken(funcSep, "", 0)) + t.allowNegative = true + default: + if t.allowNegative && ch == '-' { + t.numberBuffer += "-" + t.allowNegative = false + continue + } + if err := t.emptyNumberBufferAsLiteral(); err != nil { + return err + } + t.emptyStrBufferAsVariable() + if len(t.tkns) > 0 && t.tkns[len(t.tkns)-1].Type == operatorType { + t.tkns[len(t.tkns)-1].SValue += string(ch) + } else { + t.tkns = append(t.tkns, newToken(operatorType, string(ch), 0)) + } + t.allowNegative = true + } + } + if err := t.emptyNumberBufferAsLiteral(); err != nil { + return err + } + t.emptyStrBufferAsVariable() + return nil +} + +func (t *tokenizer) toRPN() ([]*token, error) { + var tkns []*token + var stack tokenStack + for _, tkn := range t.tkns { + switch tkn.Type { + case literalType: + tkns = append(tkns, tkn) + case variableType: + tkns = append(tkns, tkn) + case functionType: + stack.Push(tkn) + case funcSep: + for stack.Head().Type != leftParenthesisType { + if stack.Head().Type == eof { + return nil, ErrInvalidExpression + } + tkns = append(tkns, stack.Pop()) + } + case operatorType: + leftOp, ok := t.operators[tkn.SValue] + if !ok { + return nil, fmt.Errorf("unknown operator: %s", tkn.SValue) + } + for { + if stack.Head().Type == operatorType { + rightOp, ok := t.operators[stack.Head().SValue] + if !ok { + return nil, fmt.Errorf("unknown operator: %s", stack.Head().SValue) + } + if leftOp.Priority < rightOp.Priority || (leftOp.Priority == rightOp.Priority && leftOp.Assoc == RightAssoc) { + tkns = append(tkns, stack.Pop()) + continue + } + } + break + } + stack.Push(tkn) + case leftParenthesisType: + stack.Push(tkn) + case rightParenthesisType: + for stack.Head().Type != leftParenthesisType { + if stack.Head().Type == eof { + return nil, ErrInvalidParenthesis + } + tkns = append(tkns, stack.Pop()) + } + stack.Pop() + if stack.Head().Type == functionType { + tkns = append(tkns, stack.Pop()) + } + } + } + for stack.Head().Type != eof { + if stack.Head().Type == leftParenthesisType { + return nil, ErrInvalidParenthesis + } + tkns = append(tkns, stack.Pop()) + } + return tkns, nil +} + +type tokenStack struct { + ts []*token +} + +func (ts *tokenStack) Push(t *token) { + ts.ts = append(ts.ts, t) +} + +func (ts *tokenStack) Pop() *token { + if len(ts.ts) == 0 { + return &token{Type: eof} + } + var head *token + head, ts.ts = ts.ts[len(ts.ts)-1], ts.ts[:len(ts.ts)-1] + return head +} + +func (ts *tokenStack) Head() *token { + if len(ts.ts) == 0 { + return &token{Type: eof} + } + return ts.ts[len(ts.ts)-1] +} + +func isComma(ch byte) bool { + return ch == ',' +} + +func isDot(ch byte) bool { + return ch == '.' +} + +func isNumber(ch byte) bool { + return ch >= '0' && ch <= '9' +} + +func isAlpha(ch byte) bool { + return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch == '_' +} + +func isLP(ch byte) bool { + return ch == '(' +} + +func isRP(ch byte) bool { + return ch == ')' +} diff --git a/tokenizer_test.go b/tokenizer_test.go new file mode 100644 index 0000000..8600024 --- /dev/null +++ b/tokenizer_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +import ( + "testing" +) + +func TestTokenize(t *testing.T) { + operators := map[string]*Operator{ + "+": {Op: "+", Assoc: LeftAssoc, Priority: 1, Fn: func(a float64, b float64) (float64, error) { return a + b, nil }}, + "-": {Op: "-", Assoc: LeftAssoc, Priority: 1, Fn: func(a float64, b float64) (float64, error) { return a - b, nil }}, + "*": {Op: "*", Assoc: LeftAssoc, Priority: 2, Fn: func(a float64, b float64) (float64, error) { return a * b, nil }}, + "/": {Op: "/", Assoc: LeftAssoc, Priority: 2, Fn: func(a float64, b float64) (float64, error) { return a / b, nil }}, + } + tk := newTokenizer("((15/(7-(1+1)))*-3)-(-2+(1+1))", operators) + if err := tk.tokenize(); err != nil { + t.Error(err) + } + tkns, err := tk.toRPN() + if err != nil { + t.Error(err) + } + expected := []token{ + {literalType, "", 15}, + {literalType, "", 7}, + {literalType, "", 1}, + {literalType, "", 1}, + {operatorType, "+", 0}, + {operatorType, "-", 0}, + {operatorType, "/", 0}, + {literalType, "", -3}, + {operatorType, "*", 0}, + {literalType, "", -2}, + {literalType, "", 1}, + {literalType, "", 1}, + {operatorType, "+", 0}, + {operatorType, "+", 0}, + {operatorType, "-", 0}, + } + if len(tkns) != len(expected) { + t.Errorf("Expected len = %d, got %d", len(expected), len(tkns)) + } + for i, tkn := range tkns { + if tkn.Type != expected[i].Type { + t.Errorf("Expected type %d, got %d at pos %d", expected[i].Type, tkn.Type, i) + } + if tkn.SValue != expected[i].SValue { + t.Errorf("Expected %s, got %s at pos %d", expected[i].SValue, tkn.SValue, i) + } + if tkn.FValue != expected[i].FValue { + t.Errorf("Expected %f, got %f at pos %d", expected[i].FValue, tkn.FValue, i) + } + } + tk = newTokenizer("a**b==10", operators) + if err := tk.tokenize(); err != nil { + t.Error(err) + } + expected = []token{ + {variableType, "a", 0}, + {operatorType, "**", 0}, + {variableType, "b", 0}, + {operatorType, "==", 0}, + {literalType, "", 10}, + } + if len(tk.tkns) != len(expected) { + t.Errorf("Expected len = %d, got %d", len(expected), len(tkns)) + } + for i, tkn := range tk.tkns { + if tkn.Type != expected[i].Type { + t.Errorf("Expected type %d, got %d at pos %d", expected[i].Type, tkn.Type, i) + } + if tkn.SValue != expected[i].SValue { + t.Errorf("Expected %s, got %s at pos %d", expected[i].SValue, tkn.SValue, i) + } + if tkn.FValue != expected[i].FValue { + t.Errorf("Expected %f, got %f at pos %d", expected[i].FValue, tkn.FValue, i) + } + } +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..ea8d4c4 --- /dev/null +++ b/types.go @@ -0,0 +1,55 @@ +// Copyright (c) 2020 Alexander Kiryukhin + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package executor + +import ( + "errors" +) + +// ErrInvalidExpression invalid expression error +// ErrInvalidParenthesis invalid parenthesis error +var ( + ErrInvalidExpression = errors.New("invalid expression") + ErrInvalidParenthesis = errors.New("invalid parenthesis") +) + +type tokenType int + +const ( + literalType tokenType = iota + variableType + operatorType + leftParenthesisType + rightParenthesisType + functionType + funcSep + eof +) + +type token struct { + Type tokenType + SValue string + FValue float64 +} + +func newToken(ttype tokenType, SValue string, FValue float64) *token { + return &token{Type: ttype, SValue: SValue, FValue: FValue} +}