├── .gitignore ├── README.md ├── bean.go ├── broker.go ├── broker_test.go ├── consts.go ├── core.go ├── core_test.go ├── reverse_polish.go ├── tree.go └── tree_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.idea 3 | *.iml 4 | gometalinter.json 5 | validate.sh 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Intro 2 | 3 | go-rule-engine是用Golang实现的小型规则引擎,可以使用json串构造子规则,以及逻辑表达式表征子规则间逻辑运算关系。也可以通过直接构造子规则对象传入,这样方便子规则的持久化和复用。 4 | 5 | ### Usage 6 | 7 | ```go 8 | // build rules 9 | jsonRules := []byte(`[ 10 | {"op": "=", "key": "Grade", "val": 3, "id": 1, "msg": "Grade not match"}, 11 | {"op": "=", "key": "Sex", "val": "male", "id": 2, "msg": "not male"}, 12 | {"op": ">=", "key": "Score.Math", "val": 90, "id": 3, "msg": "Math not so well"}, 13 | {"op": ">=", "key": "Score.Physic", "val": 90, "id": 4, "msg": "Physic not so well"} 14 | ]`) 15 | logic := "1 and not 2 and (3 or 4)" 16 | ruleToFit, err := NewRulesWithJSONAndLogic(jsonRules, logic) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | // prepare obj 22 | type Exams struct { 23 | Math int 24 | Physic int 25 | } 26 | type Student struct { 27 | Name string 28 | Grade int 29 | Sex string 30 | Score *Exams 31 | } 32 | Chris := &Student{ 33 | Name: "Chris", 34 | Grade: 3, 35 | Sex: "female", 36 | Score: &Exams{Math: 88, Physic: 91}, 37 | } 38 | 39 | // fit 40 | fit, msg := ruleToFit.Fit(Chris) 41 | t.Log(fit) 42 | t.Log(msg) 43 | ``` 44 | 45 | ```go 46 | // result 47 | true // fit 48 | map[1:Grade not match 2:not male 4:Physic not so well] // 注意:当fit=true,msg是命中的ruleIds 49 | ``` 50 | 51 | ```go 52 | Helen := &Student{ 53 | Name: "Helen", 54 | Grade: 4, 55 | Sex: "female", 56 | Score: &Exams{Math: 96, Physic: 93}, 57 | } 58 | ``` 59 | 60 | ```go 61 | // result 62 | false // fit 63 | map[1:Grade not match] // msg 64 | ``` 65 | 66 | 67 | 68 | ### 概念 69 | 70 | ##### 子规则Rule 71 | 72 | ```go 73 | // key支持链式表达 74 | {"op": ">=", "key": "Score.Physic", "val": 90, "id": 4, "msg": "Physic not so well"} 75 | 76 | // Rule 最小单元,子规则 77 | type Rule struct { 78 | Op string `json:"op"` // 预算符 79 | Key string `json:"key"` // 目标变量键名 80 | Val interface{} `json:"val"` // 目标变量子规则存值 81 | ID int `json:"id"` // 子规则ID 82 | Msg string `json:"msg"` // 该规则抛出的负提示 83 | } 84 | ``` 85 | 86 | ##### 逻辑表达式logic 87 | 88 | ```go 89 | // 数字是rule的ID,当需要所有rule都为true,可以缺省写法:logic="" 90 | "1 and not 2 and (3 or 4)" 91 | ``` 92 | 93 | ##### 规则Rules 94 | 95 | ```go 96 | // 即创建的ruleToFit对象 97 | ruleToFit, err := NewRulesWithJSONAndLogic(jsonRules, logic) 98 | 99 | // Rules 规则,拥有逻辑表达式 100 | type Rules struct { 101 | Rules []*Rule // 子规则集合 102 | Logic string // 逻辑表达式,使用子规则ID运算表达 103 | Name string // 规则名称 104 | Msg string // 规则抛出的负提示 105 | } 106 | ``` 107 | 108 | ##### 匹配结果Fit 109 | 110 | ```go 111 | fit, msg := ruleToFit.Fit(Chris) 112 | t.Log(fit) 113 | 114 | false 115 | ``` 116 | 117 | ##### 原因msg 118 | 119 | ```go 120 | fit, msg := ruleToFit.Fit(Chris) 121 | t.Log(fit) 122 | t.Log(msg) 123 | 124 | false 125 | // fit=false, 键1是导致fit为false的那个rule的ID,值是那个rule的msg字段,用于提示 126 | map[1:Grade not match] 127 | // 注意:若fit=true, msg则是参与导致fit为true的那些rule的IDs,这是为了表达反向逻辑的规则中的原因 128 | ``` 129 | 130 | 131 | 132 | ### API 133 | 134 | ```go 135 | // NewRulesWithJSONAndLogic 用json串构造Rules的标准方法,logic表达式如果没有则传空字符串 136 | func NewRulesWithJSONAndLogic(jsonStr []byte, logic string) (*Rules, error) 137 | 138 | // NewRulesWithArrayAndLogic 用rule数组构造Rules的标准方法,logic表达式如果没有则传空字符串 139 | func NewRulesWithArrayAndLogic(rules []*Rule, logic string) (*Rules, error) 140 | 141 | // Fit Rules匹配传入结构体 142 | func (rs *Rules) Fit(o interface{}) (bool, map[int]string) 143 | 144 | // FitWithMap Rules匹配map 145 | func (rs *Rules) FitWithMap(o map[string]interface{}) (bool, map[int]string) 146 | 147 | // FitAskVal Rules匹配结构体,同时返回所有子规则key对应实际值 148 | func (rs *Rules) FitAskVal(o interface{}) (bool, map[int]string, map[int]interface{}) 149 | 150 | // FitWithMapAskVal Rules匹配map,同时返回所有子规则key对应实际值 151 | func (rs *Rules) FitWithMapAskVal(o map[string]interface{}) (bool, map[int]string, map[int]interface{}) 152 | 153 | // GetRuleIDsByLogicExpression 根据逻辑表达式得到规则id列表 154 | func GetRuleIDsByLogicExpression(logic string) ([]int, error) 155 | ``` 156 | 157 | 158 | 159 | ### 支持的算符 160 | 161 | ```go 162 | // 等于 163 | case "=", "eq": 164 | 165 | // 大于 166 | case ">", "gt": 167 | 168 | // 小于 169 | case "<", "lt": 170 | 171 | // 大于或等于 172 | case ">=", "gte": 173 | 174 | // 小于或等于 175 | case "<=", "lte": 176 | 177 | // 不等于 178 | case "!=", "neq": 179 | 180 | // 取值在...之中(val用逗号,分隔取值) 181 | case "@", "in": 182 | 183 | // 取值不能在...之中 184 | case "!@", "nin": 185 | 186 | // 正则表达式 187 | case "^$", "regex": 188 | 189 | // 为空不存在 190 | case "0", "empty": 191 | 192 | // 不为空 193 | case "1", "nempty": 194 | 195 | // 区间(只支持数字比较) 196 | // 支持开闭区间格式,val="[,12.1]", "(1, 3]", "(8, )" etc. 197 | case "<<", "between": 198 | 199 | ``` 200 | 201 | ### 支持的逻辑 202 | 203 | ```go 204 | // 与 205 | and 206 | 207 | // 或 208 | or 209 | 210 | // 非 211 | not 212 | 213 | // 括号,可嵌套 214 | () 215 | ``` 216 | 217 | -------------------------------------------------------------------------------- /bean.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | // Rule 最小单元,子规则 4 | type Rule struct { 5 | Op string `json:"op"` // 算符 6 | Key string `json:"key"` // 目标变量键名 7 | Val interface{} `json:"val"` // 目标变量子规则存值 8 | ID int `json:"id"` // 子规则ID 9 | Msg string `json:"msg"` // 该规则抛出的负提示 10 | } 11 | 12 | // Rules 规则,拥有逻辑表达式 13 | type Rules struct { 14 | Rules []*Rule // 子规则集合 15 | Logic string // 逻辑表达式,使用子规则ID运算表达 16 | Name string // 规则名称 17 | Msg string // 规则抛出的负提示 18 | Val interface{} // 改规则所代表的存值 19 | } 20 | 21 | // RulesList 规则组,顺序即优先级 22 | type RulesList struct { 23 | RulesList []*Rules 24 | Name string 25 | Msg string 26 | } 27 | 28 | // ValidOperators 有效逻辑运算符 29 | var ValidOperators = []string{"and", "or", "not"} 30 | 31 | // Node 树节点 32 | type Node struct { 33 | Expr string // 分割的logic表达式 34 | ChildrenOp string // 孩子树之间的运算符: and, or, not 35 | Val bool // 节点值 36 | Computed bool // 节点值被计算过 37 | Leaf bool // 是否叶子节点 38 | Should bool // 为了Fit为true,此节点必须的值 39 | Blamed bool // 此节点为了Fit为true,有责任必须为某值 40 | Children []*Node // 孩子树 41 | } 42 | 43 | type operator string 44 | 45 | const ( 46 | // OperatorAnd and 47 | OperatorAnd operator = "and" 48 | // OperatorOr or 49 | OperatorOr operator = "or" 50 | // OperatorNot not 51 | OperatorNot operator = "not" 52 | ) 53 | 54 | // ValidAtomOperatorsDisplay 有效子规则运算符-展示 55 | var ValidAtomOperatorsDisplay = []string{"=", ">", "<", ">=", "<=", "!=", "in", "nin", "regex", "empty", "nempty", "between", "intersect"} 56 | -------------------------------------------------------------------------------- /broker.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/fatih/structs" 9 | ) 10 | 11 | // NewRulesWithJSONAndLogicAndInfo 用json串构造Rules的完全方法,logic表达式如果没有则传空字符串, ["name": "规则名称", "msg": "规则不符合的提示"] 12 | func NewRulesWithJSONAndLogicAndInfo(jsonStr []byte, logic string, extractInfo map[string]string) (*Rules, error) { 13 | rulesObj, err := NewRulesWithJSONAndLogic(jsonStr, logic) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return injectExtractInfo(rulesObj, extractInfo), nil 18 | } 19 | 20 | // NewRulesWithArrayAndLogicAndInfo 用rule数组构造Rules的完全方法,logic表达式如果没有则传空字符串, ["name": "规则名称", "msg": "规则不符合的提示"] 21 | func NewRulesWithArrayAndLogicAndInfo(rules []*Rule, logic string, extractInfo map[string]string) (*Rules, error) { 22 | rulesObj, err := NewRulesWithArrayAndLogic(rules, logic) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return injectExtractInfo(rulesObj, extractInfo), nil 27 | } 28 | 29 | // NewRulesWithJSONAndLogic 用json串构造Rules的标准方法,logic表达式如果没有则传空字符串 30 | func NewRulesWithJSONAndLogic(jsonStr []byte, logic string) (*Rules, error) { 31 | if logic == "" { 32 | // empty logic 33 | return newRulesWithJSON(jsonStr) 34 | } 35 | rulesObj, err := newRulesWithJSON(jsonStr) 36 | if err != nil { 37 | return nil, err 38 | } 39 | rulesObj, err = injectLogic(rulesObj, logic) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return rulesObj, nil 45 | } 46 | 47 | // NewRulesWithArrayAndLogic 用rule数组构造Rules的标准方法,logic表达式如果没有则传空字符串 48 | func NewRulesWithArrayAndLogic(rules []*Rule, logic string) (*Rules, error) { 49 | if logic == "" { 50 | // empty logic 51 | return newRulesWithArray(rules), nil 52 | } 53 | rulesObj := newRulesWithArray(rules) 54 | rulesObj, err := injectLogic(rulesObj, logic) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return rulesObj, nil 60 | } 61 | 62 | // Fit Rules匹配传入结构体 63 | func (rs *Rules) Fit(o interface{}) (bool, map[int]string) { 64 | m := structs.Map(o) 65 | return rs.FitWithMap(m) 66 | } 67 | 68 | // FitWithMap Rules匹配map 69 | func (rs *Rules) FitWithMap(o map[string]interface{}) (bool, map[int]string) { 70 | fit, tips, _ := rs.fitWithMapInFact(o) 71 | return fit, tips 72 | } 73 | 74 | // FitAskVal Rules匹配结构体,同时返回所有子规则key值 75 | func (rs *Rules) FitAskVal(o interface{}) (bool, map[int]string, map[int]interface{}) { 76 | m := structs.Map(o) 77 | return rs.FitWithMapAskVal(m) 78 | } 79 | 80 | // FitWithMapAskVal Rules匹配map,同时返回所有子规则key值 81 | func (rs *Rules) FitWithMapAskVal(o map[string]interface{}) (bool, map[int]string, map[int]interface{}) { 82 | return rs.fitWithMapInFact(o) 83 | } 84 | 85 | // GetRuleIDsByLogicExpression 根据逻辑表达式得到规则id列表 86 | func GetRuleIDsByLogicExpression(logic string) ([]int, error) { 87 | var result []int 88 | formatLogic, errLogic := validLogic(logic) 89 | if errLogic != nil { 90 | return nil, errLogic 91 | } 92 | if formatLogic == EmptyStr { 93 | return result, nil 94 | } 95 | 96 | // return rule id list 97 | var mapGot = make(map[int]bool) 98 | listSymbol := strings.Split(formatLogic, Space) 99 | regex := regexp.MustCompile(PatternNumber) 100 | for _, symbol := range listSymbol { 101 | if regex.MatchString(symbol) { 102 | // is id, check it 103 | id, err := strconv.Atoi(symbol) 104 | if err != nil { 105 | return nil, err 106 | } 107 | // keep unique 108 | if _, ok := mapGot[id]; !ok { 109 | result = append(result, id) 110 | mapGot[id] = true 111 | } 112 | } 113 | } 114 | return result, nil 115 | } 116 | 117 | // NewRulesList RulesList的构造方法,["name": "规则集的名称", "msg": "规则集的简述"] 118 | func NewRulesList(listRules []*Rules, extractInfo map[string]string) *RulesList { 119 | // check if every rules has name, if not give a index as name 120 | for index, rules := range listRules { 121 | if rules.Name == EmptyStr { 122 | rules.Name = strconv.Itoa(index + 1) 123 | } 124 | } 125 | name := extractInfo["name"] 126 | msg := extractInfo["msg"] 127 | return &RulesList{ 128 | RulesList: listRules, 129 | Name: name, 130 | Msg: msg, 131 | } 132 | } 133 | 134 | // Fit RulesList's fit, means hitting first rules in array 135 | func (rst *RulesList) Fit(o interface{}) *Rules { 136 | m := structs.Map(o) 137 | return rst.FitWithMap(m) 138 | } 139 | 140 | // FitWithMap RulesList's fit, means hitting first rules in array 141 | func (rst *RulesList) FitWithMap(o map[string]interface{}) *Rules { 142 | for _, rs := range rst.RulesList { 143 | if flag, _ := rs.FitWithMap(o); flag { 144 | return rs 145 | } 146 | } 147 | return nil 148 | } 149 | 150 | // FitGetStr return hit rules value, string 151 | func (rst *RulesList) FitGetStr(o interface{}) (bool, string) { 152 | rs := rst.Fit(o) 153 | if rs == nil { 154 | return false, EmptyStr 155 | } 156 | if str, ok := rs.Val.(string); ok { 157 | return true, str 158 | } 159 | return true, EmptyStr 160 | } 161 | 162 | // FitGetFloat64 return hit value, float64 163 | func (rst *RulesList) FitGetFloat64(o interface{}) (bool, float64) { 164 | rs := rst.Fit(o) 165 | if rs == nil { 166 | return false, EmptyFloat64 167 | } 168 | return true, formatNumber(rs.Val) 169 | } 170 | 171 | // FitGetInt64 return hit value, int64 172 | func (rst *RulesList) FitGetInt64(o interface{}) (bool, int64) { 173 | rs := rst.Fit(o) 174 | if rs == nil { 175 | return false, EmptyFloat64 176 | } 177 | var result int64 178 | switch t := rs.Val.(type) { 179 | case uint: 180 | result = int64(t) 181 | case uint8: 182 | result = int64(t) 183 | case uint16: 184 | result = int64(t) 185 | case uint32: 186 | result = int64(t) 187 | case uint64: 188 | result = int64(t) 189 | case int: 190 | result = int64(t) 191 | case int8: 192 | result = int64(t) 193 | case int16: 194 | result = int64(t) 195 | case int32: 196 | result = int64(t) 197 | case int64: 198 | result = t 199 | case float32: 200 | result = int64(t + 1e-5) 201 | case float64: 202 | result = int64(t + 1e-5) 203 | } 204 | return true, result 205 | } 206 | 207 | // CheckLogicExpressionAndFormat 检查逻辑表达式正确性,并返回formatted 208 | func CheckLogicExpressionAndFormat(logic string) (string, error) { 209 | return validLogic(logic) 210 | } 211 | -------------------------------------------------------------------------------- /broker_test.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRules_Fit3(t *testing.T) { 11 | // build rules 12 | jsonRules := []byte(`[ 13 | {"op": "=", "key": "Grade", "val": 3, "id": 1, "msg": "Grade not match"}, 14 | {"op": "=", "key": "Sex", "val": "male", "id": 2, "msg": "not male"}, 15 | {"op": ">=", "key": "Score.Math", "val": 90, "id": 3, "msg": "Math not so well"}, 16 | {"op": ">=", "key": "Score.Physic", "val": 90, "id": 4, "msg": "Physic not so well"} 17 | ]`) 18 | logic := "1 and not 2 and (3 or 4)" 19 | ruleToFit, err := NewRulesWithJSONAndLogic(jsonRules, logic) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | // prepare obj 25 | type Exams struct { 26 | Math int 27 | Physic int 28 | } 29 | type Student struct { 30 | Name string 31 | Grade int 32 | Sex string 33 | Score *Exams 34 | } 35 | //Chris := &Student{ 36 | // Name: "Chris", 37 | // Grade: 3, 38 | // Sex: "female", 39 | // Score: &Exams{Math: 88, Physic: 91}, 40 | //} 41 | Helen := &Student{ 42 | Name: "Helen", 43 | Grade: 4, 44 | Sex: "female", 45 | Score: &Exams{Math: 96, Physic: 93}, 46 | } 47 | 48 | // fit 49 | fit, msg := ruleToFit.Fit(Helen) 50 | assert.False(t, fit) 51 | t.Log(fit) 52 | t.Log(msg) 53 | } 54 | 55 | func TestRules_Fit4(t *testing.T) { 56 | jsonRules := []byte(`[ 57 | {"op": "=", "key": "A", "val": 3, "id": 1, "msg": "A fail"}, 58 | {"op": ">", "key": "B", "val": 1, "id": 2, "msg": "B fail"}, 59 | {"op": "<", "key": "C", "val": 5, "id": 3, "msg": "C fail"} 60 | ]`) 61 | logic := "1 or 2" 62 | rs, err := NewRulesWithJSONAndLogic(jsonRules, logic) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | type Obj struct { 67 | A int 68 | B int 69 | C int 70 | } 71 | o := &Obj{ 72 | A: 3, 73 | B: 3, 74 | C: 3, 75 | } 76 | fit, msg := rs.Fit(o) 77 | assert.True(t, fit) 78 | t.Log(msg) 79 | 80 | head := logicToTree(logic) 81 | err = head.traverseTreeInPostOrderForCalculate(map[int]bool{1: true, 2: true}) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | fmt.Printf("\n%+v\n", head) 86 | } 87 | 88 | func TestRules_Fit5(t *testing.T) { 89 | jsonRules := []byte(`[ 90 | {"op": "=", "key": "A", "val": 3, "id": 1, "msg": "A fail"}, 91 | {"op": ">", "key": "B", "val": 1, "id": 2, "msg": "B fail"}, 92 | {"op": "<", "key": "C", "val": 5, "id": 3, "msg": "C fail"} 93 | ]`) 94 | logic := "1 2" 95 | _, err := NewRulesWithJSONAndLogic(jsonRules, logic) 96 | assert.NotNil(t, err) 97 | } 98 | 99 | func TestRules_Fit6(t *testing.T) { 100 | // ExpectTimeOkRuleJSON 预期时间ok的规则 101 | var ExpectTimeOkRuleJSON = []byte(`[ 102 | {"op": "<", "key": "SecondsAfterOnShelf", "val": 21600, "id": 1, "msg": "新上架<6h"}, 103 | {"op": "=", "key": "CustomerType", "val": "new", "id": 2, "msg": "新客户"}, 104 | {"op": ">", "key": "SecondsBetweenWatchAndOnShelf", "val": 21600, "id": 3, "msg": "需要带看在上架6h以后"}, 105 | {"op": "=", "key": "FinanceAuditPass", "val": 1, "id": 4, "msg": "需要预审通过"}, 106 | {"op": "!=", "key": "IsDealer", "val": 1, "id": 5, "msg": "不能是车商"} 107 | ]`) 108 | // ExpectTimeOkRuleLogic 判断预期时间ok的逻辑 109 | var ExpectTimeOkRuleLogic = "(1 and ((2 and 3) or (2 and 4 and 5) or not 2)) or not 1" 110 | rule, err := NewRulesWithJSONAndLogic(ExpectTimeOkRuleJSON, ExpectTimeOkRuleLogic) 111 | if err != nil { 112 | t.Error(err) 113 | } 114 | t.Log(rule) 115 | 116 | // wrap data 117 | type A struct { 118 | SecondsAfterOnShelf int 119 | CustomerType string 120 | SecondsBetweenWatchAndOnShelf int 121 | FinanceAuditPass int 122 | IsDealer int 123 | } 124 | a := &A{ 125 | SecondsAfterOnShelf: 2160, 126 | CustomerType: "new", 127 | SecondsBetweenWatchAndOnShelf: 2160, 128 | FinanceAuditPass: 0, 129 | IsDealer: 1, 130 | } 131 | 132 | fit, msg := rule.Fit(a) 133 | t.Log(fit) 134 | t.Log(msg) 135 | assert.False(t, fit) 136 | assert.Equal(t, "需要带看在上架6h以后", msg[3]) 137 | } 138 | 139 | func TestRules_Fit7(t *testing.T) { 140 | var jsonIn = []byte(`[ 141 | {"op": "@", "key": "A", "val": "11, 2, 3, 1", "id": 1, "msg": "error 1"}, 142 | {"op": "!@", "key": "B", "val": "11, 2, 3, 1", "id": 2, "msg": "error 2"}, 143 | {"op": "@", "key": "C", "val": "11, 2, 3, 1", "id": 3, "msg": "error 3"}, 144 | {"op": "!@", "key": "D", "val": "11, 2, 3, 1", "id": 4, "msg": "error 4"}, 145 | {"op": "@", "key": "E", "val": "11, 2, 3, 1, 88.1", "id": 5, "msg": "error 5"}, 146 | {"op": "@", "key": "F", "val": "11, 2, 3, 1, 88.1", "id": 6, "msg": "error 6"}, 147 | {"op": "@", "key": "G", "val": "11, 2, 3, 1, 88.1, 0.001", "id": 7, "msg": "error 7"}, 148 | {"op": "@", "key": "H", "val": "11, 2, 3, 1, 88.1, 0.001, ab c", "id": 8, "msg": "error 8"} 149 | ]`) 150 | r, err := NewRulesWithJSONAndLogic(jsonIn, "") 151 | if err != nil { 152 | t.Error(err) 153 | } 154 | 155 | // to fit 156 | type obj struct { 157 | A string 158 | B string 159 | C int 160 | D int32 161 | E float32 162 | F float64 163 | G float64 164 | H string 165 | } 166 | o := &obj{ 167 | A: "3", 168 | B: "4", 169 | C: 1, 170 | D: 8, 171 | E: 88.1, 172 | F: 88.12, 173 | G: 1e-3, 174 | H: "ab c", 175 | } 176 | fit, msg := r.Fit(o) 177 | t.Logf("result: %v", fit) 178 | t.Logf("msg: %+v", msg) 179 | assert.False(t, fit) 180 | assert.Equal(t, "error 6", msg[6]) 181 | } 182 | 183 | func TestGetRuleIDsByLogicExpression(t *testing.T) { 184 | logic := "1 and (2 or (4or not5and2or1))" 185 | ids, err := GetRuleIDsByLogicExpression(logic) 186 | if err != nil { 187 | t.Error(err) 188 | } 189 | t.Log(ids) 190 | assert.Equal(t, []int{1, 2, 4, 5}, ids) 191 | } 192 | 193 | func TestRules_Fit8(t *testing.T) { 194 | jsonRules := []byte(`[ 195 | {"op": "=", "key": "A", "val": 3, "id": 1, "msg": "A fail"}, 196 | {"op": ">", "key": "B", "val": 1, "id": 2, "msg": "B fail"}, 197 | {"op": "<", "key": "C", "val": 5, "id": 3, "msg": "C fail"} 198 | ]`) 199 | logic := "" 200 | rs, err := NewRulesWithJSONAndLogic(jsonRules, logic) 201 | if err != nil { 202 | t.Error(err) 203 | } 204 | type Obj struct { 205 | A int 206 | B int 207 | C int 208 | } 209 | o := &Obj{ 210 | A: 3, 211 | B: 3, 212 | C: 3, 213 | } 214 | fit, msg := rs.Fit(o) 215 | assert.True(t, fit) 216 | assert.Equal(t, 3, len(msg)) 217 | 218 | logic = "1 or 2 or 3 or (2and3)" 219 | o = &Obj{ 220 | A: 3, 221 | B: 1, 222 | C: 7, 223 | } 224 | rs, err = NewRulesWithJSONAndLogic(jsonRules, logic) 225 | if err != nil { 226 | t.Error(err) 227 | } 228 | fit, msg = rs.Fit(o) 229 | assert.True(t, fit) 230 | assert.Equal(t, "A fail", msg[1]) 231 | } 232 | 233 | func TestRules_Fit9(t *testing.T) { 234 | jsonRules := []byte(`[ 235 | {"op": "<<", "key": "A", "val": "(2.99,3]", "id": 1, "msg": "A"}, 236 | {"op": "between", "key": "B", "val": "(1, 3.1)", "id": 2, "msg": "B"}, 237 | {"op": "<<", "key": "C", "val": "[, 6]", "id": 3, "msg": "C"}, 238 | {"op": "between", "key": "D", "val": "(-11,-2]", "id": 4, "msg": "D"} 239 | ]`) 240 | type Obj struct { 241 | A int 242 | B int 243 | C int 244 | D int 245 | } 246 | logic := "1 and 2 and 3 and 4" 247 | o := &Obj{ 248 | A: 3, 249 | B: 3, 250 | C: 3, 251 | D: -3, 252 | } 253 | rs, err := NewRulesWithJSONAndLogic(jsonRules, logic) 254 | if err != nil { 255 | t.Error(err) 256 | } 257 | fit, msg := rs.Fit(o) 258 | t.Log(fit) 259 | t.Log(msg) 260 | assert.True(t, fit) 261 | mapExpected := map[int]string{1: "A", 2: "B", 3: "C", 4: "D"} 262 | assert.Equal(t, mapExpected, msg) 263 | } 264 | 265 | func TestRulesList_Fit(t *testing.T) { 266 | jsonRules := []byte(`[ 267 | {"op": "<<", "key": "A", "val": "(2.99,3]", "id": 1, "msg": "A"}, 268 | {"op": "between", "key": "B", "val": "(1, 3.1)", "id": 2, "msg": "B"}, 269 | {"op": "<<", "key": "C", "val": "[, 6]", "id": 3, "msg": "C"}, 270 | {"op": "between", "key": "D", "val": "(-11,-2]", "id": 4, "msg": "D"} 271 | ]`) 272 | type Obj struct { 273 | A int 274 | B int 275 | C int 276 | D int 277 | } 278 | logic := "1 and 2 and 3 and 4" 279 | o := &Obj{ 280 | A: 3, 281 | B: 3, 282 | C: 3, 283 | D: -3, 284 | } 285 | rs, err := NewRulesWithJSONAndLogic(jsonRules, logic) 286 | if err != nil { 287 | t.Error(err) 288 | } 289 | 290 | rst := &RulesList{ 291 | RulesList: []*Rules{rs}, 292 | } 293 | result := rst.Fit(o) 294 | assert.NotNil(t, result) 295 | } 296 | -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | // PatternNumber 纯数字的正则表达式 4 | const PatternNumber = "^\\d*$" 5 | 6 | // Space 空格 7 | const Space = " " 8 | 9 | // EmptyStr 空串 10 | const EmptyStr = "" 11 | 12 | // EmptyFloat64 空数字 13 | const EmptyFloat64 = 0 14 | -------------------------------------------------------------------------------- /core.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "math/rand" 7 | "reflect" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | "math" 13 | ) 14 | 15 | func validLogic(logic string) (string, error) { 16 | formatLogic := formatLogicExpression(logic) 17 | if formatLogic == Space || formatLogic == EmptyStr { 18 | return EmptyStr, nil 19 | } 20 | // validate the formatLogic string 21 | // 1. only contain legal symbol 22 | isValidSymbol := isFormatLogicExpressionAllValidSymbol(formatLogic) 23 | if !isValidSymbol { 24 | return EmptyStr, errors.New("invalid logic expression: invalid symbol") 25 | } 26 | 27 | // 2. check logic expression by trying to calculate result with random bool 28 | err := tryToCalculateResultByFormatLogicExpressionWithRandomProbe(formatLogic) 29 | if err != nil { 30 | return EmptyStr, errors.New("invalid logic expression: can not calculate") 31 | } 32 | return formatLogic, nil 33 | } 34 | 35 | func injectLogic(rules *Rules, logic string) (*Rules, error) { 36 | formatLogic, err := validLogic(logic) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if formatLogic == EmptyStr { 41 | return rules, nil 42 | } 43 | 44 | // all ids in logic string must be in rules ids 45 | isValidIds := isFormatLogicExpressionAllIdsExist(formatLogic, rules) 46 | if !isValidIds { 47 | return nil, errors.New("invalid logic expression: invalid id") 48 | } 49 | rules.Logic = formatLogic 50 | 51 | return rules, nil 52 | } 53 | 54 | func injectExtractInfo(rules *Rules, extractInfo map[string]string) *Rules { 55 | if name, ok := extractInfo["name"]; ok { 56 | rules.Name = name 57 | } 58 | if msg, ok := extractInfo["msg"]; ok { 59 | rules.Msg = msg 60 | } 61 | return rules 62 | } 63 | 64 | func newRulesWithJSON(jsonStr []byte) (*Rules, error) { 65 | var rules []*Rule 66 | err := json.Unmarshal(jsonStr, &rules) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return newRulesWithArray(rules), nil 71 | } 72 | 73 | func newRulesWithArray(rules []*Rule) *Rules { 74 | // give rule an id 75 | var maxID = 1 76 | for _, rule := range rules { 77 | if rule.ID > maxID { 78 | maxID = rule.ID 79 | } 80 | } 81 | for index := range rules { 82 | if rules[index].ID == 0 { 83 | maxID++ 84 | rules[index].ID = maxID 85 | } 86 | } 87 | return &Rules{ 88 | Rules: rules, 89 | } 90 | } 91 | 92 | func (rs *Rules) fitWithMapInFact(o map[string]interface{}) (bool, map[int]string, map[int]interface{}) { 93 | var results = make(map[int]bool) 94 | var tips = make(map[int]string) 95 | var values = make(map[int]interface{}) 96 | var hasLogic = false 97 | var allRuleIDs []int 98 | if rs.Logic != EmptyStr { 99 | hasLogic = true 100 | } 101 | for _, rule := range rs.Rules { 102 | v := pluck(rule.Key, o) 103 | if v != nil && rule.Val != nil { 104 | typeV := reflect.TypeOf(v) 105 | typeR := reflect.TypeOf(rule.Val) 106 | if !typeV.Comparable() || !typeR.Comparable() { 107 | return false, nil, nil 108 | } 109 | } 110 | values[rule.ID] = v 111 | 112 | flag := rule.fit(v) 113 | results[rule.ID] = flag 114 | if !flag { 115 | // fit false, record msg, for no logic expression usage 116 | tips[rule.ID] = rule.Msg 117 | } 118 | allRuleIDs = append(allRuleIDs, rule.ID) 119 | } 120 | // compute result by considering logic 121 | 122 | if !hasLogic { 123 | for _, flag := range results { 124 | if !flag { 125 | return false, tips, values 126 | } 127 | } 128 | return true, rs.getTipsByRuleIDs(allRuleIDs), values 129 | } 130 | answer, ruleIDs, err := rs.calculateExpressionByTree(results) 131 | // tree can return fail reasons in fact 132 | tips = rs.getTipsByRuleIDs(ruleIDs) 133 | if err != nil { 134 | return false, nil, values 135 | } 136 | return answer, tips, values 137 | } 138 | 139 | func (rs *Rules) getTipsByRuleIDs(ids []int) map[int]string { 140 | var tips = make(map[int]string) 141 | var allTips = make(map[int]string) 142 | for _, rule := range rs.Rules { 143 | allTips[rule.ID] = rule.Msg 144 | } 145 | for _, id := range ids { 146 | tips[id] = allTips[id] 147 | } 148 | return tips 149 | } 150 | 151 | func (r *Rule) fit(v interface{}) bool { 152 | op := r.Op 153 | // judge if need convert to uniform type 154 | var ok bool 155 | // index-0 actual, index-1 expect 156 | var pairStr = make([]string, 2) 157 | var pairNum = make([]float64, 2) 158 | var isStr, isNum, isObjStr, isRuleStr bool 159 | pairStr[0], ok = v.(string) 160 | if !ok { 161 | pairNum[0] = formatNumber(v) 162 | isStr = false 163 | isNum = true 164 | isObjStr = false 165 | } else { 166 | isStr = true 167 | isNum = false 168 | isObjStr = true 169 | } 170 | pairStr[1], ok = r.Val.(string) 171 | if !ok { 172 | pairNum[1] = formatNumber(r.Val) 173 | isStr = false 174 | isRuleStr = false 175 | } else { 176 | isNum = false 177 | isRuleStr = true 178 | } 179 | 180 | var flagOpIn bool 181 | // if in || nin 182 | if op == "@" || op == "in" || op == "!@" || op == "nin" || op == "<<" || op == "between" { 183 | flagOpIn = true 184 | if !isObjStr && isRuleStr { 185 | pairStr[0] = strconv.FormatFloat(pairNum[0], 'f', -1, 64) 186 | } 187 | } 188 | 189 | // if types different, ignore in & nin 190 | if !isStr && !isNum && !flagOpIn { 191 | return false 192 | } 193 | 194 | switch op { 195 | case "=", "eq": 196 | if isNum { 197 | return pairNum[0] == pairNum[1] 198 | } 199 | if isStr { 200 | return pairStr[0] == pairStr[1] 201 | } 202 | return false 203 | case ">", "gt": 204 | if isNum { 205 | return pairNum[0] > pairNum[1] 206 | } 207 | if isStr { 208 | return pairStr[0] > pairStr[1] 209 | } 210 | return false 211 | case "<", "lt": 212 | if isNum { 213 | return pairNum[0] < pairNum[1] 214 | } 215 | if isStr { 216 | return pairStr[0] < pairStr[1] 217 | } 218 | return false 219 | case ">=", "gte": 220 | if isNum { 221 | return pairNum[0] >= pairNum[1] 222 | } 223 | if isStr { 224 | return pairStr[0] >= pairStr[1] 225 | } 226 | return false 227 | case "<=", "lte": 228 | if isNum { 229 | return pairNum[0] <= pairNum[1] 230 | } 231 | if isStr { 232 | return pairStr[0] <= pairStr[1] 233 | } 234 | return false 235 | case "!=", "neq": 236 | if isNum { 237 | return pairNum[0] != pairNum[1] 238 | } 239 | if isStr { 240 | return pairStr[0] != pairStr[1] 241 | } 242 | return false 243 | case "@", "in": 244 | return isIn(pairStr[0], pairStr[1], !isObjStr) 245 | case "!@", "nin": 246 | return !isIn(pairStr[0], pairStr[1], !isObjStr) 247 | case "^$", "regex": 248 | return checkRegex(pairStr[1], pairStr[0]) 249 | case "0", "empty": 250 | return v == nil 251 | case "1", "nempty": 252 | return v != nil 253 | case "<<", "between": 254 | return isBetween(pairNum[0], pairStr[1]) 255 | case "@@", "intersect": 256 | return isIntersect(pairStr[1], pairStr[0]) 257 | default: 258 | return false 259 | } 260 | } 261 | 262 | func pluck(key string, o map[string]interface{}) interface{} { 263 | if o == nil || key == EmptyStr { 264 | return nil 265 | } 266 | paths := strings.Split(key, ".") 267 | var ok bool 268 | for index, step := range paths { 269 | // last step is object key 270 | if index == len(paths)-1 { 271 | return o[step] 272 | } 273 | // explore deeper 274 | if o, ok = o[step].(map[string]interface{}); !ok { 275 | return nil 276 | } 277 | } 278 | return nil 279 | } 280 | 281 | func formatNumber(v interface{}) float64 { 282 | switch t := v.(type) { 283 | case uint: 284 | return float64(t) 285 | case uint8: 286 | return float64(t) 287 | case uint16: 288 | return float64(t) 289 | case uint32: 290 | return float64(t) 291 | case uint64: 292 | return float64(t) 293 | case int: 294 | return float64(t) 295 | case int8: 296 | return float64(t) 297 | case int16: 298 | return float64(t) 299 | case int32: 300 | return float64(t) 301 | case int64: 302 | return float64(t) 303 | case float32: 304 | return float64(t) 305 | case float64: 306 | return t 307 | default: 308 | return 0 309 | } 310 | } 311 | 312 | func checkRegex(pattern, o string) bool { 313 | regex, err := regexp.Compile(pattern) 314 | if err != nil { 315 | return false 316 | } 317 | return regex.MatchString(o) 318 | } 319 | 320 | func formatLogicExpression(strRawExpr string) string { 321 | var flagPre, flagNow string 322 | strBracket := "bracket" 323 | strSpace := "space" 324 | strNotSpace := "notSpace" 325 | strOrigin := strings.ToLower(strRawExpr) 326 | runesPretty := make([]rune, 0) 327 | 328 | for _, c := range strOrigin { 329 | if c <= '9' && c >= '0' { 330 | flagNow = "num" 331 | } else if c <= 'z' && c >= 'a' { 332 | flagNow = "char" 333 | } else if c == '(' || c == ')' { 334 | flagNow = strBracket 335 | } else { 336 | flagNow = flagPre 337 | } 338 | if flagNow != flagPre || flagNow == strBracket && flagPre == strBracket { 339 | // should insert space here 340 | runesPretty = append(runesPretty, []rune(Space)[0]) 341 | } 342 | runesPretty = append(runesPretty, c) 343 | flagPre = flagNow 344 | } 345 | // remove redundant space 346 | flagPre = strNotSpace 347 | runesTrim := make([]rune, 0) 348 | for _, c := range runesPretty { 349 | if c == []rune(Space)[0] { 350 | flagNow = strSpace 351 | } else { 352 | flagNow = strNotSpace 353 | } 354 | if flagNow == strSpace && flagPre == strSpace { 355 | // continuous space 356 | continue 357 | } else { 358 | runesTrim = append(runesTrim, c) 359 | } 360 | flagPre = flagNow 361 | } 362 | strPrettyTrim := string(runesTrim) 363 | strPrettyTrim = strings.Trim(strPrettyTrim, Space) 364 | 365 | return strPrettyTrim 366 | } 367 | 368 | func isFormatLogicExpressionAllValidSymbol(strFormatLogic string) bool { 369 | listSymbol := strings.Split(strFormatLogic, Space) 370 | for _, symbol := range listSymbol { 371 | flag := false 372 | regex, err := regexp.Compile(PatternNumber) 373 | if err != nil { 374 | return false 375 | } 376 | if regex.MatchString(symbol) { 377 | // is number ok 378 | continue 379 | } 380 | for _, op := range ValidOperators { 381 | if op == symbol { 382 | // is operator ok 383 | flag = true 384 | } 385 | } 386 | for _, v := range []string{"(", ")"} { 387 | if v == symbol { 388 | // is bracket ok 389 | flag = true 390 | } 391 | } 392 | if !flag { 393 | return false 394 | } 395 | } 396 | return true 397 | } 398 | 399 | func isFormatLogicExpressionAllIdsExist(strFormatLogic string, rules *Rules) bool { 400 | mapExistIds := make(map[string]bool) 401 | for _, eachRule := range rules.Rules { 402 | mapExistIds[strconv.Itoa(eachRule.ID)] = true 403 | } 404 | listSymbol := strings.Split(strFormatLogic, Space) 405 | regex, err := regexp.Compile(PatternNumber) 406 | if err != nil { 407 | return false 408 | } 409 | for _, symbol := range listSymbol { 410 | if regex.MatchString(symbol) { 411 | // is id, check it 412 | if _, ok := mapExistIds[symbol]; ok { 413 | continue 414 | } else { 415 | return false 416 | } 417 | } 418 | } 419 | return true 420 | } 421 | 422 | func tryToCalculateResultByFormatLogicExpressionWithRandomProbe(strFormatLogic string) error { 423 | listSymbol := strings.Split(strFormatLogic, Space) 424 | regex, err := regexp.Compile(PatternNumber) 425 | if err != nil { 426 | return err 427 | } 428 | // random probe 429 | mapProbe := make(map[int]bool) 430 | for _, symbol := range listSymbol { 431 | if regex.MatchString(symbol) { 432 | id, iErr := strconv.Atoi(symbol) 433 | if iErr != nil { 434 | return iErr 435 | } 436 | randomInt := rand.Intn(10) 437 | randomBool := randomInt < 5 438 | mapProbe[id] = randomBool 439 | } 440 | } 441 | // calculate still use reverse_polish_notation 442 | r := &Rules{} 443 | _, err = r.calculateExpression(strFormatLogic, mapProbe) 444 | return err 445 | } 446 | 447 | func numOfOperandInLogic(op string) int8 { 448 | mapOperand := map[string]int8{"or": 2, "and": 2, "not": 1} 449 | return mapOperand[op] 450 | } 451 | 452 | func computeOneInLogic(op string, v []bool) (bool, error) { 453 | switch op { 454 | case "or": 455 | return v[0] || v[1], nil 456 | case "and": 457 | return v[0] && v[1], nil 458 | case "not": 459 | return !v[0], nil 460 | default: 461 | return false, errors.New("unrecognized op") 462 | } 463 | } 464 | 465 | func isIn(needle, haystack string, isNeedleNum bool) bool { 466 | // get number of needle 467 | var iNum float64 468 | var err error 469 | if isNeedleNum { 470 | if iNum, err = strconv.ParseFloat(needle, 64); err != nil { 471 | return false 472 | } 473 | } 474 | // compatible to "1, 2, 3" and "1,2,3" 475 | li := strings.Split(haystack, ",") 476 | for _, o := range li { 477 | trimO := strings.TrimLeft(o, " ") 478 | if isNeedleNum { 479 | oNum, err := strconv.ParseFloat(trimO, 64) 480 | if err != nil { 481 | continue 482 | } 483 | if math.Abs(iNum-oNum) < 1E-5 { 484 | // 考虑浮点精度问题 485 | return true 486 | } 487 | } else if needle == trimO { 488 | return true 489 | } 490 | } 491 | return false 492 | } 493 | 494 | func isIntersect(objStr string, ruleStr string) bool { 495 | // compatible to "1, 2, 3" and "1,2,3" 496 | vl := strings.Split(objStr, ",") 497 | li := strings.Split(ruleStr, ",") 498 | for _, o := range li { 499 | trimO := strings.Trim(o, " ") 500 | for _, v := range vl { 501 | trimV := strings.Trim(v, " ") 502 | if trimV == trimO { 503 | return true 504 | } 505 | } 506 | } 507 | return false 508 | } 509 | 510 | func isBetween(obj float64, scope string) bool { 511 | scope = strings.Trim(scope, " ") 512 | var equalLeft, equalRight bool 513 | // [] 双闭区间 514 | result := regexp.MustCompile("^\\[ *(-?\\d*.?\\d*) *, *(-?\\d*.?\\d*) *]$").FindStringSubmatch(scope) 515 | if len(result) > 2 { 516 | equalLeft = true 517 | equalRight = true 518 | return calculateBetween(obj, result, equalLeft, equalRight) 519 | } 520 | // [) 左闭右开区间 521 | result = regexp.MustCompile("^\\[ *(-?\\d*.?\\d*) *, *(-?\\d*.?\\d*) *\\)$").FindStringSubmatch(scope) 522 | if len(result) > 2 { 523 | equalLeft = true 524 | equalRight = false 525 | return calculateBetween(obj, result, equalLeft, equalRight) 526 | } 527 | // (] 左开右闭区间 528 | result = regexp.MustCompile("^\\( *(-?\\d*.?\\d*) *, *(-?\\d*.?\\d*) *]$").FindStringSubmatch(scope) 529 | if len(result) > 2 { 530 | equalLeft = false 531 | equalRight = true 532 | return calculateBetween(obj, result, equalLeft, equalRight) 533 | } 534 | // () 双开区间 535 | result = regexp.MustCompile("^\\( *(-?\\d*.?\\d*) *, *(-?\\d*.?\\d*) *\\)$").FindStringSubmatch(scope) 536 | if len(result) > 2 { 537 | equalLeft = false 538 | equalRight = false 539 | return calculateBetween(obj, result, equalLeft, equalRight) 540 | } 541 | return false 542 | } 543 | 544 | func calculateBetween(obj float64, result []string, equalLeft, equalRight bool) bool { 545 | var hasLeft, hasRight bool 546 | var left, right float64 547 | var err error 548 | if result[1] != "" { 549 | hasLeft = true 550 | left, err = strconv.ParseFloat(result[1], 64) 551 | if err != nil { 552 | return false 553 | } 554 | } 555 | if result[2] != "" { 556 | hasRight = true 557 | right, err = strconv.ParseFloat(result[2], 64) 558 | if err != nil { 559 | return false 560 | } 561 | } 562 | // calculate 563 | if !hasLeft && !hasRight { 564 | return false 565 | } 566 | flag := true 567 | if hasLeft { 568 | if equalLeft { 569 | flag = flag && obj >= left 570 | } else { 571 | flag = flag && obj > left 572 | } 573 | } 574 | if hasRight { 575 | if equalRight { 576 | flag = flag && obj <= right 577 | } else { 578 | flag = flag && obj < right 579 | } 580 | } 581 | return flag 582 | } 583 | -------------------------------------------------------------------------------- /core_test.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewRulesWithJSON(t *testing.T) { 10 | jsonStr := []byte(`[{"op": "=", "key": "status", "val": 1}]`) 11 | rules, err := newRulesWithJSON(jsonStr) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | rule := &Rule{ 16 | Op: "=", 17 | Key: "status", 18 | Val: float64(1), 19 | ID: 2, 20 | } 21 | assert.Equal(t, rule, rules.Rules[0]) 22 | } 23 | 24 | func TestPluck(t *testing.T) { 25 | obj := map[string]interface{}{"op": map[string]interface{}{"deep": 1}, "key": "status", "val": 1} 26 | result := pluck("op.deep", obj) 27 | assert.Equal(t, 1, result) 28 | } 29 | 30 | func TestRule_Fit(t *testing.T) { 31 | rule := &Rule{ 32 | Op: "=", 33 | Key: "status", 34 | Val: 0, 35 | } 36 | result := rule.fit(0) 37 | assert.True(t, result) 38 | } 39 | 40 | func TestRule_Fit1(t *testing.T) { 41 | rule := &Rule{ 42 | Op: "@@", 43 | Key: "status", 44 | Val: "bn,gh,kl", 45 | } 46 | result := rule.fit("as,df,gh") 47 | assert.True(t, result) 48 | } 49 | 50 | func TestRules_Fit(t *testing.T) { 51 | jsonStr := []byte(`[ 52 | {"op": "@", "key": "status", "val": "abcd", "id": 13, "msg": "状态不对"}, 53 | {"op": "=", "key": "name", "val": "peter", "id": 15}, 54 | {"op": ">=", "key": "data.deep", "val": 1, "id": 17, "msg": "deep 数值不对"} 55 | ]`) 56 | rules, err := newRulesWithJSON(jsonStr) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | rules.Logic = "( 15 or 13 ) and 17 and not 13" 61 | 62 | obj := map[string]interface{}{"data": map[string]interface{}{"deep": 1}, "name": "peter", "status": "abc"} 63 | result, _ := rules.FitWithMap(obj) 64 | assert.True(t, result) 65 | } 66 | 67 | func TestRules_Fit2(t *testing.T) { 68 | jsonStr := []byte(`[ 69 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 70 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 71 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 72 | ]`) 73 | rules, err := newRulesWithJSON(jsonStr) 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | rules.Logic = "( 15 or 13 ) and 17 and not 13" 78 | type B struct { 79 | Deep int 80 | } 81 | type A struct { 82 | Data B 83 | Name string 84 | Status string 85 | } 86 | obj := A{ 87 | Data: B{ 88 | Deep: 1, 89 | }, 90 | Name: "peter", 91 | Status: "abc", 92 | } 93 | result, _ := rules.Fit(obj) 94 | assert.True(t, result) 95 | } 96 | 97 | func TestNewRulesWithJSONAndLogic(t *testing.T) { 98 | jsonStr := []byte(`[ 99 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 100 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 101 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 102 | ]`) 103 | logic := " 13 and (15 )" 104 | rules, err := NewRulesWithJSONAndLogic(jsonStr, logic) 105 | if err != nil { 106 | t.Error(err) 107 | } 108 | assert.Equal(t, "13 and ( 15 )", rules.Logic) 109 | } 110 | 111 | func TestNewRulesWithJSONAndLogic2(t *testing.T) { 112 | jsonStr := []byte(`[ 113 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 114 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 115 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 116 | ]`) 117 | logic := " 13 or (15 )" 118 | rules, err := NewRulesWithJSONAndLogic(jsonStr, logic) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | assert.Equal(t, "13 or ( 15 )", rules.Logic) 123 | } 124 | 125 | func TestNewRulesWithJSONAndLogic3(t *testing.T) { 126 | jsonStr := []byte(`[ 127 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 128 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 129 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 130 | ]`) 131 | logic := " 13 and (15or13 )" 132 | rules, err := NewRulesWithJSONAndLogic(jsonStr, logic) 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | assert.Equal(t, "13 and ( 15 or 13 )", rules.Logic) 137 | } 138 | 139 | func TestNewRulesWithArrayAndLogic(t *testing.T) { 140 | jsonStr := []byte(`[ 141 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 142 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 143 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 144 | ]`) 145 | logic := " 13 and (15or13 )" 146 | rules, err := NewRulesWithJSONAndLogic(jsonStr, logic) 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | rules, err = NewRulesWithArrayAndLogic(rules.Rules, logic) 151 | if err != nil { 152 | t.Error(err) 153 | } 154 | assert.Equal(t, "13 and ( 15 or 13 )", rules.Logic) 155 | } 156 | 157 | func TestNewRulesWithJSONAndLogicAndInfo(t *testing.T) { 158 | jsonStr := []byte(`[ 159 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 160 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 161 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 162 | ]`) 163 | logic := " 13 and (15or13 )" 164 | extractInfo := map[string]string{ 165 | "name": "名称", 166 | "msg": "提示", 167 | } 168 | rules, err := NewRulesWithJSONAndLogicAndInfo(jsonStr, logic, extractInfo) 169 | if err != nil { 170 | t.Error(err) 171 | } 172 | assert.Equal(t, "名称", rules.Name) 173 | } 174 | 175 | func TestNewRulesWithArrayAndLogicAndInfo(t *testing.T) { 176 | jsonStr := []byte(`[ 177 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 178 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 179 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 180 | ]`) 181 | logic := " 13 and (15or13 )" 182 | extractInfo := map[string]string{ 183 | "name": "名称", 184 | "msg": "提示", 185 | } 186 | rules, err := NewRulesWithJSONAndLogic(jsonStr, logic) 187 | if err != nil { 188 | t.Error(err) 189 | } 190 | rules, err = NewRulesWithArrayAndLogicAndInfo(rules.Rules, logic, extractInfo) 191 | if err != nil { 192 | t.Error(err) 193 | } 194 | assert.Equal(t, "提示", rules.Msg) 195 | } 196 | 197 | func TestNewRulesSet(t *testing.T) { 198 | jsonStr := []byte(`[ 199 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 200 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 201 | {"op": ">=", "key": "Data.Deep", "val": 1, "id": 17} 202 | ]`) 203 | logic := " 13 and (15or13 )" 204 | extractInfo := map[string]string{ 205 | "name": "", 206 | "msg": "提示", 207 | } 208 | rules, err := NewRulesWithJSONAndLogic(jsonStr, logic) 209 | if err != nil { 210 | t.Error(err) 211 | } 212 | rules, err = NewRulesWithArrayAndLogicAndInfo(rules.Rules, logic, extractInfo) 213 | if err != nil { 214 | t.Error(err) 215 | } 216 | 217 | rulesSet := NewRulesList([]*Rules{rules}, extractInfo) 218 | assert.Equal(t, rulesSet.RulesList[0].Name, "1") 219 | } 220 | 221 | func TestRulesSet_FitSetWithMap(t *testing.T) { 222 | jsonStr := []byte(`[ 223 | {"op": "@", "key": "Status", "val": "abcd", "id": 13}, 224 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 225 | {"op": ">=", "key": "Key", "val": 1, "id": 17} 226 | ]`) 227 | logic := " " 228 | extractInfo := map[string]string{ 229 | "name": "", 230 | "msg": "提示", 231 | } 232 | rules, err := NewRulesWithJSONAndLogicAndInfo(jsonStr, logic, extractInfo) 233 | if err != nil { 234 | t.Error(err) 235 | } 236 | 237 | rulesSet := NewRulesList([]*Rules{rules}, extractInfo) 238 | 239 | obj := map[string]interface{}{"Name": "peter", "Status": "abcd", "Key": 0} 240 | fitRules, _ := rules.FitWithMap(obj) 241 | assert.False(t, fitRules) 242 | 243 | result := rulesSet.FitWithMap(obj) 244 | assert.Nil(t, result) 245 | } 246 | 247 | func TestRules_FitWithMap(t *testing.T) { 248 | jsonStr := []byte(`[ 249 | {"op": "=", "key": "Status", "val": "abcd", "id": 13}, 250 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 251 | {"op": ">=", "key": "Key", "val": 1, "id": 17} 252 | ]`) 253 | logic := "13 and 15" 254 | extractInfo := map[string]string{ 255 | "name": "", 256 | "msg": "提示", 257 | } 258 | rules, err := NewRulesWithJSONAndLogicAndInfo(jsonStr, logic, extractInfo) 259 | if err != nil { 260 | t.Error(err) 261 | } 262 | objMap := map[string]interface{}{"Status": "abcd"} 263 | fit, _ := rules.FitWithMap(objMap) 264 | assert.False(t, fit) 265 | } 266 | 267 | func TestRules_FitWithMapAskVal(t *testing.T) { 268 | jsonStr := []byte(`[ 269 | {"op": "=", "key": "Status", "val": "abcd", "id": 13}, 270 | {"op": "=", "key": "Name", "val": "peter", "id": 15}, 271 | {"op": ">=", "key": "Key", "val": 1, "id": 17} 272 | ]`) 273 | logic := "13" 274 | extractInfo := map[string]string{ 275 | "name": "", 276 | "msg": "提示", 277 | } 278 | rules, err := NewRulesWithJSONAndLogicAndInfo(jsonStr, logic, extractInfo) 279 | if err != nil { 280 | t.Error(err) 281 | } 282 | objMap := map[string]interface{}{"Status": "abcd"} 283 | fit, _, val := rules.FitWithMapAskVal(objMap) 284 | valExpect := map[int]interface{}{17: nil, 13: "abcd", 15: nil} 285 | assert.True(t, fit) 286 | assert.Equal(t, valExpect, val) 287 | } 288 | 289 | func TestValidLogic(t *testing.T) { 290 | logic := " 1 and2or(3 or not4) " 291 | formatLogic, err := validLogic(logic) 292 | if err != nil { 293 | t.Error(err) 294 | } 295 | assert.Equal(t, "1 and 2 or ( 3 or not 4 )", formatLogic) 296 | } 297 | -------------------------------------------------------------------------------- /reverse_polish.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "container/list" 5 | "errors" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func (*Rules) calculateExpression(expr string, values map[int]bool) (bool, error) { 12 | listExpr := strings.Split(expr, " ") 13 | stackNum := list.New() 14 | stackOp := list.New() 15 | 16 | // regex 17 | patternNum := "^\\d*$" 18 | regexNum, err := regexp.Compile(patternNum) 19 | if err != nil { 20 | return false, err 21 | } 22 | 23 | for _, c := range listExpr { 24 | // judge if number 25 | if regexNum.MatchString(c) { 26 | stackNum.PushBack(c) 27 | } else { 28 | var lastOp string 29 | var ok bool 30 | if stackOp.Back() != nil { 31 | lastOpRaw := stackOp.Back().Value 32 | lastOp, ok = lastOpRaw.(string) 33 | if !ok { 34 | return false, errors.New("error type of operator") 35 | } 36 | } 37 | if isOpBiggerInLogic(c, lastOp) || c == "(" { 38 | // if ( meet ), should delete ( 39 | if lastOp == "(" && c == ")" { 40 | stackOp.Remove(stackOp.Back()) 41 | } else { 42 | stackOp.PushBack(c) 43 | } 44 | } else { 45 | iterMax := stackOp.Len() 46 | for i := 0; i < iterMax; i++ { 47 | lastOpRaw := stackOp.Back() 48 | if lastOpRaw == nil { 49 | break 50 | } 51 | lastOp, ok := lastOpRaw.Value.(string) 52 | if !ok { 53 | return false, errors.New("error type of operator") 54 | } 55 | if isOpBiggerInLogic(c, lastOp) { 56 | break 57 | } else { 58 | stackNum.PushBack(lastOpRaw.Value) 59 | stackOp.Remove(lastOpRaw) 60 | } 61 | } 62 | if c == ")" { 63 | // delete "(" 64 | if stackOp.Back() == nil { 65 | return false, errors.New("error expression to calculate: " + expr) 66 | } 67 | if strBracket, ok := stackOp.Back().Value.(string); ok { 68 | if strBracket != "(" { 69 | return false, errors.New("error expression to calculate: " + expr) 70 | } 71 | } 72 | stackOp.Remove(stackOp.Back()) 73 | } else { 74 | stackOp.PushBack(c) 75 | } 76 | } 77 | } 78 | } 79 | 80 | // dump op to num stack 81 | iterMax := stackOp.Len() 82 | for i := 0; i < iterMax; i++ { 83 | if stackOp.Back() == nil { 84 | break 85 | } 86 | // judge stackOp char valid: not ( or ) 87 | if strOp, ok := stackOp.Back().Value.(string); ok { 88 | if strOp == "(" || strOp == ")" { 89 | return false, errors.New("error expression to calculate: " + expr) 90 | } 91 | } else { 92 | return false, errors.New("error expression to calculate: " + expr) 93 | } 94 | 95 | stackNum.PushBack(stackOp.Back().Value) 96 | stackOp.Remove(stackOp.Back()) 97 | } 98 | 99 | // count 100 | iterMax = stackNum.Len() 101 | for i := 0; i < iterMax; i++ { 102 | itemRaw := stackNum.Front().Value 103 | item, ok := itemRaw.(string) 104 | if !ok { 105 | return false, errors.New("error type in stack number") 106 | } 107 | if regexNum.MatchString(item) { 108 | index, err := strconv.Atoi(item) 109 | if err != nil { 110 | return false, err 111 | } 112 | if val, ok := values[index]; ok { 113 | stackOp.PushBack(val) 114 | } else { 115 | return false, errors.New("empty operand value in map: " + item) 116 | } 117 | 118 | } else { 119 | // choose operands and operate 120 | if numOfOperandInLogic(item) == 2 { 121 | operandBRaw := stackOp.Back() 122 | if operandBRaw == nil { 123 | return false, errors.New("error expression to calculate: " + expr) 124 | } 125 | operandB, ok := operandBRaw.Value.(bool) 126 | if !ok { 127 | return false, errors.New("error type of operator") 128 | } 129 | stackOp.Remove(stackOp.Back()) 130 | operandARaw := stackOp.Back() 131 | if operandARaw == nil { 132 | return false, errors.New("error expression to calculate: " + expr) 133 | } 134 | operandA, ok := operandARaw.Value.(bool) 135 | if !ok { 136 | return false, errors.New("error type of operator") 137 | } 138 | stackOp.Remove(stackOp.Back()) 139 | computeOutput, err := computeOneInLogic(item, []bool{operandA, operandB}) 140 | if err != nil { 141 | return false, errors.New("error in one compute") 142 | } 143 | stackOp.PushBack(computeOutput) 144 | } 145 | if numOfOperandInLogic(item) == 1 { 146 | operandBRaw := stackOp.Back() 147 | if operandBRaw == nil { 148 | return false, errors.New("error expression to calculate: " + expr) 149 | } 150 | operandB, ok := operandBRaw.Value.(bool) 151 | if !ok { 152 | return false, errors.New("error type of operator") 153 | } 154 | stackOp.Remove(stackOp.Back()) 155 | computeOutput, err := computeOneInLogic(item, []bool{operandB}) 156 | if err != nil { 157 | return false, errors.New("error in one compute") 158 | } 159 | stackOp.PushBack(computeOutput) 160 | } 161 | } 162 | stackNum.Remove(stackNum.Front()) 163 | } 164 | 165 | if stackOp.Back() == nil || stackOp.Len() != 1 { 166 | return false, errors.New("error expression to calculate: " + expr) 167 | } 168 | result, ok := stackOp.Back().Value.(bool) 169 | if !ok { 170 | return false, errors.New("error type in final result") 171 | } 172 | return result, nil 173 | } 174 | 175 | func isOpBiggerInLogic(obj, base string) bool { 176 | if obj == "" { 177 | return false 178 | } 179 | if base == "" { 180 | return true 181 | } 182 | mapPriority := map[string]int8{"or": 2, "and": 3, "not": 5, "(": 0, ")": 1} 183 | return mapPriority[obj] > mapPriority[base] 184 | } 185 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "math/rand" 11 | 12 | "github.com/satori/go.uuid" 13 | ) 14 | 15 | /** 16 | 利用树来计算规则引擎 17 | 输入:子规则ID和逻辑值map 18 | 输出:规则匹配结果,导致匹配false的子规则ID/导致true的IDs 19 | */ 20 | func (rs *Rules) calculateExpressionByTree(values map[int]bool) (bool, []int, error) { 21 | var ruleIDs []int 22 | head := logicToTree(rs.Logic) 23 | err := head.traverseTreeInPostOrderForCalculate(values) 24 | if err != nil { 25 | return false, nil, err 26 | } 27 | if !head.Computed { 28 | return false, nil, errors.New("didn't count out yet") 29 | } 30 | if !head.Val { 31 | // fail了需要找原因 32 | ruleIDs, err = head.traverseTreeInLayerToFindFailRule(ruleIDs) 33 | if err != nil { 34 | return false, nil, err 35 | } 36 | } else { 37 | // true也返回相应的ruleIDs 38 | ruleIDs, err = head.traverseTreeInLayerToFindSuccessRule(ruleIDs) 39 | if err != nil { 40 | return false, nil, err 41 | } 42 | } 43 | return head.Val, ruleIDs, nil 44 | } 45 | 46 | /** 47 | 将逻辑表达式转化为树,返回树的根节点 48 | */ 49 | func logicToTree(logic string) *Node { 50 | if logic == "" || logic == " " { 51 | return nil 52 | } 53 | var head = &Node{ 54 | Expr: logic, 55 | Should: true, 56 | Blamed: true, 57 | } 58 | head.Leaf = head.isLeaf() 59 | propagateTree(head) 60 | return head 61 | } 62 | 63 | /** 64 | 计算树所有节点值的核心方法 65 | */ 66 | func (node *Node) traverseTreeInPostOrderForCalculate(values map[int]bool) error { 67 | if node == nil { 68 | return nil 69 | } 70 | 71 | children := node.Children 72 | for _, child := range children { 73 | err := child.traverseTreeInPostOrderForCalculate(values) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | if node.Leaf { 79 | // calculate leaf node 80 | ruleID, err := strconv.Atoi(node.Expr) 81 | if err != nil { 82 | return err 83 | } 84 | if val, ok := values[ruleID]; ok { 85 | node.Val = val 86 | node.Computed = true 87 | } else { 88 | return fmt.Errorf("not exist rule_id: %d", ruleID) 89 | } 90 | return nil 91 | } 92 | // calculate not-leaf node by children and their op 93 | op := node.ChildrenOp 94 | tmpVal := node.Children[0].Val 95 | if numOfOperandInLogic(op) == 1 { 96 | node.Val = !tmpVal 97 | node.Computed = true 98 | } else { 99 | var err error 100 | for _, child := range node.Children { 101 | // because a = a and a, a = a or a, so can simply duplicated 102 | tmpVal, err = computeOneInLogic(op, []bool{tmpVal, child.Val}) 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | node.Val = tmpVal 108 | node.Computed = true 109 | } 110 | return nil 111 | } 112 | 113 | /** 114 | 层序遍历获取导致树顶false的叶子节点 115 | */ 116 | func (node *Node) traverseTreeInLayerToFindFailRule(ids []int) ([]int, error) { 117 | var buf []*Node 118 | var i int 119 | buf = append(buf, node) 120 | for { 121 | if i < len(buf) { 122 | if buf[i].Leaf { 123 | if buf[i].isFailNode() { 124 | // 找到了导致失败的叶子节点 125 | ruleID, err := strconv.Atoi(buf[i].Expr) 126 | if err != nil { 127 | return nil, err 128 | } 129 | ids = append(ids, ruleID) 130 | return ids, nil 131 | } 132 | } 133 | if buf[i].Children != nil { 134 | if buf[i].isFailNode() { 135 | // 找到了导致失败的非叶子节点,遍历它即可,所以要清空它后面的所有节点 136 | buf = append(buf[:i+1]) 137 | } 138 | buf = append(buf, buf[i].Children...) 139 | } 140 | i++ 141 | } else { 142 | break 143 | } 144 | } 145 | return ids, nil 146 | } 147 | 148 | /** 149 | 层序遍历获取导致树顶true的叶子节点 150 | */ 151 | func (node *Node) traverseTreeInLayerToFindSuccessRule(ids []int) ([]int, error) { 152 | var buf []*Node 153 | var i int 154 | buf = append(buf, node) 155 | for { 156 | if i < len(buf) { 157 | if buf[i].Leaf { 158 | if buf[i].isSuccessNode() { 159 | // 找到了导致true的叶子节点 160 | ruleID, err := strconv.Atoi(buf[i].Expr) 161 | if err != nil { 162 | return nil, err 163 | } 164 | ids = append(ids, ruleID) 165 | } 166 | } 167 | if buf[i].Children != nil { 168 | buf = append(buf, buf[i].Children...) 169 | } 170 | i++ 171 | } else { 172 | break 173 | } 174 | } 175 | return ids, nil 176 | } 177 | 178 | func (node *Node) isFailNode() bool { 179 | return node.Blamed && node.Computed && node.Should != node.Val 180 | } 181 | 182 | func (node *Node) isSuccessNode() bool { 183 | return node.Computed && node.Should == node.Val 184 | } 185 | 186 | func propagateTree(head *Node) { 187 | children := head.splitExprToChildren() 188 | if children != nil { 189 | head.Children = children 190 | } else { 191 | return 192 | } 193 | for index := range head.Children { 194 | propagateTree(head.Children[index]) 195 | } 196 | } 197 | 198 | func (node *Node) splitExprToChildren() []*Node { 199 | // wrap biggest (***) block 200 | exprWrap, mapReplace := replaceBiggestBracketContent(node.Expr) 201 | // or layer 202 | ors := strings.Split(exprWrap, " or ") 203 | if len(ors) > 1 { 204 | node.ChildrenOp = string(OperatorOr) 205 | return node.shipChildren(ors, mapReplace) 206 | } 207 | // and layer 208 | ands := strings.Split(exprWrap, " and ") 209 | if len(ands) > 1 { 210 | node.ChildrenOp = string(OperatorAnd) 211 | return node.shipChildren(ands, mapReplace) 212 | } 213 | // not layer 214 | not := strings.Split(exprWrap, "not ") 215 | if len(not) > 1 { 216 | node.ChildrenOp = string(OperatorNot) 217 | return node.shipChildren([]string{not[1]}, mapReplace) 218 | } 219 | return nil 220 | } 221 | 222 | func (node *Node) shipChildren(splits []string, mapReplace map[string]string) []*Node { 223 | var children = make([]*Node, 0) 224 | var isFirstChild = true 225 | for _, o := range splits { 226 | for k, v := range mapReplace { 227 | if o == k { 228 | o = mapReplace[k] 229 | } else if strings.Contains(o, k) { 230 | o = strings.Replace(o, k, "( "+v+" )", -1) 231 | } 232 | } 233 | var should bool 234 | var blamed bool 235 | switch node.ChildrenOp { 236 | case string(OperatorAnd): 237 | // 跟父节点 238 | should = node.Should 239 | // and和not的时候所有子树都有责任 240 | blamed = true 241 | case string(OperatorOr): 242 | should = node.Should 243 | // or的时候只有第一个子树有责任 244 | if isFirstChild { 245 | blamed = true 246 | } 247 | case string(OperatorNot): 248 | should = !node.Should 249 | // and和not的时候所有子树都有责任 250 | blamed = true 251 | } 252 | // 父节点无责任,子树也无责任 253 | blamed = node.Blamed && blamed 254 | 255 | child := &Node{ 256 | Expr: o, 257 | Should: should, 258 | Blamed: blamed, 259 | } 260 | // judge if leaf 261 | child.Leaf = child.isLeaf() 262 | 263 | children = append(children, child) 264 | isFirstChild = false 265 | } 266 | return children 267 | } 268 | 269 | func (node *Node) isLeaf() bool { 270 | if flag, _ := regexp.MatchString("^\\d+$", node.Expr); flag { 271 | return true 272 | } 273 | return false 274 | } 275 | 276 | func replaceBiggestBracketContent(expr string) (string, map[string]string) { 277 | var result = expr 278 | var mapReplace = make(map[string]string) 279 | for { 280 | before := result 281 | result, mapReplace = replaceBiggestBracketContentAtOnce(result, mapReplace) 282 | if before == result { 283 | // replace finished 284 | break 285 | } 286 | } 287 | return result, mapReplace 288 | } 289 | 290 | func replaceBiggestBracketContentAtOnce(expr string, mapReplace map[string]string) (string, map[string]string) { 291 | var result = expr 292 | var flag bool 293 | bracketStack := make([]rune, 0) 294 | toReplace := make([]rune, 0) 295 | 296 | for _, v := range expr { 297 | if flag { 298 | // add to buffer 299 | toReplace = append(toReplace, v) 300 | } 301 | if v == '(' { 302 | flag = true 303 | bracketStack = append(bracketStack, v) 304 | } else if v == ')' { 305 | // delete last ')' 306 | bracketStack = append(bracketStack[:len(bracketStack)-1]) 307 | if len(bracketStack) == 0 { 308 | // it's one biggest (***)block, break to replace 309 | break 310 | } 311 | } 312 | } 313 | 314 | if flag { 315 | // delete last ) 316 | toReplace = toReplace[:len(toReplace)-1] 317 | var key string 318 | if u, err := uuid.NewV1(); err != nil { 319 | // uuid error, just give me something random 320 | key = strconv.FormatFloat(rand.Float64(), 'f', -1, 64) 321 | } else { 322 | key = u.String() 323 | } 324 | 325 | result = strings.Replace(result, "("+string(toReplace)+")", key, 1) 326 | mapReplace[key] = strings.Trim(string(toReplace), " ") 327 | } 328 | return result, mapReplace 329 | } 330 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package ruler 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLogicToTree(t *testing.T) { 11 | logic := "1 or 2" 12 | head := logicToTree(logic) 13 | t.Log(head) 14 | assert.NotNil(t, head) 15 | assert.Equal(t, "1 or 2", head.Expr) 16 | } 17 | 18 | func TestReplaceBiggestBracketContent3(t *testing.T) { 19 | expr := "1 or 2 and 3 or ( 2 and 4 )" 20 | result, _ := replaceBiggestBracketContentAtOnce(expr, make(map[string]string)) 21 | t.Log(result) 22 | assert.Contains(t, result, "1 or 2 and 3") 23 | } 24 | 25 | func TestReplaceBiggestBracketContent(t *testing.T) { 26 | expr := "( 1 or 2 ) and 3 or ( 2 and 4 )" 27 | result, _ := replaceBiggestBracketContentAtOnce(expr, make(map[string]string)) 28 | t.Log(result) 29 | assert.Contains(t, result, "3 or ( 2 and 4 )") 30 | } 31 | 32 | func TestReplaceBiggestBracketContent2(t *testing.T) { 33 | expr := "( 1 or 2 ) and 3 or ( 2 and 4 )" 34 | result, mapReplace := replaceBiggestBracketContent(expr) 35 | t.Log(result) 36 | t.Log(mapReplace) 37 | assert.Contains(t, result, "and 3 or") 38 | } 39 | 40 | func TestReplaceBiggestBracketContent4(t *testing.T) { 41 | expr := "( 1 or 2 ) and ( 3 or ( 2 and 4 ) )" 42 | result, mapReplace := replaceBiggestBracketContent(expr) 43 | t.Log(result) 44 | t.Log(mapReplace) 45 | assert.Contains(t, result, "and") 46 | } 47 | 48 | func TestReplaceBiggestBracketContent5(t *testing.T) { 49 | expr := "1 or 2 and ( 3 or ( 2 and 4 ) )" 50 | result, _ := replaceBiggestBracketContentAtOnce(expr, make(map[string]string)) 51 | t.Log(result) 52 | assert.Contains(t, result, "1 or 2 and") 53 | } 54 | 55 | func TestLogicToTree2(t *testing.T) { 56 | logic := "1 and 2 and ( 3 or not ( 2 and 4 ) )" 57 | head := logicToTree(logic) 58 | traverseTreeInLayer(head) 59 | assert.NotNil(t, head) 60 | } 61 | 62 | func traverseTreeInLayer(head *Node) { 63 | var buf []*Node 64 | var i int 65 | buf = append(buf, head) 66 | for { 67 | if i < len(buf) { 68 | fmt.Printf("%+v\n", buf[i]) 69 | if buf[i].Children != nil { 70 | buf = append(buf, buf[i].Children...) 71 | } 72 | i++ 73 | } else { 74 | break 75 | } 76 | } 77 | } 78 | 79 | func traverseTreeInPostOrder(head *Node) { 80 | children := head.Children 81 | for _, child := range children { 82 | traverseTreeInPostOrder(child) 83 | } 84 | 85 | if head.Leaf { 86 | fmt.Printf("%+v\n", head) 87 | return 88 | } 89 | fmt.Printf("%+v\n", head) 90 | } 91 | 92 | func TestLogicToTree3(t *testing.T) { 93 | logic := "1 and 2 and ( 3 or not ( 2 and 4 ) )" 94 | head := logicToTree(logic) 95 | traverseTreeInPostOrder(head) 96 | assert.NotNil(t, head) 97 | } 98 | --------------------------------------------------------------------------------