initial commit

This commit is contained in:
Alexander Kiryukhin 2020-02-13 22:55:13 +03:00
commit 24ca753ba3
18 changed files with 1034 additions and 0 deletions

14
.codecov.yml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
module github.com/neonxp/GoMathExecutor
go 1.13

44
operator.go Normal file
View 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
View 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
View 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
View 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}
}