Skip to content

Commit

Permalink
Merge pull request PaesslerAG#71 from machship-mm/decimal-for-merge
Browse files Browse the repository at this point in the history
Add DecimalArithmetic language
  • Loading branch information
generikvault committed Nov 2, 2021
2 parents bfa8e95 + d469bfa commit 9d17f67
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 9 deletions.
18 changes: 18 additions & 0 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ func BenchmarkGval(bench *testing.B) {
expression: `foo.Nested.Funk`,
parameter: fooFailureParameters,
},
{
name: "decimal arithmetic",
expression: "(requests_made * requests_succeeded / 100)",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"requests_made": 99.0,
"requests_succeeded": 90.0,
},
},
{
name: "decimal logic",
expression: "(requests_made * requests_succeeded / 100) >= 90",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"requests_made": 99.0,
"requests_succeeded": 90.0,
},
},
}
for _, benchmark := range benchmarks {
eval, err := Full().NewEvaluable(benchmark.expression)
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module github.com/PaesslerAG/gval

go 1.15

require github.com/PaesslerAG/jsonpath v0.1.0
require (
github.com/PaesslerAG/jsonpath v0.1.0
github.com/shopspring/decimal v1.3.1
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
43 changes: 42 additions & 1 deletion gval.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Package gval provides a generic expression language.
// All functions, infix and prefix operators can be replaced by composing languages into a new one.
//
// The package contains concrete expression languages for common application in text, arithmetic, propositional logic and so on.
// The package contains concrete expression languages for common application in text, arithmetic, decimal arithmetic, propositional logic and so on.
// They can be used as basis for a custom expression language or to evaluate expressions directly.
package gval

Expand All @@ -12,6 +12,8 @@ import (
"reflect"
"text/scanner"
"time"

"github.com/shopspring/decimal"
)

//Evaluate given parameter with given expression in gval full language
Expand Down Expand Up @@ -51,6 +53,17 @@ func Arithmetic() Language {
return arithmetic
}

// DecimalArithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// DecimalArithmetic operators expect decimal.Decimal operands (github.com/shopspring/decimal)
// and are used to calculate money/decimal rather than floating point calculations.
// Called with unfitting input, they try to convert the input to decimal.Decimal.
// They can parse strings and convert any type of int or float.
func DecimalArithmetic() Language {
return decimalArithmetic
}

// Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^).
//
// Bitmask operators expect float64 operands.
Expand Down Expand Up @@ -172,6 +185,34 @@ var arithmetic = NewLanguage(
base,
)

var decimalArithmetic = NewLanguage(
InfixDecimalOperator("+", func(a, b decimal.Decimal) (interface{}, error) { return a.Add(b), nil }),
InfixDecimalOperator("-", func(a, b decimal.Decimal) (interface{}, error) { return a.Sub(b), nil }),
InfixDecimalOperator("*", func(a, b decimal.Decimal) (interface{}, error) { return a.Mul(b), nil }),
InfixDecimalOperator("/", func(a, b decimal.Decimal) (interface{}, error) { return a.Div(b), nil }),
InfixDecimalOperator("%", func(a, b decimal.Decimal) (interface{}, error) { return a.Mod(b), nil }),
InfixDecimalOperator("**", func(a, b decimal.Decimal) (interface{}, error) { return a.Pow(b), nil }),

InfixDecimalOperator(">", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThan(b), nil }),
InfixDecimalOperator(">=", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThanOrEqual(b), nil }),
InfixDecimalOperator("<", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThan(b), nil }),
InfixDecimalOperator("<=", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThanOrEqual(b), nil }),

InfixDecimalOperator("==", func(a, b decimal.Decimal) (interface{}, error) { return a.Equal(b), nil }),
InfixDecimalOperator("!=", func(a, b decimal.Decimal) (interface{}, error) { return !a.Equal(b), nil }),
base,
//Base is before these overrides so that the Base options are overridden
PrefixExtension(scanner.Int, parseDecimal),
PrefixExtension(scanner.Float, parseDecimal),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return decimal.NewFromFloat(i).Neg(), nil
}),
)

var bitmask = NewLanguage(
InfixNumberOperator("^", func(a, b float64) (interface{}, error) { return float64(int64(a) ^ int64(b)), nil }),
InfixNumberOperator("&", func(a, b float64) (interface{}, error) { return float64(int64(a) & int64(b)), nil }),
Expand Down
65 changes: 65 additions & 0 deletions gval_parameterized_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"
"testing"
"time"

"github.com/shopspring/decimal"
)

func TestParameterized(t *testing.T) {
Expand Down Expand Up @@ -624,6 +626,69 @@ func TestParameterized(t *testing.T) {
}{},
want: 2.,
},
{
name: "Decimal math doesn't experience rounding error",
expression: "(x * 12.146) - y",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: decimal.NewFromFloat(156.825),
equalityFunc: decimalEqualityFunc,
},
{
name: "Decimal logical operators fractional difference",
expression: "((x * 12.146) - y) > 156.824999999",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: true,
},
{
name: "Decimal logical operators whole number difference",
expression: "((x * 12.146) - y) > 156",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: true,
},
{
name: "Decimal logical operators exact decimal match against GT",
expression: "((x * 12.146) - y) > 156.825",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: false,
},
{
name: "Decimal logical operators exact equality",
expression: "((x * 12.146) - y) == 156.825",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: true,
},
{
name: "Decimal mixes with string logic with force fail",
expression: `(((x * 12.146) - y) == 156.825) && a == "test" && !b && b`,
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
"a": "test",
"b": false,
},
want: false,
},
},
t,
)
Expand Down
32 changes: 25 additions & 7 deletions gval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import (
"reflect"
"strings"
"testing"

"github.com/shopspring/decimal"
)

