Skip to content

Commit

Permalink
feat: add bind and explain package (#27)
Browse files Browse the repository at this point in the history
Additional Packages:
- bind is for binding variables into expr string
- explain is for explaining step-by-step operations in expr (beta ver., can be improved later)

BREAKING CHANGES:
- deprecated package boolean, float and integer has been removed. (I don't think anyone use it anyway)
  • Loading branch information
muktihari authored Aug 8, 2023
1 parent 044b386 commit a13465b
Show file tree
Hide file tree
Showing 21 changed files with 1,267 additions and 1,842 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions arithmetic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
40 changes: 40 additions & 0 deletions bind/README.md
Original file line number Diff line number Diff line change
@@ -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)"
```
245 changes: 245 additions & 0 deletions bind/bind.go
Original file line number Diff line number Diff line change
@@ -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 -> "<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]" "<nil>", 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
}
}
Loading

0 comments on commit a13465b

Please sign in to comment.