diff --git a/README.md b/README.md index e43bd78..720c2c6 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,11 @@ Expr is a simple, lightweight and performant programming toolkit for evaluating ``` ## Usage +### Bind +For binding variables into expr string, see [Bind](https://github.com/muktihari/expr/blob/master/bind/README.md) + +### Explain +For explaining step-by-step operations, see [Explain](https://github.com/muktihari/expr/blob/master/explain/README.md) ### Any - Any parses the given expr string into any type it returns as a result. e.g: diff --git a/arithmetic.go b/arithmetic.go index 8523315..a3b6e05 100644 --- a/arithmetic.go +++ b/arithmetic.go @@ -31,13 +31,13 @@ func arithmetic(v, vx, vy *Visitor, binaryExpr *ast.BinaryExpr) { return } - // Auto figure out value type + // NumericTypeAuto: Auto figure out value type if vx.kind == KindImag || vy.kind == KindImag { calculateComplex(v, vx, vy, binaryExpr) return } - // calculate any others float64 + // calculate other types as float64 calculateFloat(v, vx, vy, binaryExpr) } diff --git a/bind/README.md b/bind/README.md new file mode 100644 index 0000000..a0fbaa4 --- /dev/null +++ b/bind/README.md @@ -0,0 +1,40 @@ +# Bind +Bind binds variables values into string expression in fast and safety way. When the variable pattern is invalid, it return an error. + +e.g.: should be `{price}` but written as `{price }` (with space) will return an error. + + +## Usage +### Bind +```go + s := "{price} - ({price} * {discount-percentage})" + v, err := bind.Bind(s, + "price", 100, + "discount-percentage", 0.1, + ) + if err != nil { + panic(err) + } + + fmt.Println(v) // "100 - (100 * 0.1)" +``` + +### SetIdent +Using custom identifier. +```go + bind.SetIdent(&bind.Ident{ + Prefix: ":", + Suffix: "", + }) + + s := ":price - (:price * :discount_percentage)" + v, err := bind.Bind(s, + "price", 100, + "discount_percentage", 0.1, + ) + if err != nil { + panic(err) + } + + fmt.Println(v) // "100 - (100 * 0.1)" +``` diff --git a/bind/bind.go b/bind/bind.go new file mode 100644 index 0000000..5f1acc8 --- /dev/null +++ b/bind/bind.go @@ -0,0 +1,245 @@ +// bind is an helper to bind values into variables in the string expression. +package bind + +import ( + "errors" + "fmt" + "strconv" + "strings" + "unicode" +) + +var ( + // ErrKeyValsLengthIsOdd occurs when given keyvals is not matched [key, val] structure. + ErrKeyValsLengthIsOdd = errors.New("keyvals's length is odd") + // ErrKeyvalsIsEmptyOrNil occurs when keyvals is empty or nil + ErrKeyvalsIsEmptyOrNil = errors.New("keyvals is empty or nil") + // ErrKeyIsNotAString occurs when given key in keyvals contains non-string type. + ErrKeyIsNotAString = errors.New("key in keyvals is not a string") + // ErrMalformedVariablePattern occurs when s is malformed or is not valid + ErrMalformedVariablePattern = errors.New("malformed variable pattern") + // ErrEmptyPrefix occurs when prefix is empty "" while it's a mandatory to bind the variables. + ErrEmptyPrefix = errors.New("empty prefix") +) + +var std = &Binder{Ident: DefaultIdent(), Formatter: DefaultFormater()} + +// Bind binds given keyvals values into the given s. Key in keyvals should be a string that consist of alphanumeric [a-z, A-Z, 0-9] and symbol ['-', '_'] only. +// +// - e.g. price after discount calculation expression: +// +// - s: "{price} - ({price} * {discount-percentage})" +// +// - keyvals: ["price", 100, "discount-percentage", 0.1] +// +// - resulting value: "100 - (100 * 0.1)" +// +// Note: If s is a really big string (len(s) > 60k for example) consider creating your own binder using strings.Replacer, see [bind_benchmark_test.go] file. +// +// Otherwise, use this for faster process with low memory footprint and low memory alloc. +func Bind(s string, keyvals ...interface{}) (string, error) { + return std.Bind(s, keyvals...) +} + +// SetIdent sets custom variable identifier to std. See bind.Ident{} for details. +func SetIdent(ident *Ident) { + if ident != nil { + std.Ident = ident + } +} + +// SetIdent sets custom keyvals formatter to std. See bind.Formatter for details. +func SetFormatter(formatter Formatter) { + if formatter != nil { + std.Formatter = formatter + } +} + +// Ident is variable name identifier. Prefix is mandatory when Suffix is optional. +// +// e.g. +// - "{price}" : the "{" is the prefix identifier and "}" is the suffix identifier of variable named price. +// - ":price:" : the ":" is the prefix identifier and ":" is the suffix identifier of variable named price. +// - ":price" : the ":" is the prefix identifier and "" is the suffix identifier of variable named price. +type Ident struct { + Prefix string // Prefix is mandatory + Suffix string // Suffix is optional +} + +func DefaultIdent() *Ident { + return &Ident{ + Prefix: "{", + Suffix: "}", + } +} + +// Formatter formats keyvals values into string values. Key will never be quoted, only the Value will be quoted. +// +// e.g. +// - "price" -> "price" +// - 100 -> "100" +// - 2.1 -> "2.1" +// - struct{}{} -> "{}" +// - nil -> "" +type Formatter func(v interface{}) string + +// DefaultFormater returns format +func DefaultFormater() Formatter { return Format } + +// Binder binds variable values into string expression, it finds the variable name using specified identifier bind.Ident{}. +type Binder struct { + Ident *Ident // variable identifier on string expression + Formatter Formatter // keyvals values formatter. +} + +type SyntaxError struct { + Msg string + Begin int + End int + Value string + Err error +} + +func (s SyntaxError) Error() string { + return fmt.Sprintf("%s [value:\"%s\",beg:%d,end:%d]: %v", s.Msg, s.Value, s.Begin, s.End, s.Err) +} + +func (s SyntaxError) Unwrap() error { return s.Err } + +// Bind binds keyvals values into s, key should be a string and val can be any. If keyvals is nil, s will be returned. +func (b *Binder) Bind(s string, keyvals ...interface{}) (string, error) { + if len(keyvals) == 0 { + return "", ErrKeyvalsIsEmptyOrNil + } + + if len(keyvals)%2 != 0 { + return "", ErrKeyValsLengthIsOdd + } + + if b.Ident == nil { + b.Ident = DefaultIdent() + } + + if b.Ident.Prefix == "" { + return "", ErrEmptyPrefix + } + + if b.Formatter == nil { + b.Formatter = DefaultFormater() + } + + prefix, suffix := b.Ident.Prefix, b.Ident.Suffix + lenPrefix, lenSuffix := len(prefix), len(suffix) + + m := make(map[string]string, len(keyvals)/2) + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + return "", fmt.Errorf("key '%v' is not a string, err: %w", key, ErrKeyIsNotAString) + } + val := b.Formatter(keyvals[i+1]) + key = prefix + key + suffix + m[key] = val + } + + var isPrefixBegin, isBreakBySuffix bool + var begin, end int + + strbuf := new(strings.Builder) + var cur int + for i := 0; i < len(s); i++ { + if !isPrefixBegin { + if i+lenPrefix < len(s) && s[i:i+lenPrefix] == prefix { // find beginning of a prefix + isPrefixBegin = true + isBreakBySuffix = false + begin = i + i += lenPrefix - 1 + } + continue + } + + if lenSuffix != 0 && i+lenSuffix <= len(s) { // check breaking point by a proper suffix if specified + if s[i:i+lenSuffix] == suffix { + end = i + lenSuffix + i += lenSuffix - 1 + + strbuf.WriteString(s[cur:begin]) + strbuf.WriteString(m[s[begin:end]]) + cur = end + + isPrefixBegin = false + isBreakBySuffix = true + continue + } + } + + // check breaking point + r := rune(s[i]) + if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-') { + end = i + strbuf.WriteString(s[cur:begin]) + strbuf.WriteString(m[s[begin:end]]) + cur = end + + isPrefixBegin = false + isBreakBySuffix = false + + if lenSuffix != 0 { // not broken by suffix when it should + return "", &SyntaxError{ + Msg: "suffix is specified but it is broken by '" + string(r) + "' before reaching suffix", + Begin: begin, + End: end, + Value: s[begin:end], + Err: ErrMalformedVariablePattern, + } + } + } + } + + if isPrefixBegin { + if lenSuffix != 0 && !isBreakBySuffix { + return "", &SyntaxError{ + Msg: "suffix is specified but it is not properly ended", + Begin: begin, + End: len(s), + Value: s[begin:], + Err: ErrMalformedVariablePattern, + } + } + } + + strbuf.WriteString(s[cur:]) + + return strbuf.String(), nil +} + +// Format formats given v type into string. withQuote specify whether non-boolean and non-numeric value to be string quoted. +func Format(v interface{}) string { + // declared common used types for faster conversion + switch val := v.(type) { + case int: + return strconv.Itoa(val) + case int64: + return strconv.FormatInt(val, 10) + case float64: + return strconv.FormatFloat(val, 'f', -1, 64) + case complex128: + return strconv.FormatComplex(val, 'f', -1, 128) + case string: + return "\"" + val + "\"" + case bool: + return strconv.FormatBool(val) + case error: + return "\"" + val.Error() + "\"" + case fmt.Stringer: + return "\"" + val.String() + "\"" + default: // slower but it can handle "{}" "[1, 2]" "", etc. + s := fmt.Sprintf("%v", v) // e.g. int32(2) -> 2 + if idx := strings.IndexFunc(s, func(r rune) bool { + return r == '[' || r == ']' || r == '{' || r == '}' || r == '<' || r == '>' + }); idx != -1 { + return "\"" + s + "\"" + } + return s + } +} diff --git a/bind/bind_benchmark_test.go b/bind/bind_benchmark_test.go new file mode 100644 index 0000000..0b93161 --- /dev/null +++ b/bind/bind_benchmark_test.go @@ -0,0 +1,323 @@ +package bind_test + +import ( + "errors" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/muktihari/expr/bind" +) + +var ( + benchDefaultIdent = bind.DefaultIdent() + benchDefaultFormat = bind.DefaultFormater() +) + +// benchBindWithStringsReplacer is the fastest when len(s) > 6k chars, but not that significant in <=600. This implementation +// only replace the exact pattern, when there is small typo like added " " space between prefix and suffix, it will not being replaced. +// example: should be {price}, but written {price }. while default Bind can return an error. +func benchBindWithStringsReplacer(s string, keyvals ...interface{}) (string, error) { + if len(keyvals) == 0 { + return s, nil + } + + if len(keyvals)%2 != 0 { + return "", bind.ErrKeyValsLengthIsOdd + } + + strkeyvals := make([]string, 0, len(keyvals)) + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + return "", fmt.Errorf("key '%v' is not a string, err: %w", key, bind.ErrKeyIsNotAString) + } + val := benchDefaultFormat(keyvals[i+1]) + key = benchDefaultIdent.Prefix + key + benchDefaultIdent.Suffix + strkeyvals = append(strkeyvals, key, val) + } + + return strings.NewReplacer(strkeyvals...).Replace(s), nil +} + +// benchBindWithStringsReplaceAll is the fastest for extra small len(s) (tested: len = 76), but getting slower when len(s) is increasing. +func benchBindWithStringsReplaceAll(s string, keyvals ...interface{}) (string, error) { + if len(keyvals) == 0 { + return s, nil + } + + if len(keyvals)%2 != 0 { + return "", bind.ErrKeyValsLengthIsOdd + } + + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + return "", fmt.Errorf("key '%v' is not a string, err: %w", key, bind.ErrKeyIsNotAString) + } + key, val := keyvals[i].(string), benchDefaultFormat(keyvals[i+1]) + key = benchDefaultIdent.Prefix + key + benchDefaultIdent.Suffix + s = strings.ReplaceAll(s, key, val) + } + + return s, nil +} + +var reg = regexp.MustCompile(`{(.*?)}`) // compiling is expensive, so compile at build time + +// benchBindWithRegexp is the slowest most of the time, but when s is big, it's the second slowest after strings.ReplaceAll. Used for benchmarking. +func benchBindWithRegexp(s string, keyvals ...interface{}) (string, error) { + if len(keyvals) == 0 { + return s, nil + } + + if len(keyvals)%2 != 0 { + return "", bind.ErrKeyValsLengthIsOdd + } + + m := make(map[string]string, len(keyvals)/2) + + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + return "", fmt.Errorf("key '%v' is not a string, err: %w", key, bind.ErrKeyIsNotAString) + } + val := benchDefaultFormat(keyvals[i+1]) + m[benchDefaultIdent.Prefix+key+benchDefaultIdent.Suffix] = val + } + + s = reg.ReplaceAllStringFunc(s, func(s string) string { return m[s] }) + + return s, nil +} + +func TestBindAllBenchmarkBindAlternatives(t *testing.T) { + t.SkipNow() // only comment when you need to test the bind alternatives. + + multiplier := 1 + + tt := []struct { + in string + keyvals []interface{} + out string + err error + }{ + { + in: "{price} - ({price} * {discount-percentage})", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + }, + out: "100 - (100 * 0.1)", + }, + func() struct { + in string + keyvals []interface{} + out string + err error + } { + s := "{price} - ({price} * {discount-percentage})" + for i := 0; i < multiplier; i++ { + s += fmt.Sprintf(" + {Variable%d}", i) + } + r := "100 - (100 * 0.1)" + for i := 0; i < multiplier; i++ { + r += " + 100.213" + } + + keyvals := make([]interface{}, 0, multiplier) + keyvals = append(keyvals, + "price", 100, + "discount-percentage", 0.1, + ) + for i := 0; i < multiplier; i++ { + keyvals = append(keyvals, + fmt.Sprintf("Variable%d", i), 100.213, + ) + } + + return struct { + in string + keyvals []interface{} + out string + err error + }{ + in: s, keyvals: keyvals, out: r, err: nil, + } + }(), + } + + for _, tc := range tt { + tc := tc + t.Run("last 10 chars: "+string(tc.in[len(tc.in)-10:]), func(t *testing.T) { + t.Run("benchBindWithStringsReplaceAll", func(t *testing.T) { + out, err := benchBindWithStringsReplaceAll(tc.in, tc.keyvals...) + if !errors.Is(err, tc.err) { + t.Fatalf("expected error: %v, got: %v", tc.err, err) + } + if out != tc.out { + t.Fatalf("expected out: %v, got: %v", tc.out, out) + } + }) + t.Run("benchBindWithRegexp", func(t *testing.T) { + out, err := benchBindWithRegexp(tc.in, tc.keyvals...) + if !errors.Is(err, tc.err) { + t.Fatalf("expected error: %v, got: %v", tc.err, err) + } + if out != tc.out { + t.Fatalf("expected out: %v, got: %v", tc.out, out) + } + }) + t.Run("benchBindWithStringsReplacer", func(t *testing.T) { + out, err := benchBindWithStringsReplacer(tc.in, tc.keyvals...) + if !errors.Is(err, tc.err) { + t.Fatalf("expected error: %v, got: %v", tc.err, err) + } + if out != tc.out { + t.Fatalf("expected out: %v, got: %v", tc.out, out) + } + }) + }) + } +} + +func multiplyStringTemplate(multiplier int) (str string, strResult string, varInStrCount int, keyvals []interface{}) { + templateStr := "{price} - ({price} * {discount-percentage})" + templateResult := "100 - (100 * 0.1)" + + keyvals = append(keyvals, + "price", 100, + "discount-percentage", 0.1, + ) + + str, varInStrCount = templateStr, 3 + for i := 0; i < multiplier; i++ { + str += fmt.Sprintf(" + %s + {Variable%d}", templateStr, i) + keyvals = append(keyvals, + fmt.Sprintf("Variable%d", i), 100.213, + ) + varInStrCount += 4 + } + + strResult = templateResult + for i := 0; i < multiplier; i++ { + strResult += fmt.Sprintf(" + %s + 100.213", templateResult) + } + + return str, strResult, varInStrCount, keyvals +} + +func BenchmarkBind(b *testing.B) { + type strct struct { + scenario string + str string + strResult string + varInStrCount int + keyvals []interface{} + } + + tt := []strct{ + { + scenario: "extra small", + str: "(({price} * (1 - {discount-percentage})) - {another-discount}) * (1 + {tax})", + strResult: "((100 * (1 - 0.1)) - 1) * (1 + 0.2)", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + "another-discount", 1, + "tax", 0.2, + }, + varInStrCount: 4, + }, + func() strct { + multiplier := 10 + + str, strResult, varInStrCount, keyvals := multiplyStringTemplate(multiplier) + + return strct{ + scenario: "small", + str: str, + strResult: strResult, + varInStrCount: varInStrCount, + keyvals: keyvals, + } + }(), + func() strct { + multiplier := 100 + + str, strResult, varInStrCount, keyvals := multiplyStringTemplate(multiplier) + + return strct{ + scenario: "medium", + str: str, + strResult: strResult, + varInStrCount: varInStrCount, + keyvals: keyvals, + } + }(), + func() strct { + multiplier := 1000 + + str, strResult, varInStrCount, keyvals := multiplyStringTemplate(multiplier) + + return strct{ + scenario: "large", + str: str, + strResult: strResult, + varInStrCount: varInStrCount, + keyvals: keyvals, + } + }(), + } + + for _, tc := range tt { + tc := tc + b.Run(tc.scenario+fmt.Sprintf(" len(s):%d, len(varInStrCount): %d, vars:%d", len(tc.str), tc.varInStrCount, len(tc.keyvals)/2), func(b *testing.B) { + b.Run("Bind", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, err := bind.Bind(tc.str, tc.keyvals...) + if err != nil { + b.Fatalf("expected nil, got: %v", err) + } + if v != tc.strResult { + b.Fatalf("expected value: %s, got: %s", tc.strResult, v) + } + } + }) + b.Run("bindWithStringsReplaceAll", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, err := benchBindWithStringsReplaceAll(tc.str, tc.keyvals...) + if err != nil { + b.Fatalf("expected nil, got: %v", err) + } + if v != tc.strResult { + b.Fatalf("expected value: %s, got: %s", tc.strResult, v) + } + } + }) + b.Run("bindWithRegex", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, err := benchBindWithRegexp(tc.str, tc.keyvals...) + if err != nil { + b.Fatalf("expected nil, got: %v", err) + } + if v != tc.strResult { + b.Fatalf("expected value: %s, got: %s", tc.strResult, v) + } + } + }) + b.Run("benchBindWithStringsReplacer", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, err := benchBindWithStringsReplacer(tc.str, tc.keyvals...) + if err != nil { + b.Fatalf("expected nil, got: %v", err) + } + if v != tc.strResult { + b.Fatalf("expected value: %s, got: %s", tc.strResult, v) + } + } + }) + }) + } +} diff --git a/bind/bind_test.go b/bind/bind_test.go new file mode 100644 index 0000000..414a69d --- /dev/null +++ b/bind/bind_test.go @@ -0,0 +1,233 @@ +package bind + +import ( + "errors" + "fmt" + "testing" + "time" +) + +func TestStdBinder(t *testing.T) { + tt := []struct { + in string + keyvals []interface{} + out string + err error + }{ + { + in: "{price} - ({price} * {discount-percentage})", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + }, + out: "100 - (100 * 0.1)", + }, + { + in: "{is-eligible} && {age} >= 60", + keyvals: []interface{}{ + "is-eligible", true, + "age", 28, + }, + out: "true && 28 >= 60", + }, + { + in: "{is-eligible} && {age} >= 60", + keyvals: []interface{}{ + "is-eligible", true, + 28, 28, + }, + err: ErrKeyIsNotAString, + }, + { + in: "{is-eligible} && {age} >= 60", + keyvals: []interface{}{ + "is-eligible", true, + "age", + }, + err: ErrKeyValsLengthIsOdd, + }, + { + in: "{is-eligible} && {age} >= 60", + keyvals: nil, + err: ErrKeyvalsIsEmptyOrNil, + }, + { + in: "{is-eligible} == \"\"", + keyvals: []interface{}{ + "is-eligible", nil, + }, + out: "\"\" == \"\"", + }, + { + in: "{is-eligible} && {age } >= 60", + keyvals: []interface{}{ + "is-eligible", true, + "age", 28, + }, + err: ErrMalformedVariablePattern, + }, + { + in: "{is-eligible} && {age} >= {old", + keyvals: []interface{}{ + "is-eligible", true, + "age", 28, + "old", 28, + }, + err: ErrMalformedVariablePattern, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.in, func(t *testing.T) { + out, err := Bind(tc.in, tc.keyvals...) + if !errors.Is(err, tc.err) { + t.Fatalf("expected error: %v, got: %v", tc.err, err) + } + if out != tc.out { + t.Fatalf("expected out: %v, got: %v", tc.out, out) + } + }) + } +} + +func TestCustomBinder(t *testing.T) { + tt := []struct { + in string + keyvals []interface{} + binder *Binder + out string + err error + }{ + { + in: ":price: - (:price: * :discount-percentage:)", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + }, + binder: &Binder{Ident: &Ident{ + Prefix: ":", Suffix: ":", + }}, + out: "100 - (100 * 0.1)", + }, + { + in: ":price - (:price * :discount-percentage)", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + }, + binder: &Binder{Ident: &Ident{ + Prefix: ":", Suffix: "", + }}, + out: "100 - (100 * 0.1)", + }, + { + in: "{price} - ({price} * {discount-percentage})", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + }, + binder: &Binder{}, + out: "100 - (100 * 0.1)", + }, + { + in: "{price} - ({price} * {discount-percentage})", + keyvals: []interface{}{ + "price", 100, + "discount-percentage", 0.1, + }, + binder: &Binder{Ident: &Ident{Prefix: ""}}, + err: ErrEmptyPrefix, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.in, func(t *testing.T) { + out, err := tc.binder.Bind(tc.in, tc.keyvals...) + if !errors.Is(err, tc.err) { + t.Fatalf("expected error: %v, got: %v", tc.err, err) + } + if out != tc.out { + t.Fatalf("expected out: %v, got: %v", tc.out, out) + } + }) + } +} + +func TestSetIdent(t *testing.T) { + ident := &Ident{Prefix: ":", Suffix: ""} + stdIdent := std.Ident + SetIdent(ident) + if std.Ident != ident { + t.Fatalf("expected: %v, got: %v", ident, std.Ident) + } + std.Ident = stdIdent +} + +func TestSetFormatter(t *testing.T) { + var formatter Formatter = func(v interface{}) string { + return fmt.Sprintf("%v", v) + } + + stdFormatter := std.Formatter + SetFormatter(formatter) + if fmt.Sprintf("%p", std.Formatter) != fmt.Sprintf("%p", formatter) { + t.Fatalf("expected: %v, got: %v", formatter, std.Formatter) + } + std.Formatter = stdFormatter +} + +func TestFormat(t *testing.T) { + type TestStruct struct{ Field string } + var emptyErr error + + tt := []struct { + in interface{} + out string + }{ + {in: 12, out: "12"}, + {in: int64(12), out: "12"}, + {in: int32(12), out: "12"}, + {in: float64(12.3), out: "12.3"}, + {in: complex(2, -9), out: "(2-9i)"}, + {in: true, out: "true"}, + {in: "expr", out: "\"expr\""}, + {in: time.Time{}, out: "\"0001-01-01 00:00:00 +0000 UTC\""}, // test fmt.Stringer + {in: "expr", out: "\"expr\""}, + {in: struct{}{}, out: "\"{}\""}, + {in: emptyErr, out: "\"\""}, + {in: fmt.Errorf("error something"), out: "\"error something\""}, + {in: []byte("c"), out: "\"[99]\""}, + {in: []string{"abc", "def"}, out: "\"[abc def]\""}, + {in: TestStruct{}, out: "\"{}\""}, + {in: TestStruct{Field: "value"}, out: "\"{value}\""}, + {in: &TestStruct{}, out: "\"&{}\""}, + {in: &TestStruct{Field: "value"}, out: "\"&{value}\""}, + } + + defaultFormat := DefaultFormater() + for _, tc := range tt { + tc := tc + t.Run(fmt.Sprintf("%T: %v", tc.in, tc.in), func(t *testing.T) { + s := defaultFormat(tc.in) + if s != tc.out { + t.Fatalf("expected string value: %s, got: %s", tc.out, s) + } + }) + } +} + +func TestSyntaxError(t *testing.T) { + err := &SyntaxError{ + Msg: "test", + Begin: 0, + End: 5, + Value: "value", + Err: ErrEmptyPrefix, + } + expectedErrorString := "test [value:\"value\",beg:0,end:5]: " + ErrEmptyPrefix.Error() + if err.Error() != expectedErrorString { + t.Fatalf("expected err string: %s, got: %s", expectedErrorString, err.Error()) + } +} diff --git a/boolean/boolean.go b/boolean/boolean.go deleted file mode 100644 index d915f4e..0000000 --- a/boolean/boolean.go +++ /dev/null @@ -1,238 +0,0 @@ -// Deprecated: This package is no longer maintained and might be deleted in the future, use expr.Visitor instead. -package boolean - -import ( - "errors" - "fmt" - "go/ast" - "go/token" - "strconv" - "strings" -) - -// ErrUnsupportedOperator is error unsupported operator -var ErrUnsupportedOperator = errors.New("unsupported operator") - -// ErrInvalidOperationOnFloat is error invalid operation on float -var ErrInvalidOperationOnFloat = errors.New("invalid operation on float") - -// ErrIntegerDividedByZero occurs when x/y and y equals to 0, Go does not allow integer to be divided by zero -var ErrIntegerDividedByZero = errors.New("integer divided by zero") - -// Visitor is boolean visitor interface -// -// Deprecated: use expr.Visitor instead. -type Visitor interface { - Visit(node ast.Node) ast.Visitor - Result() (bool, error) -} - -// NewVisitor creates new boolean visitor -// -// Deprecated: use expr.NewVisitor() instead. -func NewVisitor() Visitor { - return &visitor{} -} - -type visitor struct { - kind token.Token - res string - err error -} - -func (v *visitor) visitUnary(unaryExpr *ast.UnaryExpr) ast.Visitor { - switch unaryExpr.Op { - case token.NOT, token.ADD, token.SUB: - xVisitor := &visitor{} - ast.Walk(xVisitor, unaryExpr.X) - if xVisitor.err != nil { - v.err = xVisitor.err - return nil - } - switch unaryExpr.Op { - case token.NOT: - res, _ := strconv.ParseBool(xVisitor.res) - v.res = strconv.FormatBool(!res) - case token.ADD: - v.res, v.kind = xVisitor.res, xVisitor.kind - case token.SUB: - if strings.HasPrefix(xVisitor.res, "-") { - v.res, v.kind = strings.TrimPrefix(xVisitor.res, "-"), xVisitor.kind - return nil - } - v.res, v.kind = "-"+xVisitor.res, xVisitor.kind - } - default: - v.err = ErrUnsupportedOperator - } - return nil -} - -func (v *visitor) arithmetic(xVisitor, yVisitor *visitor, op token.Token) { - if xVisitor.kind == token.FLOAT || yVisitor.kind == token.FLOAT { - x, _ := strconv.ParseFloat(xVisitor.res, 64) - y, _ := strconv.ParseFloat(yVisitor.res, 64) - switch op { - case token.ADD: - v.res, v.kind = fmt.Sprintf("%f", x+y), token.FLOAT - case token.SUB: - v.res, v.kind = fmt.Sprintf("%f", x-y), token.FLOAT - case token.MUL: - v.res, v.kind = fmt.Sprintf("%f", x*y), token.FLOAT - case token.QUO: - v.res, v.kind = fmt.Sprintf("%f", x/y), token.FLOAT - case token.REM: - v.err = ErrInvalidOperationOnFloat - } - return - } - - x, _ := strconv.Atoi(xVisitor.res) - y, _ := strconv.Atoi(yVisitor.res) - switch op { - case token.ADD: - v.res, v.kind = fmt.Sprintf("%d", x+y), token.INT - case token.SUB: - v.res, v.kind = fmt.Sprintf("%d", x-y), token.INT - case token.MUL: - v.res, v.kind = fmt.Sprintf("%d", x*y), token.INT - case token.QUO: - if y == 0 { - v.err = ErrIntegerDividedByZero - return - } - v.res, v.kind = fmt.Sprintf("%d", x/y), token.INT - case token.REM: - v.res, v.kind = fmt.Sprintf("%d", x%y), token.INT - } -} - -func (v *visitor) comparison(xVisitor, yVisitor *visitor, op token.Token) { - switch op { - case token.EQL: - v.res = strconv.FormatBool(xVisitor.res == yVisitor.res) - return - case token.NEQ: - v.res = strconv.FormatBool(xVisitor.res != yVisitor.res) - return - } - - if xVisitor.kind == token.STRING || yVisitor.kind == token.STRING { - switch op { - case token.GTR: - v.res = strconv.FormatBool(xVisitor.res > yVisitor.res) - case token.GEQ: - v.res = strconv.FormatBool(xVisitor.res >= yVisitor.res) - case token.LSS: - v.res = strconv.FormatBool(xVisitor.res < yVisitor.res) - case token.LEQ: - v.res = strconv.FormatBool(xVisitor.res <= yVisitor.res) - } - return - } - - if xVisitor.kind == token.FLOAT || yVisitor.kind == token.FLOAT { - x, _ := strconv.ParseFloat(xVisitor.res, 64) - y, _ := strconv.ParseFloat(yVisitor.res, 64) - switch op { - case token.GTR: - v.res = strconv.FormatBool(x > y) - case token.GEQ: - v.res = strconv.FormatBool(x >= y) - case token.LSS: - v.res = strconv.FormatBool(x < y) - case token.LEQ: - v.res = strconv.FormatBool(x <= y) - } - return - } - - x, _ := strconv.Atoi(xVisitor.res) - y, _ := strconv.Atoi(yVisitor.res) - switch op { - case token.GTR: - v.res = strconv.FormatBool(x > y) - case token.GEQ: - v.res = strconv.FormatBool(x >= y) - case token.LSS: - v.res = strconv.FormatBool(x < y) - case token.LEQ: - v.res = strconv.FormatBool(x <= y) - } -} - -func (v *visitor) logical(xVisitor, yVisitor *visitor, op token.Token) { - var x, y bool - x, v.err = strconv.ParseBool(xVisitor.res) - if v.err != nil { - return - } - y, v.err = strconv.ParseBool(yVisitor.res) - if v.err != nil { - return - } - if op == token.LAND { - v.res = strconv.FormatBool(x && y) - return - } - v.res = strconv.FormatBool(x || y) // token.LOR -} - -func (v *visitor) visitBinary(binaryExpr *ast.BinaryExpr) ast.Visitor { - xVisitor := &visitor{} - ast.Walk(xVisitor, binaryExpr.X) - if xVisitor.err != nil { - v.err = xVisitor.err - return nil - } - yVisitor := &visitor{} - ast.Walk(yVisitor, binaryExpr.Y) - if yVisitor.err != nil { - v.err = yVisitor.err - return nil - } - - switch binaryExpr.Op { - case token.EQL, token.NEQ, token.GTR, token.GEQ, token.LSS, token.LEQ: - v.comparison(xVisitor, yVisitor, binaryExpr.Op) - case token.ADD, token.SUB, token.MUL, token.QUO, token.REM: - v.arithmetic(xVisitor, yVisitor, binaryExpr.Op) - case token.LAND, token.LOR: - v.logical(xVisitor, yVisitor, binaryExpr.Op) - default: - v.err = ErrUnsupportedOperator - } - return nil -} - -func (v *visitor) Visit(node ast.Node) ast.Visitor { - if node == nil || v.err != nil { - return nil - } - - switch d := node.(type) { - case *ast.ParenExpr: - return v.Visit(d.X) - case *ast.UnaryExpr: - return v.visitUnary(d) - case *ast.BinaryExpr: - return v.visitBinary(d) - case *ast.BasicLit: - v.res = d.Value - v.kind = d.Kind - return nil - case *ast.Ident: - v.res = d.String() - v.kind = token.STRING - return nil - } - - return v -} - -func (v *visitor) Result() (bool, error) { - if v.err != nil { - return false, v.err - } - return strconv.ParseBool(v.res) -} diff --git a/boolean/boolean_test.go b/boolean/boolean_test.go deleted file mode 100644 index a264bf2..0000000 --- a/boolean/boolean_test.go +++ /dev/null @@ -1,621 +0,0 @@ -package boolean - -import ( - "errors" - "go/ast" - "go/token" - "testing" -) - -func TestVisitUnary(t *testing.T) { - tt := []struct { - name string - unaryExpr *ast.UnaryExpr - expectedResult string - expectedErr error - }{ - { - name: "!true", - unaryExpr: &ast.UnaryExpr{ - Op: token.NOT, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "true", - }, - }, - expectedResult: "false", - }, - { - name: "+2", - unaryExpr: &ast.UnaryExpr{ - Op: token.ADD, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedResult: "2", - }, - { - name: "-2", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedResult: "-2", - }, - { - name: "-(-2)", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.ParenExpr{ - X: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - }, - }, - expectedResult: "2", - }, - { - name: "*2", - unaryExpr: &ast.UnaryExpr{ - Op: token.MUL, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "-(*2)", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.ParenExpr{ - X: &ast.UnaryExpr{ - Op: token.MUL, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - }, - }, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := visitor{} - _ = v.visitUnary(tc.unaryExpr) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, v.err) - } - if v.res != tc.expectedResult { - t.Fatalf("expected result: %s, got: %s", tc.expectedResult, v.res) - } - }) - } -} - -func TestArithmetic(t *testing.T) { - tt := []struct { - name string - x *visitor - y *visitor - op token.Token - expectedResult string - expectedErr error - }{ - { - name: "float, 8.5 + 1.5 = 10", - x: &visitor{res: "8.5", kind: token.FLOAT}, - y: &visitor{res: "1.5", kind: token.FLOAT}, - op: token.ADD, - expectedResult: "10.000000", - }, - { - name: "float, 8.5 - 1.5 = 7", - x: &visitor{res: "8.5", kind: token.FLOAT}, - y: &visitor{res: "1.5", kind: token.FLOAT}, - op: token.SUB, - expectedResult: "7.000000", - }, - { - name: "float, 8.5 * 1.5 = 12.75", - x: &visitor{res: "8.5", kind: token.FLOAT}, - y: &visitor{res: "1.5", kind: token.FLOAT}, - op: token.MUL, - expectedResult: "12.750000", - }, - { - name: "float, 8.5 / 1.5 = 5.666667", - x: &visitor{res: "8.5", kind: token.FLOAT}, - y: &visitor{res: "1.5", kind: token.FLOAT}, - op: token.QUO, - expectedResult: "5.666667", - }, - { - name: "float, 8.5 % 1.5 = ErrInvalidOperationOnFloat", - x: &visitor{res: "8.5", kind: token.FLOAT}, - y: &visitor{res: "1.5", kind: token.FLOAT}, - op: token.REM, - expectedErr: ErrInvalidOperationOnFloat, - }, - { - name: "int, 8 + 1 = 9", - x: &visitor{res: "8", kind: token.INT}, - y: &visitor{res: "1", kind: token.INT}, - op: token.ADD, - expectedResult: "9", - }, - { - name: "int, 8 - 1 = 7", - x: &visitor{res: "8", kind: token.INT}, - y: &visitor{res: "1", kind: token.INT}, - op: token.SUB, - expectedResult: "7", - }, - { - name: "int, 8 * 2 = 16", - x: &visitor{res: "8", kind: token.INT}, - y: &visitor{res: "2", kind: token.INT}, - op: token.MUL, - expectedResult: "16", - }, - { - name: "int, 8 / 2 = 4", - x: &visitor{res: "8", kind: token.INT}, - y: &visitor{res: "2", kind: token.INT}, - op: token.QUO, - expectedResult: "4", - }, - { - name: "int, 8 / 0 = ErrIntegerDividedByZero", - x: &visitor{res: "8", kind: token.INT}, - y: &visitor{res: "0", kind: token.INT}, - op: token.QUO, - expectedErr: ErrIntegerDividedByZero, - }, - { - name: "int, 8 % 3 = 2", - x: &visitor{res: "8", kind: token.INT}, - y: &visitor{res: "3", kind: token.INT}, - op: token.REM, - expectedResult: "2", - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := &visitor{} - v.arithmetic(tc.x, tc.y, tc.op) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, v.err) - } - if v.res != tc.expectedResult { - t.Fatalf("expected result: %s, got: %s", tc.expectedResult, v.res) - } - }) - } -} - -func TestComparison(t *testing.T) { - tt := []struct { - name string - x *visitor - y *visitor - op token.Token - expectedResult string - }{ - { - name: "aaaa == aaaa = true", - x: &visitor{res: "a", kind: token.STRING}, - y: &visitor{res: "a", kind: token.STRING}, - op: token.EQL, - expectedResult: "true", - }, - { - name: "aaaa != aaaa = false", - x: &visitor{res: "a", kind: token.STRING}, - y: &visitor{res: "a", kind: token.STRING}, - op: token.NEQ, - expectedResult: "false", - }, - { - name: "b > a = true", - x: &visitor{res: "b", kind: token.STRING}, - y: &visitor{res: "a", kind: token.STRING}, - op: token.GTR, - expectedResult: "true", - }, - { - name: "b >= a = true", - x: &visitor{res: "b", kind: token.STRING}, - y: &visitor{res: "a", kind: token.STRING}, - op: token.GEQ, - expectedResult: "true", - }, - { - name: "a < b = true", - x: &visitor{res: "a", kind: token.STRING}, - y: &visitor{res: "b", kind: token.STRING}, - op: token.LSS, - expectedResult: "true", - }, - { - name: "a <= b = true", - x: &visitor{res: "a", kind: token.STRING}, - y: &visitor{res: "b", kind: token.STRING}, - op: token.LEQ, - expectedResult: "true", - }, - { - name: "10.567 > 10.234 = true", - x: &visitor{res: "10.567", kind: token.FLOAT}, - y: &visitor{res: "10.234", kind: token.FLOAT}, - op: token.GTR, - expectedResult: "true", - }, - { - name: "10.567 >= 10.234 = true", - x: &visitor{res: "10.567", kind: token.FLOAT}, - y: &visitor{res: "10.234", kind: token.FLOAT}, - op: token.GEQ, - expectedResult: "true", - }, - { - name: "10 >= 10 = true", - x: &visitor{res: "10", kind: token.FLOAT}, - y: &visitor{res: "10", kind: token.FLOAT}, - op: token.GEQ, - expectedResult: "true", - }, - { - name: "10.234 < 10.567 = true", - x: &visitor{res: "10.234", kind: token.FLOAT}, - y: &visitor{res: "10.567", kind: token.FLOAT}, - op: token.LSS, - expectedResult: "true", - }, - { - name: "10.234 <= 10.567 = true", - x: &visitor{res: "10.234", kind: token.FLOAT}, - y: &visitor{res: "10.567", kind: token.FLOAT}, - op: token.LEQ, - expectedResult: "true", - }, - { - name: "10 <= 10 = true", - x: &visitor{res: "10", kind: token.FLOAT}, - y: &visitor{res: "10", kind: token.FLOAT}, - op: token.LEQ, - expectedResult: "true", - }, - { - name: "10 > 9 = true", - x: &visitor{res: "10", kind: token.INT}, - y: &visitor{res: "9", kind: token.INT}, - op: token.GTR, - expectedResult: "true", - }, - { - name: "10 >= 9 = true", - x: &visitor{res: "10", kind: token.INT}, - y: &visitor{res: "9", kind: token.INT}, - op: token.GEQ, - expectedResult: "true", - }, - { - name: "10 >= 10 = true", - x: &visitor{res: "10", kind: token.INT}, - y: &visitor{res: "10", kind: token.INT}, - op: token.GEQ, - expectedResult: "true", - }, - { - name: "9 < 10 = true", - x: &visitor{res: "9", kind: token.INT}, - y: &visitor{res: "10", kind: token.INT}, - op: token.LSS, - expectedResult: "true", - }, - { - name: "9 <= 10 = true", - x: &visitor{res: "9", kind: token.INT}, - y: &visitor{res: "10", kind: token.INT}, - op: token.LEQ, - expectedResult: "true", - }, - { - name: "10 <= 10 = true", - x: &visitor{res: "10", kind: token.INT}, - y: &visitor{res: "10", kind: token.INT}, - op: token.LEQ, - expectedResult: "true", - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := &visitor{} - v.comparison(tc.x, tc.y, tc.op) - if v.res != tc.expectedResult { - t.Fatalf("expected result: %s, got: %s", tc.expectedResult, v.res) - } - }) - } -} - -func TestLogical(t *testing.T) { - tt := []struct { - name string - x *visitor - y *visitor - op token.Token - expectedResult string - }{ - { - name: "true && true = true", - x: &visitor{res: "true"}, - y: &visitor{res: "true"}, - op: token.LAND, - expectedResult: "true", - }, - { - name: "true && false = false", - x: &visitor{res: "true"}, - y: &visitor{res: "false"}, - op: token.LAND, - expectedResult: "false", - }, - { - name: "false && false = false", - x: &visitor{res: "false"}, - y: &visitor{res: "false"}, - op: token.LAND, - expectedResult: "false", - }, - { - name: "true || true = true", - x: &visitor{res: "true"}, - y: &visitor{res: "true"}, - op: token.LOR, - expectedResult: "true", - }, - { - name: "true || false = true", - x: &visitor{res: "true"}, - y: &visitor{res: "false"}, - op: token.LOR, - expectedResult: "true", - }, - { - name: "false || false = false", - x: &visitor{res: "false"}, - y: &visitor{res: "false"}, - op: token.LOR, - expectedResult: "false", - }, - { - name: "true && 10 = ?", - x: &visitor{res: "true"}, - y: &visitor{res: "10"}, - op: token.LAND, - expectedResult: "", - }, - { - name: "10 || true = ?", - x: &visitor{res: "10"}, - y: &visitor{res: "true"}, - op: token.LOR, - expectedResult: "", - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := &visitor{} - v.logical(tc.x, tc.y, tc.op) - if v.res != tc.expectedResult { - t.Fatalf("expected result: %s, got: %s", tc.expectedResult, v.res) - } - }) - } -} - -func TestVisitBinary(t *testing.T) { - tt := []struct { - name string - x ast.Expr - y ast.Expr - ops []token.Token - expectedResults []string - expectedErrs []error - }{ - { - name: "comparison", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - ops: []token.Token{token.EQL, token.NEQ, token.GTR, token.GEQ, token.LSS, token.LEQ}, - expectedResults: []string{"false", "true", "true", "true", "false", "false"}, - }, - { - name: "arithmetic", - x: &ast.BasicLit{Value: "45", Kind: token.INT}, - y: &ast.BasicLit{Value: "5", Kind: token.INT}, - ops: []token.Token{token.ADD, token.SUB, token.MUL, token.QUO, token.REM}, - expectedResults: []string{"50", "40", "225", "9", "0"}, - }, - { - name: "logical", - x: &ast.BasicLit{Value: "true"}, - y: &ast.BasicLit{Value: "false"}, - ops: []token.Token{token.LAND, token.LOR}, - expectedResults: []string{"false", "true"}, - }, - { - name: "ErrUnsupportedOperator", - x: &ast.BasicLit{Value: "true"}, - y: &ast.BasicLit{Value: "false"}, - ops: []token.Token{token.ASSIGN}, - expectedResults: []string{""}, - expectedErrs: []error{ErrUnsupportedOperator}, - }, - { - name: "err on x: ErrUnsupportedOperator", - x: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "true"}, - Y: &ast.BasicLit{Value: "false"}, - Op: token.ASSIGN, - }, - y: &ast.BasicLit{Value: "false"}, - ops: []token.Token{token.ASSIGN}, - expectedResults: []string{""}, - expectedErrs: []error{ErrUnsupportedOperator}, - }, - { - name: "err on Y: ErrUnsupportedOperator", - x: &ast.BasicLit{Value: "false"}, - y: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "true"}, - Y: &ast.BasicLit{Value: "false"}, - Op: token.ASSIGN, - }, - ops: []token.Token{token.ASSIGN}, - expectedResults: []string{""}, - expectedErrs: []error{ErrUnsupportedOperator}, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := &visitor{} - for i, op := range tc.ops { - var ( - op = op - expectedResult = tc.expectedResults[i] - expectedErr error - ) - - if tc.expectedErrs != nil { - expectedErr = tc.expectedErrs[i] - } - - t.Run(op.String(), func(t *testing.T) { - binaryExpr := &ast.BinaryExpr{X: tc.x, Y: tc.y, Op: op} - _ = v.visitBinary(binaryExpr) - if !errors.Is(v.err, expectedErr) { - t.Fatalf("expected error: %v, got: %v", expectedErr, v.err) - } - if v.res != expectedResult { - t.Fatalf("expected result: %s, got: %s", expectedResult, v.res) - } - }) - } - }) - } -} - -func TestVisit(t *testing.T) { - tt := []struct { - name string - node ast.Node - expectedResult string - }{ - { - name: "*ast.ParenExpr, (1) = 1", - node: &ast.ParenExpr{ - X: &ast.BasicLit{Value: "1", Kind: token.INT}, - }, - expectedResult: "1", - }, - { - name: "*ast.UnaryExpr, !true = false", - node: &ast.UnaryExpr{ - X: &ast.BasicLit{Value: "true", Kind: token.STRING}, - Op: token.NOT, - }, - expectedResult: "false", - }, - { - name: "*ast.BinaryExpr, 1 + 2 = 3", - node: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "1", Kind: token.INT}, - Y: &ast.BasicLit{Value: "2", Kind: token.INT}, - Op: token.ADD, - }, - expectedResult: "3", - }, - { - name: "*ast.BasicLit, 1", - node: &ast.BasicLit{Value: "1", Kind: token.INT}, - expectedResult: "1", - }, - { - name: "*ast.Ident, true", - node: &ast.Ident{Name: "true"}, - expectedResult: "true", - }, - { - name: "not supported, e.g. *ast.ArrayType", - node: &ast.ArrayType{}, - expectedResult: "", - }, - { - name: "nil node", - node: nil, - expectedResult: "", - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := NewVisitor().(*visitor) - _ = v.Visit(tc.node) - if v.res != tc.expectedResult { - t.Fatalf("expected result: %s, got: %s", tc.expectedResult, v.res) - } - }) - } -} - -func TestResult(t *testing.T) { - tt := []struct { - name string - v *visitor - expectedResult bool - expectedErr error - }{ - {name: "true", v: &visitor{res: "true"}, expectedResult: true}, - {name: "false", v: &visitor{res: "false"}, expectedResult: false}, - {name: "false", v: &visitor{err: ErrIntegerDividedByZero}, expectedErr: ErrIntegerDividedByZero}, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - res, err := tc.v.Result() - if !errors.Is(err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, err) - } - if res != tc.expectedResult { - t.Fatalf("expected result: %v, got: %v", tc.expectedErr, err) - } - }) - } -} diff --git a/errors.go b/errors.go index 15ed390..8ab54d4 100644 --- a/errors.go +++ b/errors.go @@ -10,8 +10,6 @@ var ( ErrUnsupportedOperator = errors.New("unsupported operator") // ErrUnaryOperation occurs when unary operation failed ErrUnaryOperation = errors.New("unary operation") - // ErrUnsupportedBasicLit occurs when basic lit is not supported - ErrUnsupportedBasicLit = errors.New("unsupported basic type literal") // ErrArithmeticOperation occurs when either x or y is not int or float ErrArithmeticOperation = errors.New("arithmetic operation") // ErrIntegerDividedByZero occurs when x/y and y equals to 0 and AllowIntDivByZero == false (default). diff --git a/explain/README.md b/explain/README.md new file mode 100644 index 0000000..46ac135 --- /dev/null +++ b/explain/README.md @@ -0,0 +1,23 @@ +# Explain + +Explain is a standalone package aimed to explain step by step operation in expr. + +```go + s := "1 + 2 + 3" + steps, err := explain.Explain(s) + if err != nil { + panic(err) + } + + fmt.Printf("%#v\n", steps) + /* + []explain.Step{ + {[]string{"1 + 2"}, "3"}, + {[]string{"(1 + 2) + 3", "3 + 3"}, "6"}, + } + */ + + // explanation: + // 1 + 2 -> 3 + // (1 + 2) + 3 -> (3 + 3) -> 6 +``` \ No newline at end of file diff --git a/explain/explain.go b/explain/explain.go new file mode 100644 index 0000000..6258db9 --- /dev/null +++ b/explain/explain.go @@ -0,0 +1,42 @@ +// explain is a standalone package aimed to explain step by step operation in expr. +package explain + +import ( + "go/ast" + "go/parser" +) + +// Step contains smaller operations of given s with its result. +// One step can have multiple equivalent forms, starts with original form until the final form. +type Step struct { + EquivalentForms []string + Result string +} + +// Explains explains step-by-step process of evaluating s. +func Explain(s string) ([]Step, error) { + e, err := parser.ParseExpr(s) + if err != nil { + return nil, err + } + + v := &Visitor{} + ast.Walk(v, e) + if err := v.err; err != nil { + return nil, err + } + + // sanitize results + explains := make([]Step, len(v.transforms)) + for i, tranform := range v.transforms { + explains[i] = Step{ + EquivalentForms: []string{tranform.Segmented}, + Result: tranform.Evaluated, + } + if tranform.Segmented != tranform.EquivalentForm { + explains[i].EquivalentForms = append(explains[i].EquivalentForms, tranform.EquivalentForm) + } + } + + return explains, nil +} diff --git a/explain/explain_test.go b/explain/explain_test.go new file mode 100644 index 0000000..5aec052 --- /dev/null +++ b/explain/explain_test.go @@ -0,0 +1,68 @@ +package explain_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/muktihari/expr" + "github.com/muktihari/expr/explain" +) + +func TestExplain(t *testing.T) { + tt := []struct { + str string + explains []explain.Step + err error + }{ + { + str: "1 + 2", + explains: []explain.Step{ + {[]string{"1 + 2"}, "3"}, + }, + }, + { + str: "1 + 2 + 3", + explains: []explain.Step{ + {[]string{"1 + 2"}, "3"}, + {[]string{"(1 + 2) + 3", "3 + 3"}, "6"}, + }, + }, + { + str: "!true || ((5 > 3) && 1 == 1)", + explains: []explain.Step{ + {[]string{"!true"}, "false"}, + {[]string{"5 > 3"}, "true"}, + {[]string{"1 == 1"}, "true"}, + {[]string{"(5 > 3) && (1 == 1)", "true && true"}, "true"}, + {[]string{"false || (true && true)", "false || true"}, "true"}, + }, + }, + { + str: "!(true) && !7", + err: expr.ErrUnaryOperation, + }, + } + + t.Run("parser error", func(t *testing.T) { + _, err := explain.Explain("1 +") + if err == nil { + t.Fatalf("expected error, got: %v", err) + } + }) + + for _, tc := range tt { + tc := tc + t.Run(tc.str, func(t *testing.T) { + explains, err := explain.Explain(tc.str) + if !errors.Is(err, tc.err) { + t.Fatalf("expected err: %v, got: %v", tc.err, err) + } + + if diff := cmp.Diff(explains, tc.explains); diff != "" { + t.Fatal(diff) + } + }) + } + +} diff --git a/explain/visitor.go b/explain/visitor.go new file mode 100644 index 0000000..c198f2f --- /dev/null +++ b/explain/visitor.go @@ -0,0 +1,151 @@ +package explain + +import ( + "fmt" + "go/ast" + + "github.com/muktihari/expr" + "github.com/muktihari/expr/conv" +) + +type exprType int + +const ( + illegalExpr exprType = iota + parentExpr + unaryExpr + binaryExpr + basicLit + ident +) + +// Transform holds transformation of operations result. +type Transform struct { + Segmented string + EquivalentForm string + Evaluated string +} + +// Visitor satisfies ast.Visitor and it will evaluate given expression in step-by-step transformation values. +type Visitor struct { + transforms []Transform + depth int + exprType exprType + value string + err error +} + +var _ ast.Visitor = &Visitor{} + +// Value return []Transform +func (v *Visitor) Value() []Transform { return v.transforms } + +// Err returns visitor's error +func (v *Visitor) Err() error { return v.err } + +func (v *Visitor) Visit(node ast.Node) ast.Visitor { + if node == nil || v.err != nil { + return nil + } + + switch d := node.(type) { + case *ast.ParenExpr: + v.exprType = parentExpr + + vx := &Visitor{depth: v.depth + 1} + ast.Walk(vx, d.X) + if vx.err != nil { + v.err = vx.err + return nil + } + + v.value = "(" + vx.value + ")" + v.transforms = append(v.transforms, vx.transforms...) + case *ast.UnaryExpr: + v.exprType = unaryExpr + + vx := &Visitor{depth: v.depth + 1} + ast.Walk(vx, d.X) + if vx.err != nil { + v.err = vx.err + return nil + } + v.transforms = append(v.transforms, vx.transforms...) + + ev := newExprVisitor() + ast.Walk(ev, d) + if err := ev.Err(); err != nil { + v.err = err + return nil + } + + rv := conv.FormatExpr(d) + v.value = ev.Value() + + v.transforms = append(v.transforms, Transform{ + Segmented: rv, + EquivalentForm: rv, + Evaluated: ev.Value(), + }) + case *ast.BinaryExpr: + v.exprType = binaryExpr + + vx := &Visitor{depth: v.depth + 1} + ast.Walk(vx, d.X) + if vx.err != nil { + v.err = vx.err + return nil + } + v.transforms = append(v.transforms, vx.transforms...) + + vy := &Visitor{depth: v.depth + 1} + ast.Walk(vy, d.Y) + if vy.err != nil { + v.err = vy.err + return nil + } + v.transforms = append(v.transforms, vy.transforms...) + + if vx.exprType == binaryExpr { + vx.value = "(" + conv.FormatExpr(d.X) + ")" + } + if vy.exprType == binaryExpr { + vy.value = "(" + conv.FormatExpr(d.Y) + ")" + } + + // evaluated values + ev := newExprVisitor() + ast.Walk(ev, d) + if err := ev.Err(); err != nil { + v.err = err + return nil + } + + evx := newExprVisitor() + ast.Walk(evx, d.X) + + evy := newExprVisitor() + ast.Walk(evy, d.Y) + + v.value = fmt.Sprintf("%s %s %s", evx.Value(), d.Op, evy.Value()) + + v.transforms = append(v.transforms, Transform{ + Segmented: fmt.Sprintf("%s %s %s", vx.value, d.Op, vy.value), + EquivalentForm: v.value, + Evaluated: ev.Value(), + }) + case *ast.BasicLit: + v.value, v.exprType = d.Value, basicLit + case *ast.Ident: + v.value, v.exprType = d.Name, ident + } + + return nil +} + +func newExprVisitor() *expr.Visitor { + return expr.NewVisitor( + expr.WithNumericType(expr.NumericTypeAuto), + expr.WithAllowIntegerDividedByZero(true), + ) +} diff --git a/explain/visitor_test.go b/explain/visitor_test.go new file mode 100644 index 0000000..1604757 --- /dev/null +++ b/explain/visitor_test.go @@ -0,0 +1,116 @@ +package explain + +import ( + "errors" + "go/ast" + "go/parser" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/muktihari/expr" +) + +func TestVisit(t *testing.T) { + tt := []struct { + str string + transforms []Transform + err error + }{ + { + str: "((1 * 2) + 5) * (3 * 4)", + transforms: []Transform{ + {Segmented: "1 * 2", EquivalentForm: "1 * 2", Evaluated: "2"}, + {Segmented: "(1 * 2) + 5", EquivalentForm: "2 + 5", Evaluated: "7"}, + {Segmented: "3 * 4", EquivalentForm: "3 * 4", Evaluated: "12"}, + {Segmented: "(2 + 5) * (3 * 4)", EquivalentForm: "7 * 12", Evaluated: "84"}, + }, + }, + { + str: "(1 * 2) * (3 * 4)", + transforms: []Transform{ + {Segmented: "1 * 2", EquivalentForm: "1 * 2", Evaluated: "2"}, + {Segmented: "3 * 4", EquivalentForm: "3 * 4", Evaluated: "12"}, + {Segmented: "(1 * 2) * (3 * 4)", EquivalentForm: "2 * 12", Evaluated: "24"}, + }, + }, + { + str: "2 + 1 + 2 * 3 + 3", + transforms: []Transform{ + {Segmented: "2 + 1", EquivalentForm: "2 + 1", Evaluated: "3"}, + {Segmented: "2 * 3", EquivalentForm: "2 * 3", Evaluated: "6"}, + {Segmented: "(2 + 1) + (2 * 3)", EquivalentForm: "3 + 6", Evaluated: "9"}, + {Segmented: "(2 + 1 + 2 * 3) + 3", EquivalentForm: "9 + 3", Evaluated: "12"}, + }, + }, + { + str: "1 * 1 > 1 + 2", + transforms: []Transform{ + {Segmented: "1 * 1", EquivalentForm: "1 * 1", Evaluated: "1"}, + {Segmented: "1 + 2", EquivalentForm: "1 + 2", Evaluated: "3"}, + {Segmented: "(1 * 1) > (1 + 2)", EquivalentForm: "1 > 3", Evaluated: "false"}, + }, + }, + { + str: "(1+2)", + transforms: []Transform{ + {Segmented: "1 + 2", EquivalentForm: "1 + 2", Evaluated: "3"}, + }, + }, + { + str: "!true || ((5 > 3) && 1==1)", + transforms: []Transform{ + {Segmented: "!true", EquivalentForm: "!true", Evaluated: "false"}, + {Segmented: "5 > 3", EquivalentForm: "5 > 3", Evaluated: "true"}, + {Segmented: "1 == 1", EquivalentForm: "1 == 1", Evaluated: "true"}, + { + Segmented: "(5 > 3) && (1==1)", + EquivalentForm: "true && true", + Evaluated: "true", + }, + { + Segmented: "false || (true && true)", + EquivalentForm: "false || true", + Evaluated: "true", + }, + }, + }, + { + str: "true && 1 == 2 && ((!7))", + err: expr.ErrUnaryOperation, + }, + { + str: "!(!9) && (!(7))", + err: expr.ErrUnaryOperation, + }, + { + str: "1.2 & 1", + err: expr.ErrBitwiseOperation, + }, + } + + // test nil node + t.Run("nil node", func(t *testing.T) { ast.Walk((&Visitor{}), nil) }) + + for _, tc := range tt { + tc := tc + t.Run(tc.str, func(t *testing.T) { + expr, err := parser.ParseExpr(tc.str) + if err != nil { + t.Fatal(err) + } + v := &Visitor{} + ast.Walk(v, expr) + if !errors.Is(v.Err(), tc.err) { + t.Fatalf("expected err: %v, got: %v", tc.err, v.err) + } + if v.err != nil { + return + } + + if diff := cmp.Diff(v.Value(), tc.transforms); diff != "" { + t.Fatal(diff) + } + }) + } + +} diff --git a/expr_benchmark_test.go b/expr_benchmark_test.go index d29aa85..6eebebd 100644 --- a/expr_benchmark_test.go +++ b/expr_benchmark_test.go @@ -58,25 +58,23 @@ func BenchmarkAny(b *testing.B) { } func BenchmarkBoolean(b *testing.B) { - for i, e := range exprs { - i, e := i, e - kind := expr.Kind(i) - if kind != expr.KindBoolean { - continue - } + var ( + e string = exprs[expr.KindBoolean] + r bool = true + ) - b.Run(kind.String(), func(b *testing.B) { - for i := 0; i < b.N; i++ { - v, err := expr.Bool(e) - if err != nil { - b.Fatalf("expected nil, got: %v", err) - } - if v != true { - b.Fatalf("expected %t, got: %t", true, v) - } + b.Run(expr.KindBoolean.String(), func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, err := expr.Bool(e) + if err != nil { + b.Fatalf("expected nil, got: %v", err) } - }) - } + if v != r { + b.Fatalf("expected %t, got: %t", true, v) + } + } + }) + } func BenchmarkComplex128(b *testing.B) { diff --git a/float/float.go b/float/float.go deleted file mode 100644 index f30a28c..0000000 --- a/float/float.go +++ /dev/null @@ -1,124 +0,0 @@ -// Deprecated: This package is no longer maintained and might be deleted in the future, use expr.Visitor instead. -package float - -import ( - "errors" - "go/ast" - "go/token" - "strconv" -) - -// ErrUnsupportedOperator is error unsupported operator -var ErrUnsupportedOperator = errors.New("unsupported operator") - -// Visitor is float visitor interface -// -// Deprecated: use expr.Visitor instead. -type Visitor interface { - Visit(node ast.Node) ast.Visitor - Result() (float64, error) -} - -// NewVisitor creates new float visitor -// -// Deprecated: use this instead: -// -// v := expr.NewVisitor( -// -// expr.WithNumericType(visitor.NumericTypeFloat) -// -// ). -func NewVisitor() Visitor { - return &visitor{} -} - -type visitor struct { - res float64 - err error -} - -func (v *visitor) visitUnary(unaryExpr *ast.UnaryExpr) ast.Visitor { - switch unaryExpr.Op { - case token.ADD, token.SUB: - xVisitor := &visitor{} - ast.Walk(xVisitor, unaryExpr.X) - if xVisitor.err != nil { - v.err = xVisitor.err - return nil - } - switch unaryExpr.Op { - case token.ADD: - v.res = xVisitor.res - case token.SUB: - v.res = xVisitor.res * -1 - } - default: - v.err = ErrUnsupportedOperator - } - return nil -} - -func (v *visitor) visitBinary(binaryExpr *ast.BinaryExpr) ast.Visitor { - switch binaryExpr.Op { - case token.ADD, token.SUB, token.MUL, token.QUO: - default: - v.err = ErrUnsupportedOperator - return nil - } - - x := &visitor{} - ast.Walk(x, binaryExpr.X) - if x.err != nil { - v.err = x.err - return nil - } - - y := &visitor{} - ast.Walk(y, binaryExpr.Y) - if y.err != nil { - v.err = y.err - return nil - } - - switch binaryExpr.Op { - case token.ADD: - v.res = x.res + y.res - case token.SUB: - v.res = x.res - y.res - case token.MUL: - v.res = x.res * y.res - case token.QUO: - v.res = x.res / y.res - } - - return nil -} - -func (v *visitor) Visit(node ast.Node) ast.Visitor { - if node == nil || v.err != nil { - return nil - } - - switch d := node.(type) { - case *ast.ParenExpr: - return v.Visit(d.X) - case *ast.UnaryExpr: - return v.visitUnary(d) - case *ast.BinaryExpr: - return v.visitBinary(d) - case *ast.BasicLit: - switch d.Kind { - case token.INT: - fallthrough - case token.FLOAT: - v.res, _ = strconv.ParseFloat(d.Value, 64) - } - return nil - } - - return v -} - -func (v *visitor) Result() (float64, error) { - return v.res, v.err -} diff --git a/float/float_test.go b/float/float_test.go deleted file mode 100644 index de8069d..0000000 --- a/float/float_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package float - -import ( - "errors" - "go/ast" - "go/token" - "math" - "testing" -) - -func TestVisitUnary(t *testing.T) { - tt := []struct { - name string - unaryExpr *ast.UnaryExpr - expectedResult float64 - expectedErr error - }{ - { - name: "+2.7", - unaryExpr: &ast.UnaryExpr{ - Op: token.ADD, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2.7", - }, - }, - expectedResult: 2.7, - }, - { - name: "-2.7", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2.7", - }, - }, - expectedResult: -2.7, - }, - { - name: "-(-2.7)", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.ParenExpr{ - X: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2.7", - }, - }, - }, - }, - expectedResult: 2.7, - }, - { - name: "!2", - unaryExpr: &ast.UnaryExpr{ - Op: token.NOT, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "-(!2)", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.ParenExpr{ - X: &ast.UnaryExpr{ - Op: token.NOT, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - }, - }, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := visitor{} - _ = v.visitUnary(tc.unaryExpr) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, v.err) - } - if v.res != tc.expectedResult { - t.Fatalf("expected result: %f, got: %f", tc.expectedResult, v.res) - } - }) - } -} - -func TestVisitBinary(t *testing.T) { - tt := []struct { - name string - x ast.Expr - y ast.Expr - op token.Token - expectedResult float64 - expectedErr error - }{ - { - name: "7.1 + 3 = 10.1", - x: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - y: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - op: token.ADD, - expectedResult: 10.1, - }, - { - name: "7.1 - 3 = 4.1", - x: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - y: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - op: token.SUB, - expectedResult: 4.1, - }, - { - name: "7.1 * 3 = 21.3", - x: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - y: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - op: token.MUL, - expectedResult: 21.3, - }, - { - name: "7.1 / 3 = 2.37", - x: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - y: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - op: token.QUO, - expectedResult: 2.37, - }, - { - name: "40.0 % 2 = ErrUnsupportedOperator", - x: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - y: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - op: token.REM, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "10 % 2 * 2 = ErrUnsupportedOperator", - x: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - Y: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - Op: token.REM, - }, - y: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - op: token.MUL, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "10 * 2 % 2 = ErrUnsupportedOperator", - x: &ast.BasicLit{Value: "3", Kind: token.FLOAT}, - y: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - Y: &ast.BasicLit{Value: "7.1", Kind: token.FLOAT}, - Op: token.REM, - }, - op: token.MUL, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - binaryExpr := &ast.BinaryExpr{X: tc.x, Y: tc.y, Op: tc.op} - v := &visitor{} - _ = v.visitBinary(binaryExpr) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected error: %v, got: %v", tc.expectedErr, v.err) - } - - // avoid floating point precision problem, only compare up to 2 decimal should be ok - if math.Round(v.res*100)/100 != math.Round(tc.expectedResult*100)/100 { - t.Fatalf("expected result: %f, got: %f", tc.expectedResult, v.res) - } - }) - } -} - -func TestVisit(t *testing.T) { - tt := []struct { - name string - node ast.Node - expectedResult float64 - }{ - { - name: "*ast.ParenExpr (1.1) = 1.1", - node: &ast.ParenExpr{ - X: &ast.BasicLit{Value: "1.1", Kind: token.FLOAT}, - }, - expectedResult: 1.1, - }, - { - name: "*ast.BinaryExpr 1.6 + 2 = 3.6", - node: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "1.6", Kind: token.FLOAT}, - Y: &ast.BasicLit{Value: "2", Kind: token.FLOAT}, - Op: token.ADD, - }, - expectedResult: 3.6, - }, - { - name: "*ast.BasicLit 1", - node: &ast.BasicLit{Value: "1", Kind: token.INT}, - expectedResult: 1, - }, - { - name: "not supported, e.g. *ast.ArrayType", - node: &ast.ArrayType{}, - expectedResult: 0, - }, - { - name: "nil node", - node: nil, - expectedResult: 0, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := NewVisitor().(*visitor) - _ = v.Visit(tc.node) - - // avoid floating point precision problem, only compare up to 2 decimal should be sufficient - if math.Round(v.res*100)/100 != math.Round(tc.expectedResult*100)/100 { - t.Fatalf("expected result: %f, got: %f", tc.expectedResult, v.res) - } - }) - } -} - -func TestResult(t *testing.T) { - tt := []struct { - name string - v *visitor - expectedResult float64 - expectedErr error - }{ - {name: "9.9", v: &visitor{res: 9.9}, expectedResult: 9.9}, - {name: "ErrUnsupportedOperator", v: &visitor{err: ErrUnsupportedOperator}, expectedErr: ErrUnsupportedOperator}, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - res, err := tc.v.Result() - if !errors.Is(err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, err) - } - if res != tc.expectedResult { - t.Fatalf("expected result: %v, got: %v", tc.expectedErr, err) - } - }) - } -} diff --git a/go.mod b/go.mod index b1035bc..3dad3c3 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/muktihari/expr go 1.13 + +require github.com/google/go-cmp v0.5.9 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..62841cd --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/integer/integer.go b/integer/integer.go deleted file mode 100644 index a3f9066..0000000 --- a/integer/integer.go +++ /dev/null @@ -1,176 +0,0 @@ -// Deprecated: This package is no longer maintained and might be deleted in the future, use expr.Visitor instead. -package integer - -import ( - "errors" - "go/ast" - "go/token" - "strconv" -) - -// ErrUnsupportedOperator is error unsupported operator -var ErrUnsupportedOperator = errors.New("unsupported operator") - -// ErrIntegerDividedByZero occurs when x/y and y equals to 0, Go does not allow integer to be divided by zero -var ErrIntegerDividedByZero = errors.New("integer divided by zero") - -// Visitor is integer visitor interface -// -// Deprecated: use expr.Visitor instead. -type Visitor interface { - Visit(node ast.Node) ast.Visitor - Result() (int, error) -} - -// NewVisitor creates new integer visitor -// -// Deprecated: use this instead: -// -// v := expr.NewVisitor( -// -// expr.WithNumericType(visitor.NumericTypeInt), -// -// expr.WithAllowIntegerDividedByZero(true), -// -// ) -func NewVisitor() Visitor { - return &visitor{} -} - -type visitor struct { - res int - base int // base10 except stated otherwise - err error -} - -func (v *visitor) visitUnary(unaryExpr *ast.UnaryExpr) ast.Visitor { - switch unaryExpr.Op { - case token.ADD, token.SUB: - xVisitor := &visitor{} - ast.Walk(xVisitor, unaryExpr.X) - if xVisitor.err != nil { - v.err = xVisitor.err - return nil - } - switch unaryExpr.Op { - case token.ADD: - v.res = xVisitor.res - case token.SUB: - v.res = xVisitor.res * -1 - } - default: - v.err = ErrUnsupportedOperator - } - return nil -} - -func (v *visitor) arithmetic(binaryExpr *ast.BinaryExpr) { - x := &visitor{} - ast.Walk(x, binaryExpr.X) - if x.err != nil { - v.err = x.err - return - } - y := &visitor{} - ast.Walk(y, binaryExpr.Y) - if y.err != nil { - v.err = y.err - return - } - - switch binaryExpr.Op { - case token.ADD: - v.res = x.res + y.res - case token.SUB: - v.res = x.res - y.res - case token.MUL: - v.res = x.res * y.res - case token.QUO: - if y.res == 0 { - v.err = ErrIntegerDividedByZero - return - } - v.res = x.res / y.res - case token.REM: - v.res = x.res % y.res - } -} - -func (v *visitor) bitwise(binaryExpr *ast.BinaryExpr) { - x := &visitor{base: 2} - ast.Walk(x, binaryExpr.X) - if x.err != nil { - v.err = x.err - return - } - - y := &visitor{base: 2} - ast.Walk(y, binaryExpr.Y) - if y.err != nil { - v.err = y.err - return - } - - switch binaryExpr.Op { - case token.AND: - v.res = x.res & y.res - case token.OR: - v.res = x.res | y.res - case token.XOR: - v.res = x.res ^ y.res - case token.AND_NOT: - v.res = x.res &^ y.res - case token.SHL: - v.res = x.res << y.res - case token.SHR: - v.res = x.res >> y.res - } -} - -func (v *visitor) visitBinary(binaryExpr *ast.BinaryExpr) ast.Visitor { - switch binaryExpr.Op { - case token.ADD, token.SUB, token.MUL, token.QUO, token.REM: - v.arithmetic(binaryExpr) - case token.AND, token.OR, token.XOR, token.AND_NOT, token.SHL, token.SHR: - v.bitwise(binaryExpr) - default: - v.err = ErrUnsupportedOperator - } - return nil -} - -func (v *visitor) Visit(node ast.Node) ast.Visitor { - if node == nil || v.err != nil { - return nil - } - - switch d := node.(type) { - case *ast.ParenExpr: - return v.Visit(d.X) - case *ast.UnaryExpr: - return v.visitUnary(d) - case *ast.BinaryExpr: - return v.visitBinary(d) - case *ast.BasicLit: - switch d.Kind { - case token.INT: - if v.base == 2 { - var res int64 - res, v.err = strconv.ParseInt(d.Value, 2, 64) - v.res = int(res) - return nil - } - v.res, _ = strconv.Atoi(d.Value) - case token.FLOAT: - floatValue, _ := strconv.ParseFloat(d.Value, 32) - v.res = int(floatValue) - } - return nil - } - - return v -} - -func (v *visitor) Result() (int, error) { - return v.res, v.err -} diff --git a/integer/integer_test.go b/integer/integer_test.go deleted file mode 100644 index dc28f55..0000000 --- a/integer/integer_test.go +++ /dev/null @@ -1,400 +0,0 @@ -package integer - -import ( - "errors" - "go/ast" - "go/token" - "testing" -) - -func TestVisitUnary(t *testing.T) { - tt := []struct { - name string - unaryExpr *ast.UnaryExpr - expectedResult int - expectedErr error - }{ - { - name: "+2", - unaryExpr: &ast.UnaryExpr{ - Op: token.ADD, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedResult: 2, - }, - { - name: "-2", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedResult: -2, - }, - { - name: "-(-2)", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.ParenExpr{ - X: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - }, - }, - expectedResult: 2, - }, - { - name: "!2", - unaryExpr: &ast.UnaryExpr{ - Op: token.NOT, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "-(!2)", - unaryExpr: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.ParenExpr{ - X: &ast.UnaryExpr{ - Op: token.NOT, - X: &ast.BasicLit{ - Kind: token.INT, - Value: "2", - }, - }, - }, - }, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := visitor{} - _ = v.visitUnary(tc.unaryExpr) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, v.err) - } - if v.res != tc.expectedResult { - t.Fatalf("expected result: %d, got: %d", tc.expectedResult, v.res) - } - }) - } -} - -func TestArithmetic(t *testing.T) { - tt := []struct { - name string - x ast.Expr - y ast.Expr - op token.Token - expectedResult int - expectedErr error - }{ - { - name: "7 + 3 = 10", - x: &ast.BasicLit{Value: "7", Kind: token.FLOAT}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - op: token.ADD, - expectedResult: 10, - }, - { - name: "7 - 3 = 4", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - op: token.SUB, - expectedResult: 4, - }, - { - name: "7 * 3 = 21", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - op: token.MUL, - expectedResult: 21, - }, - { - name: "7 / 3 = 2", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - op: token.QUO, - expectedResult: 2, - }, - { - name: "7 % 3 = 1", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - op: token.REM, - expectedResult: 1, - }, - { - name: "7 / 0 = ErrIntegerDividedByZero", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.BasicLit{Value: "0", Kind: token.INT}, - op: token.QUO, - expectedErr: ErrIntegerDividedByZero, - }, - { - name: "!7 / 3 = 2", - x: &ast.UnaryExpr{X: &ast.BasicLit{Value: "7"}, Op: token.MUL}, - y: &ast.BasicLit{Value: "3", Kind: token.INT}, - op: token.QUO, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "7 / !3 = 2", - x: &ast.BasicLit{Value: "7", Kind: token.INT}, - y: &ast.UnaryExpr{X: &ast.BasicLit{Value: "3"}, Op: token.MUL}, - op: token.QUO, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - binaryExpr := &ast.BinaryExpr{X: tc.x, Y: tc.y, Op: tc.op} - v := &visitor{} - v.arithmetic(binaryExpr) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected error: %v, got: %v", tc.expectedErr, v.err) - } - - if v.res != tc.expectedResult { - t.Fatalf("expected result: %d, got: %d", tc.expectedResult, v.res) - } - }) - } -} - -func TestBitwise(t *testing.T) { - tt := []struct { - name string - x ast.Expr - y ast.Expr - op token.Token - expectedResult int - expectedErr error - }{ - { - name: "1101 & 1011 = 1001 ≈ 9", - x: &ast.BasicLit{Value: "1101", Kind: token.INT}, - y: &ast.BasicLit{Value: "1011", Kind: token.INT}, - op: token.AND, - expectedResult: 9, - }, - { - name: "1101 | 1011 = 1111 ≈ 15", - x: &ast.BasicLit{Value: "1101", Kind: token.INT}, - y: &ast.BasicLit{Value: "1011", Kind: token.INT}, - op: token.OR, - expectedResult: 15, - }, - { - name: "1101 ^ 1011 = 0110 ≈ 6", - x: &ast.BasicLit{Value: "1101", Kind: token.INT}, - y: &ast.BasicLit{Value: "1011", Kind: token.INT}, - op: token.XOR, - expectedResult: 6, - }, - { - name: "1101 &^ 1011 can be written as 1101 & 0100 = 0100 ≈ 4", - x: &ast.BasicLit{Value: "1101", Kind: token.INT}, - y: &ast.BasicLit{Value: "1011", Kind: token.INT}, - op: token.AND_NOT, - expectedResult: 4, - }, - { - name: "0100 << 0110 = 1000000000000 ≈ 4 << 10 = 4096", - x: &ast.BasicLit{Value: "0100", Kind: token.INT}, - y: &ast.BasicLit{Value: "1010", Kind: token.INT}, - op: token.SHL, - expectedResult: 4096, - }, - { - name: "1111 >> 0010 = 0011 ≈ 15 >> 2 = 3", - x: &ast.BasicLit{Value: "1111", Kind: token.INT}, - y: &ast.BasicLit{Value: "0010", Kind: token.INT}, - op: token.SHR, - expectedResult: 3, - }, - { - name: "!1111 & 0010 = ErrUnsupportedOperator on x", - x: &ast.UnaryExpr{ - X: &ast.BasicLit{Value: "1111", Kind: token.INT}, - Op: token.NOT, - }, - y: &ast.BasicLit{Value: "0010", Kind: token.INT}, - op: token.AND, - expectedErr: ErrUnsupportedOperator, - }, - { - name: "1111 & !0010 = ErrUnsupportedOperator on x", - x: &ast.BasicLit{Value: "1111", Kind: token.INT}, - y: &ast.UnaryExpr{ - X: &ast.BasicLit{Value: "0010", Kind: token.INT}, - Op: token.NOT, - }, - op: token.AND, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - binaryExpr := &ast.BinaryExpr{X: tc.x, Y: tc.y, Op: tc.op} - v := &visitor{} - v.bitwise(binaryExpr) - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected error: %v, got: %v", tc.expectedErr, v.err) - } - if v.res != tc.expectedResult { - t.Fatalf("expected result: %d, got: %d", tc.expectedResult, v.res) - } - }) - } -} - -func TestVisitBinary(t *testing.T) { - tt := []struct { - name string - binaryExpr *ast.BinaryExpr - expectedResult int - expectedErr error - }{ - { - name: "1 + 4 = 5", - binaryExpr: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "1", Kind: token.INT}, - Y: &ast.BasicLit{Value: "4", Kind: token.INT}, - Op: token.ADD, - }, - expectedResult: 5, - }, - { - name: "0001 & 1111 = 1", - binaryExpr: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "0001", Kind: token.INT}, - Y: &ast.BasicLit{Value: "1111", Kind: token.INT}, - Op: token.AND, - }, - expectedResult: 1, - }, - { - name: "0001 = 1111 = ErrUnsupportedOperator", - binaryExpr: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "0001", Kind: token.INT}, - Y: &ast.BasicLit{Value: "1111", Kind: token.INT}, - Op: token.ASSIGN, - }, - expectedErr: ErrUnsupportedOperator, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := &visitor{} - _ = v.visitBinary(tc.binaryExpr) - - if !errors.Is(v.err, tc.expectedErr) { - t.Fatalf("expected error: %v, got: %v", tc.expectedErr, v.err) - } - if v.res != tc.expectedResult { - t.Fatalf("expected result: %d, got: %d", tc.expectedResult, v.res) - } - }) - } - -} - -func TestVisit(t *testing.T) { - tt := []struct { - name string - node ast.Node - expectedResult int - }{ - { - name: "*ast.ParenExpr, (1) = 1", - node: &ast.ParenExpr{ - X: &ast.BasicLit{Value: "1", Kind: token.INT}, - }, - expectedResult: 1, - }, - { - name: "*ast.BinaryExpr, 1 + 2 = 3", - node: &ast.BinaryExpr{ - X: &ast.BasicLit{Value: "1", Kind: token.INT}, - Y: &ast.BasicLit{Value: "2", Kind: token.INT}, - Op: token.ADD, - }, - expectedResult: 3, - }, - { - name: "*ast.BasicLit, 1", - node: &ast.BasicLit{Value: "1", Kind: token.INT}, - expectedResult: 1, - }, - { - name: "not supported, e.g. *ast.ArrayType", - node: &ast.ArrayType{}, - expectedResult: 0, - }, - { - name: "nil node", - node: nil, - expectedResult: 0, - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - v := NewVisitor().(*visitor) - _ = v.Visit(tc.node) - - if v.res != tc.expectedResult { - t.Fatalf("expected result: %d, got: %d", tc.expectedResult, v.res) - } - }) - } -} - -func TestResult(t *testing.T) { - tt := []struct { - name string - v *visitor - expectedResult int - expectedErr error - }{ - {name: "9.9", v: &visitor{res: 9}, expectedResult: 9}, - {name: "ErrUnsupportedOperator", v: &visitor{err: ErrUnsupportedOperator}, expectedErr: ErrUnsupportedOperator}, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.name, func(t *testing.T) { - res, err := tc.v.Result() - if !errors.Is(err, tc.expectedErr) { - t.Fatalf("expected err: %v, got: %v", tc.expectedErr, err) - } - if res != tc.expectedResult { - t.Fatalf("expected result: %v, got: %v", tc.expectedErr, err) - } - }) - } -}