initial commit
This commit is contained in:
commit
24ca753ba3
18 changed files with 1034 additions and 0 deletions
14
.codecov.yml
Normal file
14
.codecov.yml
Normal file
|
@ -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
|
23
.travis.yml
Normal file
23
.travis.yml
Normal file
|
@ -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)
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
|
@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
20
LICENSE
Normal file
20
LICENSE
Normal file
|
@ -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.
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -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
|
||||||
|
```
|
142
calculator.go
Normal file
142
calculator.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
95
calculator_test.go
Normal file
95
calculator_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
17
checklicense.sh
Executable file
17
checklicense.sh
Executable file
|
@ -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
|
72
defaults.go
Normal file
72
defaults.go
Normal file
|
@ -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
|
||||||
|
}},
|
||||||
|
}
|
38
doc.go
Normal file
38
doc.go
Normal file
|
@ -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"
|
57
examples/logic.go
Normal file
57
examples/logic.go
Normal file
|
@ -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,
|
||||||
|
}))
|
||||||
|
}
|
42
examples/math.go
Normal file
42
examples/math.go
Normal file
|
@ -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,
|
||||||
|
}))
|
||||||
|
}
|
33
function.go
Normal file
33
function.go
Normal file
|
@ -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}
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/neonxp/GoMathExecutor
|
||||||
|
|
||||||
|
go 1.13
|
44
operator.go
Normal file
44
operator.go
Normal file
|
@ -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
|
||||||
|
)
|
242
tokenizer.go
Normal file
242
tokenizer.go
Normal file
|
@ -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 == ')'
|
||||||
|
}
|
98
tokenizer_test.go
Normal file
98
tokenizer_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
types.go
Normal file
55
types.go
Normal file
|
@ -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}
|
||||||
|
}
|
Loading…
Reference in a new issue