type evaluationTest struct {
name string
expression string
extension Language
parameter interface{}
want interface{}
wantErr string
name string
expression string
extension Language
parameter interface{}
want interface{}
equalityFunc func(x, y interface{}) bool
wantErr string
}

func testEvaluate(tests []evaluationTest, t *testing.T) {
Expand All @@ -34,7 +37,11 @@ func testEvaluate(tests []evaluationTest, t *testing.T) {
t.Errorf("Evaluate() error = %v", err)
return
}
if !reflect.DeepEqual(got, tt.want) {
if ef := tt.equalityFunc; ef != nil {
if !ef(got, tt.want) {
t.Errorf("Evaluate(%s) = %v, want %v", tt.expression, got, tt.want)
}
} else if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Evaluate(%s) = %v, want %v", tt.expression, got, tt.want)
}
})
Expand Down Expand Up @@ -100,3 +107,14 @@ var fooFailureParameters = map[string]interface{}{
"foo": foo,
"fooptr": &foo,
}

var decimalEqualityFunc = func(x, y interface{}) bool {
v1, ok1 := x.(decimal.Decimal)
v2, ok2 := y.(decimal.Decimal)

if !ok1 || !ok2 {
return false
}

return v1.Equal(v2)
}
7 changes: 7 additions & 0 deletions language.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"text/scanner"
"unicode"

"github.com/shopspring/decimal"
)

// Language is an expression language
Expand Down Expand Up @@ -222,6 +224,11 @@ func InfixNumberOperator(name string, f func(a, b float64) (interface{}, error))
return newLanguageOperator(name, &infix{number: f})
}

// InfixDecimalOperator for two decimal values.
func InfixDecimalOperator(name string, f func(a, b decimal.Decimal) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{decimal: f})
}

// InfixBoolOperator for two bool values.
func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{boolean: f})
Expand Down
62 changes: 62 additions & 0 deletions operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"reflect"
"strconv"
"strings"

"github.com/shopspring/decimal"
)

type stage struct {
Expand Down Expand Up @@ -80,6 +82,9 @@ func (op *infix) initiate(name string) {
if op.number != nil {
f = getFloatOpFunc(op.number, f, typeConvertion)
}
if op.decimal != nil {
f = getDecimalOpFunc(op.decimal, f, typeConvertion)
}
}
if op.shortCircuit == nil {
op.builder = func(a, b Evaluable) (Evaluable, error) {
Expand Down Expand Up @@ -229,6 +234,59 @@ func getFloatOpFunc(o func(a, b float64) (interface{}, error), f opFunc, typeCon
return f(a, b)
}
}
func convertToDecimal(o interface{}) (decimal.Decimal, bool) {
if i, ok := o.(decimal.Decimal); ok {
return i, true
}
if i, ok := o.(float64); ok {
return decimal.NewFromFloat(i), true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return decimal.Zero, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return decimal.NewFromInt(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return decimal.NewFromFloat(float64(v.Uint())), true
case reflect.Float32, reflect.Float64:
return decimal.NewFromFloat(v.Float()), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return decimal.NewFromFloat(f), true
}
}
return decimal.Zero, false
}
func getDecimalOpFunc(o func(a, b decimal.Decimal) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToDecimal(a)
y, l := convertToDecimal(b)
if k && l {
return o(x, y)
}

return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(decimal.Decimal)
y, l := b.(decimal.Decimal)
if k && l {
return o(x, y)
}

return f(a, b)
}
}

type operator interface {
merge(operator) operator
Expand Down Expand Up @@ -260,6 +318,7 @@ func (pre operatorPrecedence) initiate(name string) {}
type infix struct {
operatorPrecedence
number func(a, b float64) (interface{}, error)
decimal func(a, b decimal.Decimal) (interface{}, error)
boolean func(a, b bool) (interface{}, error)
text func(a, b string) (interface{}, error)
arbitrary func(a, b interface{}) (interface{}, error)
Expand All @@ -273,6 +332,9 @@ func (op infix) merge(op2 operator) operator {
if op.number == nil {
op.number = op2.number
}
if op.decimal == nil {
op.decimal = op2.decimal
}
if op.boolean == nil {
op.boolean = op2.boolean
}
Expand Down
10 changes: 10 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"reflect"
"strconv"
"text/scanner"

"github.com/shopspring/decimal"
)

//ParseExpression scans an expression into an Evaluable.
Expand Down Expand Up @@ -90,6 +92,14 @@ func parseNumber(c context.Context, p *Parser) (Evaluable, error) {
return p.Const(n), nil
}

func parseDecimal(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseFloat(p.TokenText(), 64)
if err != nil {
return nil, err
}
return p.Const(decimal.NewFromFloat(n)), nil
}

func parseParentheses(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseExpression(c)
if err != nil {
Expand Down

0 comments on commit 9d17f67

Please sign in to comment.