The rule engine is implemented with go-yacc, parsing the calculation string and calculating the result. It sas supported input variables, arithmetic operations, logical operations, decimal float and some built-in functions, which will be extended later as needed.
go get github.com/uyouii/rule_engine
rule_engine library requires Go version >=1.7
example project: rule_engine_example
package main
import (
"fmt"
"github.com/uyouii/rule_engine"
)
func main() {
params := []*rule_engine.Param{
rule_engine.GetParam("i", 100),
rule_engine.GetParam("f", 3.5),
rule_engine.GetParam("s", "hello world"),
rule_engine.GetParam("b", false),
rule_engine.GetParam("d", 3.3),
// get decimal from string
rule_engine.GetParamWithType("d2", rule_engine.ValueTypeDecimal, "3.3"),
}
// use decimal: true
praser, err := rule_engine.GetNewPraser(params, true)
if err != nil {
panic(err)
}
exampleList := []string{
// integrate
`4 * (2 + 3) - 5 * 3`,
// float
`3.0 * (2.5 - 3)`,
// logic
`1 < 2 and 2 < 3 and 4.0 != 4.0001`,
`1 > 2 && 1 < 2`,
// func exapmle
`max(min(10.0, 20, 30), len("vstr"))`,
`min(len("test"), abs(-4.5), min(5,6))`,
`upper("abc") == "ABC"`,
`startWith("hello world", "hel")`,
`int(97) + 3 == max(100, -1)`,
`regexMatch("^0[xX][a-fA-F0-9]+", "0xasf4")`,
`string({{d}}) == "3.3"`,
// use param
`min(len({{s}}), {{i}}, {{f}}, {{d}})`,
`int({{i}} / {{f}})`,
`{{d}} * 10 - 3 - int({{i}} / {{f}})`,
`{{d2}} - {{d}}`,
// if else
`{{d}} * 10 if len({{s}}) > 10 else {{f}} / 10`,
}
for _, example := range exampleList {
res, err := praser.Parse(example)
if err != nil {
panic(err)
}
fmt.Printf("%v --> %v\n", example, res.Value)
}
}
Output
4 * (2 + 3) - 5 * 3 --> 5
3.0 * (2.5 - 3) --> -1.5
1 < 2 and 2 < 3 and 4.0 != 4.0001 --> true
1 > 2 && 1 < 2 --> false
max(min(10.0, 20, 30), len("vstr")) --> 10
min(len("test"), abs(-4.5), min(5,6)) --> 4
upper("abc") == "ABC" --> true
startWith("hello world", "hel") --> true
int(97) + 3 == max(100, -1) --> true
regexMatch("^0[xX][a-fA-F0-9]+", "0xasf4") --> true
string({{d}}) == "3.3" --> true
min(len({{s}}), {{i}}, {{f}}, {{d}}) --> 3.3
int({{i}} / {{f}}) --> 28
{{d}} * 10 - 3 - int({{i}} / {{f}}) --> 2
{{d2}} - {{d}} --> 0
{{d}} * 10 if len({{s}}) > 10 else {{f}} / 10 --> 33
https://pkg.go.dev/github.com/uyouii/rule_engine
// 1. use GetNewPraser to get a New Praser
// the params are the variable will be used in the calculation
// is set useDecimal, all the float in the param and calculation will be changed to decimal.
func GetNewPraser(params []*Param, useDecimal bool) (*Praser, error)
// 2. use Parse to get the result
func (p *Praser) Parse(str string) (*TokenNode, error)
// for example
praser, _ := rule_engine.GetNewPraser(params, true)
res, _ := praser.Parse(`1 + 1`)
fmt.Printf(res.Value)
2
When set the struct Param:
type Param struct {
Name string // value name
Type ValueType // value type
Value interface{} // value
}
if only set Name
and Value
, then Praser
will try to parse the Type
from Value
// for example
p1 := GetParamWithType("x", rule_engine.ValueTypeDecimal, "3.3")
variable {{x}} will be prase to decimal 3.3
if both set Name
, Value
and Type
, Praser
will try to reparse the Value
according to Type
.
// for example
p2 := GetParam("y", "3.3")
variable {{y}} will be prase to string "3.3"
the detail about the variable can see Support Variable
section.
the Api Parse will return a TokenNode
as Result.
type TokenNode struct {
ValueType ValueType // result type, can see ValueType
Value interface{} // result value
}
// if want get interface{} res, can use
func(t *TokenNode) GetValue() interface{}
// if want get detail type value, can use
func (t *TokenNode) GetInt() int64
func (t *TokenNode) GetBool() bool
func (t *TokenNode) GetFloat() float64
func (t *TokenNode) GetDecimal() decimal.Decimal
func (t *TokenNode) GetString() string
Type | ValueType in Code |
---|---|
bool | ValueTypeBool |
string | ValueTypeString |
int | ValueTypeInteger |
float | ValueTypeFloat |
decimal | ValueTypeDecimal |
notice:the implementation of decimal in the project depends on the https://github.com/shopspring/decimal
The value type will be implicitly reduced to the more precise type in calculation.
if int
meet float
, will be treated as float
, and the result is float
.
If int
or float
meet decimal
, will be treated as decimal
, and the result is decimal
.
int >> float >> decimal
for example:
int + float = float
float * decimal = decimal
decimal - int = decimal
max(int, float, deciamal) = decimal
Operator | Name | Support Types |
---|---|---|
() |
Parentheses | ALL |
{{var_name}} |
External Variable | ALL |
- |
Negative | int, float, decimal |
! not |
Not | bool |
+ |
Addition | int, float, decimal |
- |
Subtraction | int, float, decimal |
* |
Multiplication | int, float, decimal |
/ |
Division | int, float, decimal |
% |
Mod | int |
> |
Larger | int, float, decimal |
>= |
Larger or Equal | int, float, decimal |
< |
Less | int, float, decimal |
<= |
Less or Equal | int, float, decimal |
== |
Equal | ALL |
!= |
NotEqual | ALL |
and && |
And | bool |
or || |
Or | bool |
x if c else y |
Ternary operator | ALL, c must bool |
Decreasing priority from top to bottom.
() {{var_name}}
! not -(Negative)
* / %
+ -
> >= < <=
== !=
if else (ternary operator)
and && or ||
rule_engine
supports variable in calculation, by use {{}}
(double braces) to enclose the variable name.
the varibale name and value should be passed in the interface, with the type: Param
can see the example:
d: type decimal, value 3.3
i: type int, value 100
{{d}} * 100 + 3 - int({{i}} * 10 / 3) --> 0
the variable type can be int
, float
, decimal
, bool
, string
Function Name | Descrption |
---|---|
len() | length of the string |
min() | min of the args |
max() | max of the args |
abs() | Abs |
upper() | Upper of the string |
lower() | Lower of the string |
startWith() | check string start with some prefix |
endWith() | check string end with some suffix |
regexMatch() | Regex Match |
int() | change arg to int type |
float() | change arg to float type |
decimal() | change arg to decimal type |
string() | change arg to string type |
// length of the string
// param {string} input string
// return {bool}
bool len(str)
e.g.
len("test")
4
// min of the args
// param {int/bool/decimal}
// return {int/bool/decimal}, result type will be the most precise type.
any min(x, y, z, ...)
e.g.
min(1, len("test"), 6.8)
6.8
// max of the args
// param {int/bool/decimal}
// return {int/bool/decimal}, result type will be the most precise type.
any max(x, y, z, ...)
e.g.
max(1, len("test"), 6.8)
6.8
// Abs of the arg
// param {int/bool/decimal} x
// return {int/bool/decimal}, result type accornding to the input type
any abs(x)
e.g.
abs(-1.1)
1.1
// the upper string of the input
// param {string} s
// return {string}
string upper(s string)
e.g.
upper("Hello World")
"HELLO WORLD"
// the lower string of the input
// param {string} s
// return {string}
string lower(s string)
e.g.
lower("Hello World")
"hello world"
// check s start with prefix
// param {string} s
// param {string} prefix
// return {bool}
bool startWith(s string, prefix string)
e.g.
startWith("Hello World", "Hello")
true
// check s start with suffix
// param {string} s
// param {string} suffix
// return {bool}
bool endWith(s string, suffix string)
e.g.
startWith("Hello World", "World")
true
// whether the string s contains any match of the regular expression pattern.
// param {string} pattern, the regex pattern
// param {string} the check string
// return {bool}
bool regexMatch(pattern string, s string)
e.g.
regexMatch("^test$", "test")
true
regexMatch("(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]","https://www.baidu.com")
true
// change value to integer type
// param {int/float/decimal/string} v
// return {int}
int int(v any)
e.g.
int("100")
100
int(33.33)
33
// change value to float type
// param {int/float/decimal/string} v
// return {float}
float float(v any)
e.g.
float("33.3")
33.3
float(100)
100
// change value to decimal type
// param {int/float/decimal/string} v
// return {decimal}
decimal decimal(v any)
e.g.
decimal("33.3")
33.3
decimal(100)
100
// change value to string type
// param {int/float/decimal/string} v
// return {string}
string string(v any)
e.g.
string(33.3)
"33.3"
string(100)
"100"
This is the BNF(Backus Normal Form) of the rule_engine, how to reduce the input and calculate the result.
top :
TRANSLATION_UNIT
TRANSLATION_UNIT :
LOGIC_EXPR END
LOGIC_EXPR :
LOGIC_OR_EXPR
LOGIC_OR_EXPR :
LOGIC_AND_EXPR
| LOGIC_OR_EXPR OR LOGIC_AND_EXPR
LOGIC_AND_EXPR :
THIRD_OPER_EXPR
| LOGIC_AND_EXPR AND THIRD_OPER_EXPR
THIRD_OPER_EXPR :
EQUAL_EXPR
| EQUAL_EXPR IF THIRD_OPER_EXPR ELSE THIRD_OPER_EXPR
EQUAL_EXPR :
RELATION_EXPR
| EQUAL_EXPR EQ RELATION_EXPR
| EQUAL_EXPR NE RELATION_EXPR
RELATION_EXPR :
ADD_EXPR
| ADD_EXPR '<' RELATION_EXPR
| ADD_EXPR '>' RELATION_EXPR
| ADD_EXPR LE RELATION_EXPR
| ADD_EXPR GE RELATION_EXPR
ADD_EXPR :
MUL_EXPR
| ADD_EXPR '+' MUL_EXPR
| ADD_EXPR '-' MUL_EXPR
MUL_EXPR :
UNARY_EXPR
| MUL_EXPR '*' UNARY_EXPR
| MUL_EXPR '/' UNARY_EXPR
| MUL_EXPR '%' UNARY_EXPR
UNARY_EXPR :
POST_EXPR
| '-' PRIMARY_EXPR
| NOT PRIMARY_EXPR
POST_EXPR :
PRIMARY_EXPR
| IDENTIFIER '(' ARGUMENT_EXPRSSION_LIST ')'
| IDENTIFIER '(' ')'
ARGUMENT_EXPRSSION_LIST :
LOGIC_EXPR
| ARGUMENT_EXPRSSION_LIST ',' LOGIC_EXPR
PRIMARY_EXPR :
INTEGER
| FLOAT
| BOOL
| STRING
| ERROR
| '(' LOGIC_EXPR ')'
| VALUE_EXPR
VALUE_EXPR :
IDLEFT VAR_NAME IDRIGHT
VAR_NAME :
IDENTIFIER
| VAR_NAME '.' IDENTIFIER
| VAR_NAME '.' INTEGER