aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Kiryukhin <a.kiryukhin@mail.ru>2020-02-13 22:55:13 +0300
committerAlexander Kiryukhin <a.kiryukhin@mail.ru>2020-02-13 22:55:13 +0300
commit24ca753ba3ab0f0d4fdc413c467bc304da06744f (patch)
tree9cdf12a41950369e3784a42173f433a07fda8fd6
initial commit
-rw-r--r--.codecov.yml14
-rw-r--r--.travis.yml23
-rw-r--r--.vscode/launch.json17
-rw-r--r--LICENSE20
-rw-r--r--README.md22
-rw-r--r--calculator.go142
-rw-r--r--calculator_test.go95
-rwxr-xr-xchecklicense.sh17
-rw-r--r--defaults.go72
-rw-r--r--doc.go38
-rw-r--r--examples/logic.go57
-rw-r--r--examples/math.go42
-rw-r--r--function.go33
-rw-r--r--go.mod3
-rw-r--r--operator.go44
-rw-r--r--tokenizer.go242
-rw-r--r--tokenizer_test.go98
-rw-r--r--types.go55
18 files changed, 1034 insertions, 0 deletions
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 <a.kiryukhin@mail.ru>
+
+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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>")
+ # 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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 <a.kiryukhin@mail.ru>
+
+// 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}
+}