@halleyassist/rule-parser 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-parser",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "The grammar for HalleyAssist rules",
5
- "main": "index.js",
5
+ "main": "src/RuleParser.production.js",
6
6
  "scripts": {
7
7
  "test": "mocha"
8
8
  },
@@ -20,11 +20,15 @@
20
20
  "ebnf": "^1.9.1"
21
21
  },
22
22
  "devDependencies": {
23
- "chai": "^4.4.1",
23
+ "chai": "^4",
24
24
  "mocha": "^10.4.0"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public",
28
28
  "registry": "https://registry.npmjs.org/"
29
- }
29
+ },
30
+ "files": [
31
+ "src/*",
32
+ "index.js"
33
+ ]
30
34
  }
@@ -0,0 +1,78 @@
1
+ const {Grammars} = require('ebnf');
2
+
3
+ const grammar = `
4
+ statement_main ::= statement EOF
5
+ logical_operator ::= AND | OR
6
+ statement ::= expression (logical_operator expression)*
7
+ expression ::= not_expression | standard_expression | parenthesis_expression
8
+ parenthesis_expression::= BEGIN_PARENTHESIS WS* statement WS* END_PARENTHESIS
9
+ not_expression ::= NOT (result | parenthesis_expression)
10
+ standard_expression ::= result ((WS* eq_approx) | (WS* basic_rhs) | ((WS+ IS)? WS+ between))?
11
+ basic_rhs ::= operator WS* result
12
+ eq_approx ::= eq_operator WS* "~" WS* result
13
+
14
+ PLUS ::= "+"
15
+ MINUS ::= "-"
16
+ MULTIPLY ::= "*"
17
+ DIVIDE ::= "/"
18
+ MODULUS ::= "%"
19
+ DEFAULT_VAL ::= "??"
20
+ arithmetic_operator ::= PLUS | MINUS | MULTIPLY | DIVIDE | MODULUS | DEFAULT_VAL
21
+ arithmetic_result ::= simple_result WS* arithmetic_operator WS* ( arithmetic_result | simple_result )
22
+
23
+ simple_result ::= fcall | value
24
+ result ::= arithmetic_result | simple_result
25
+ value ::= false | true | array | number_time | number | number_tod | time_period | string
26
+ BEGIN_ARRAY ::= WS* #x5B WS* /* [ left square bracket */
27
+ BEGIN_OBJECT ::= WS* #x7B WS* /* { left curly bracket */
28
+ END_ARRAY ::= WS* #x5D WS* /* ] right square bracket */
29
+ END_OBJECT ::= WS* #x7D WS* /* } right curly bracket */
30
+ NAME_SEPARATOR ::= WS* #x3A WS* /* : colon */
31
+ VALUE_SEPARATOR ::= WS* #x2C WS* /* , comma */
32
+ WS ::= [#x20#x09#x0A#x0D]+ /* Space | Tab | \n | \r */
33
+
34
+ operator ::= GTE | LTE | GT | LT | EQ | NEQ
35
+ eq_operator ::= EQ | NEQ
36
+
37
+ BEGIN_ARGUMENT ::= "("
38
+ END_ARGUMENT ::= ")"
39
+
40
+ BEGIN_PARENTHESIS ::= "("
41
+ END_PARENTHESIS ::= ")"
42
+
43
+ argument ::= statement WS* ("," WS*)?
44
+ arguments ::= argument*
45
+ fname ::= [a-zA-z0-9]+
46
+ fcall ::= fname WS* BEGIN_ARGUMENT arguments? END_ARGUMENT
47
+
48
+ between_number ::= number ((WS+ ("and" | "AND") WS+) | (WS* "-" WS*)) number
49
+ between_tod ::= number_tod ((WS+ ("and" | "AND") WS+)) number_tod
50
+ between ::= ("between" | "BETWEEN") WS+ (between_number | between_tod)
51
+
52
+ AND ::= (WS* "&&" WS*) | (WS+ ("AND"|"and") WS+)
53
+ OR ::= (WS* "||" WS*) | (WS+ ("OR"|"or") WS+)
54
+ GT ::= ">"
55
+ LT ::= "<"
56
+ GTE ::= ">="
57
+ LTE ::= "<="
58
+ IS ::= "is" | "IS"
59
+ EQ ::= "==" | "="
60
+ NEQ ::= "!="
61
+ NOT ::= ("!" WS*) | ("not" WS+)
62
+ false ::= "false" | "FALSE"
63
+ null ::= "null" | "NULL"
64
+ true ::= "true" | "TRUE"
65
+ array ::= BEGIN_ARRAY (value (VALUE_SEPARATOR value)*)? END_ARRAY
66
+
67
+ unit ::= "seconds" | "second" | "minutes" | "minute" | "min" | "mins" | "min" | "hours" | "hour" | "days" | "day" | "weeks" | "week"
68
+ number ::= "-"? ([0-9]+) ("." [0-9]+)? (("e" | "E") ( "-" | "+" )? ("0" | [1-9] [0-9]*))?
69
+ number_time ::= number WS+ unit
70
+ number_tod ::= ([0-9]+) ":" ([0-9]+)
71
+
72
+ time_period_const ::= "today"
73
+ time_period ::= time_period_const | between
74
+
75
+ string ::= '"' (([#x20-#x21] | [#x23-#x5B] | [#x5D-#xFFFF]) | #x5C (#x22 | #x5C | #x2F | #x62 | #x66 | #x6E | #x72 | #x74 | #x75 HEXDIG HEXDIG HEXDIG HEXDIG))* '"'
76
+ HEXDIG ::= [a-fA-F0-9]
77
+ `
78
+ module.exports = Grammars.W3C.getRules(grammar);
@@ -0,0 +1 @@
1
+ [{"name":"statement_main","bnf":[["statement","EOF"]]},{"name":"logical_operator","bnf":[["AND"],["OR"]]},{"name":"%statement0","bnf":[["logical_operator","expression"]],"fragment":true},{"name":"statement","bnf":[["expression","%statement0*"]]},{"name":"expression","bnf":[["not_expression"],["standard_expression"],["parenthesis_expression"]]},{"name":"parenthesis_expression","bnf":[["BEGIN_PARENTHESIS","WS*","statement","WS*","END_PARENTHESIS"]]},{"name":"%not_expression1","bnf":[["result"],["parenthesis_expression"]],"fragment":true},{"name":"not_expression","bnf":[["NOT","%not_expression1"]]},{"name":"%%standard_expression23","bnf":[["WS*","eq_approx"]],"fragment":true},{"name":"%%standard_expression24","bnf":[["WS*","basic_rhs"]],"fragment":true},{"name":"%%%standard_expression256","bnf":[["WS+","IS"]],"fragment":true},{"name":"%%standard_expression25","bnf":[["%%%standard_expression256?","WS+","between"]],"fragment":true},{"name":"%standard_expression2","bnf":[["%%standard_expression23"],["%%standard_expression24"],["%%standard_expression25"]],"fragment":true},{"name":"standard_expression","bnf":[["result","%standard_expression2?"]]},{"name":"basic_rhs","bnf":[["operator","WS*","result"]]},{"name":"eq_approx","bnf":[["eq_operator","WS*","\"~\"","WS*","result"]]},{"name":"PLUS","bnf":[["\"+\""]]},{"name":"MINUS","bnf":[["\"-\""]]},{"name":"MULTIPLY","bnf":[["\"*\""]]},{"name":"DIVIDE","bnf":[["\"/\""]]},{"name":"MODULUS","bnf":[["\"%\""]]},{"name":"DEFAULT_VAL","bnf":[["\"??\""]]},{"name":"arithmetic_operator","bnf":[["PLUS"],["MINUS"],["MULTIPLY"],["DIVIDE"],["MODULUS"],["DEFAULT_VAL"]]},{"name":"%arithmetic_result7","bnf":[["arithmetic_result"],["simple_result"]],"fragment":true},{"name":"arithmetic_result","bnf":[["simple_result","WS*","arithmetic_operator","WS*","%arithmetic_result7"]]},{"name":"simple_result","bnf":[["fcall"],["value"]]},{"name":"result","bnf":[["arithmetic_result"],["simple_result"]]},{"name":"value","bnf":[["false"],["true"],["array"],["number_time"],["number"],["number_tod"],["time_period"],["string"]]},{"name":"BEGIN_ARRAY","bnf":[["WS*",{},"WS*"]]},{"name":"BEGIN_OBJECT","bnf":[["WS*",{},"WS*"]]},{"name":"END_ARRAY","bnf":[["WS*",{},"WS*"]]},{"name":"END_OBJECT","bnf":[["WS*",{},"WS*"]]},{"name":"NAME_SEPARATOR","bnf":[["WS*",{},"WS*"]]},{"name":"VALUE_SEPARATOR","bnf":[["WS*",{},"WS*"]]},{"name":"%WS8","bnf":[[{}]]},{"name":"WS","bnf":[["%WS8+"]]},{"name":"operator","bnf":[["GTE"],["LTE"],["GT"],["LT"],["EQ"],["NEQ"]]},{"name":"eq_operator","bnf":[["EQ"],["NEQ"]]},{"name":"BEGIN_ARGUMENT","bnf":[["\"(\""]]},{"name":"END_ARGUMENT","bnf":[["\")\""]]},{"name":"BEGIN_PARENTHESIS","bnf":[["\"(\""]]},{"name":"END_PARENTHESIS","bnf":[["\")\""]]},{"name":"%argument9","bnf":[["\",\"","WS*"]],"fragment":true},{"name":"argument","bnf":[["statement","WS*","%argument9?"]]},{"name":"arguments","bnf":[["argument*"]]},{"name":"%fname10","bnf":[[{}]]},{"name":"fname","bnf":[["%fname10+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","arguments?","END_ARGUMENT"]]},{"name":"%%%between_number111213","bnf":[["\"and\""],["\"AND\""]],"fragment":true},{"name":"%%between_number1112","bnf":[["WS+","%%%between_number111213","WS+"]],"fragment":true},{"name":"%%between_number1114","bnf":[["WS*","\"-\"","WS*"]],"fragment":true},{"name":"%between_number11","bnf":[["%%between_number1112"],["%%between_number1114"]],"fragment":true},{"name":"between_number","bnf":[["number","%between_number11","number"]]},{"name":"%%%between_tod151617","bnf":[["\"and\""],["\"AND\""]],"fragment":true},{"name":"%%between_tod1516","bnf":[["WS+","%%%between_tod151617","WS+"]],"fragment":true},{"name":"%between_tod15","bnf":[["%%between_tod1516"]],"fragment":true},{"name":"between_tod","bnf":[["number_tod","%between_tod15","number_tod"]]},{"name":"%between18","bnf":[["\"between\""],["\"BETWEEN\""]],"fragment":true},{"name":"%between19","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[["%between18","WS+","%between19"]]},{"name":"%AND20","bnf":[["WS*","\"&&\"","WS*"]],"fragment":true},{"name":"%%AND2122","bnf":[["\"AND\""],["\"and\""]],"fragment":true},{"name":"%AND21","bnf":[["WS+","%%AND2122","WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND20"],["%AND21"]]},{"name":"%OR23","bnf":[["WS*","\"||\"","WS*"]],"fragment":true},{"name":"%%OR2425","bnf":[["\"OR\""],["\"or\""]],"fragment":true},{"name":"%OR24","bnf":[["WS+","%%OR2425","WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR23"],["%OR24"]]},{"name":"GT","bnf":[["\">\""]]},{"name":"LT","bnf":[["\"<\""]]},{"name":"GTE","bnf":[["\">=\""]]},{"name":"LTE","bnf":[["\"<=\""]]},{"name":"IS","bnf":[["\"is\""],["\"IS\""]]},{"name":"EQ","bnf":[["\"==\""],["\"=\""]]},{"name":"NEQ","bnf":[["\"!=\""]]},{"name":"%NOT26","bnf":[["\"!\"","WS*"]],"fragment":true},{"name":"%NOT27","bnf":[["\"not\"","WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT26"],["%NOT27"]]},{"name":"false","bnf":[["\"false\""],["\"FALSE\""]]},{"name":"null","bnf":[["\"null\""],["\"NULL\""]]},{"name":"true","bnf":[["\"true\""],["\"TRUE\""]]},{"name":"%%array2829","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array28","bnf":[["value","%%array2829*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array28?","END_ARRAY"]]},{"name":"unit","bnf":[["\"seconds\""],["\"second\""],["\"minutes\""],["\"minute\""],["\"min\""],["\"mins\""],["\"min\""],["\"hours\""],["\"hour\""],["\"days\""],["\"day\""],["\"weeks\""],["\"week\""]]},{"name":"%%number3031","bnf":[[{}]]},{"name":"%number30","bnf":[["%%number3031+"]],"fragment":true},{"name":"%%number3233","bnf":[[{}]]},{"name":"%number32","bnf":[["\".\"","%%number3233+"]],"fragment":true},{"name":"%%number3435","bnf":[["\"e\""],["\"E\""]],"fragment":true},{"name":"%%number3436","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%%%number343738","bnf":[[{}]]},{"name":"%%number3437","bnf":[["\"0\""],[{},"%%%number343738*"]],"fragment":true},{"name":"%number34","bnf":[["%%number3435","%%number3436?","%%number3437"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number30","%number32?","%number34?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%%number_tod3940","bnf":[[{}]]},{"name":"%number_tod39","bnf":[["%%number_tod3940+"]],"fragment":true},{"name":"%%number_tod4142","bnf":[[{}]]},{"name":"%number_tod41","bnf":[["%%number_tod4142+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod39","\":\"","%number_tod41"]]},{"name":"time_period_const","bnf":[["\"today\""]]},{"name":"time_period","bnf":[["time_period_const"],["between"]]},{"name":"%%string4344","bnf":[[{}],[{}],[{}]],"fragment":true},{"name":"%%string4345","bnf":[[{}],[{}],[{}],[{}],[{}],[{}],[{}],[{}],[{},"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string43","bnf":[["%%string4344"],[{},"%%string4345"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string43*","'\"'"]]},{"name":"HEXDIG","bnf":[[{}]]}]
package/src/RuleParser.js CHANGED
@@ -1,85 +1,8 @@
1
- const { Grammars, Parser } = require('ebnf'),
1
+ const { Parser } = require('ebnf'),
2
2
  assert = require('assert')
3
3
 
4
-
5
- const grammar = `
6
- statement_main ::= statement EOF
7
- logical_operator ::= AND | OR
8
- statement ::= expression (logical_operator expression)*
9
- expression ::= not_expression | standard_expression | parenthesis_expression
10
- parenthesis_expression::= BEGIN_PARENTHESIS WS* statement WS* END_PARENTHESIS
11
- not_expression ::= NOT (result | parenthesis_expression)
12
- standard_expression ::= result ((WS* eq_approx) | (WS* basic_rhs) | ((WS+ IS)? WS+ between))?
13
- basic_rhs ::= operator WS* result
14
- eq_approx ::= eq_operator WS* "~" WS* result
15
-
16
- PLUS ::= "+"
17
- MINUS ::= "-"
18
- MULTIPLY ::= "*"
19
- DIVIDE ::= "/"
20
- MODULUS ::= "%"
21
- DEFAULT_VAL ::= "??"
22
- arithmetic_operator ::= PLUS | MINUS | MULTIPLY | DIVIDE | MODULUS | DEFAULT_VAL
23
- arithmetic_result ::= simple_result WS* arithmetic_operator WS* ( arithmetic_result | simple_result )
24
-
25
- simple_result ::= fcall | value
26
- result ::= arithmetic_result | simple_result
27
- value ::= false | true | array | number_time | number | number_tod | time_period | string
28
- BEGIN_ARRAY ::= WS* #x5B WS* /* [ left square bracket */
29
- BEGIN_OBJECT ::= WS* #x7B WS* /* { left curly bracket */
30
- END_ARRAY ::= WS* #x5D WS* /* ] right square bracket */
31
- END_OBJECT ::= WS* #x7D WS* /* } right curly bracket */
32
- NAME_SEPARATOR ::= WS* #x3A WS* /* : colon */
33
- VALUE_SEPARATOR ::= WS* #x2C WS* /* , comma */
34
- WS ::= [#x20#x09#x0A#x0D]+ /* Space | Tab | \n | \r */
35
-
36
- operator ::= GTE | LTE | GT | LT | EQ | NEQ
37
- eq_operator ::= EQ | NEQ
38
-
39
- BEGIN_ARGUMENT ::= "("
40
- END_ARGUMENT ::= ")"
41
-
42
- BEGIN_PARENTHESIS ::= "("
43
- END_PARENTHESIS ::= ")"
44
-
45
- argument ::= statement WS* ("," WS*)?
46
- arguments ::= argument*
47
- fname ::= [a-zA-z0-9]+
48
- fcall ::= fname WS* BEGIN_ARGUMENT arguments? END_ARGUMENT
49
-
50
- between_number ::= number ((WS+ ("and" | "AND") WS+) | (WS* "-" WS*)) number
51
- between_tod ::= number_tod ((WS+ ("and" | "AND") WS+)) number_tod
52
- between ::= ("between" | "BETWEEN") WS+ (between_number | between_tod)
53
-
54
- AND ::= (WS* "&&" WS*) | (WS+ ("AND"|"and") WS+)
55
- OR ::= (WS* "||" WS*) | (WS+ ("OR"|"or") WS+)
56
- GT ::= ">"
57
- LT ::= "<"
58
- GTE ::= ">="
59
- LTE ::= "<="
60
- IS ::= "is" | "IS"
61
- EQ ::= "==" | "="
62
- NEQ ::= "!="
63
- NOT ::= ("!" WS*) | ("not" WS+)
64
- false ::= "false" | "FALSE"
65
- null ::= "null" | "NULL"
66
- true ::= "true" | "TRUE"
67
- array ::= BEGIN_ARRAY (value (VALUE_SEPARATOR value)*)? END_ARRAY
68
-
69
- unit ::= "seconds" | "second" | "minutes" | "minute" | "min" | "mins" | "min" | "hours" | "hour" | "days" | "day" | "weeks" | "week"
70
- number ::= "-"? ([0-9]+) ("." [0-9]+)? (("e" | "E") ( "-" | "+" )? ("0" | [1-9] [0-9]*))?
71
- number_time ::= number WS+ unit
72
- number_tod ::= ([0-9]+) ":" ([0-9]+)
73
-
74
- time_period_const ::= "today"
75
- time_period ::= time_period_const | between
76
-
77
- string ::= '"' (([#x20-#x21] | [#x23-#x5B] | [#x5D-#xFFFF]) | #x5C (#x22 | #x5C | #x2F | #x62 | #x66 | #x6E | #x72 | #x74 | #x75 HEXDIG HEXDIG HEXDIG HEXDIG))* '"'
78
- HEXDIG ::= [a-fA-F0-9]
79
- `
80
- let RULES = Grammars.W3C.getRules(grammar);
81
- let parser = new Parser(RULES, {debug: false});
82
- const target = 'statement_main'
4
+ let ParserRules = require('./RuleParser.ebnf.js')
5
+ let ParserCache;
83
6
 
84
7
  const ArithmeticOperators = {
85
8
  "+": 'MathAdd',
@@ -114,10 +37,12 @@ const Epsilon = 0.01
114
37
  class RuleParser {
115
38
  static toAst(txt){
116
39
  let ret
117
- //if(process.env.NODE_ENV === 'test') {
118
- // parser.debug = true
119
- //}
120
- ret = parser.getAST(txt, target);
40
+
41
+ if(!ParserCache){
42
+ ParserCache = new Parser(ParserRules, {debug: false})
43
+ }
44
+
45
+ ret = ParserCache.getAST(txt, 'statement_main');
121
46
 
122
47
  if(ret){
123
48
  return ret.children[0]
@@ -0,0 +1,284 @@
1
+ const { Parser } = require('ebnf'),
2
+ assert = require('assert')
3
+
4
+ let ParserRules = require('./RuleParser.ebnf.json')
5
+ let ParserCache;
6
+
7
+ const ArithmeticOperators = {
8
+ "+": 'MathAdd',
9
+ "-": 'MathSub',
10
+ "/": 'MathDiv',
11
+ "*": 'MathMul',
12
+ "%": 'MathMod',
13
+ "??": "Default"
14
+ }
15
+
16
+ const OperatorFn = {
17
+ ">": "Gt",
18
+ "<": "Lt",
19
+ ">=": "Gte",
20
+ "<=": "Lte",
21
+ "==": "Eq",
22
+ "=": "Eq",
23
+ "!=": "Neq"
24
+ }
25
+
26
+ const LogicalOperators = {
27
+ "&&": 'And',
28
+ "AND": 'And',
29
+ "and": 'And',
30
+ "||": 'Or',
31
+ "OR": 'Or',
32
+ "or": 'Or',
33
+ }
34
+
35
+ const Epsilon = 0.01
36
+
37
+ class RuleParser {
38
+ static toAst(txt){
39
+ let ret
40
+
41
+ if(!ParserCache){
42
+ ParserCache = new Parser(ParserRules, {debug: false})
43
+ }
44
+
45
+ ret = ParserCache.getAST(txt, 'statement_main');
46
+
47
+ if(ret){
48
+ return ret.children[0]
49
+ }
50
+ }
51
+ static _parseArgument(argument){
52
+ assert(argument.type === 'argument')
53
+ const child = argument.children[0]
54
+ return RuleParser._buildExpressionGroup(child)
55
+ }
56
+ static _parseFcall(fcall){
57
+ const fname = fcall.children[0]
58
+ const ret = [fname.text]
59
+ if(fcall.children.length != 1){
60
+ const args = fcall.children[1]
61
+ for(const a of args.children){
62
+ ret.push(RuleParser._parseArgument(a))
63
+ }
64
+ }
65
+ return ret
66
+ }
67
+ static _parseTimePeriod(tp){
68
+ switch(tp.type){
69
+ case 'time_period_const':
70
+ return ["TimePeriodConst", tp.text]
71
+ case 'between':
72
+ return ["TimePeriodBetween", RuleParser.__parseValue(tp.children[0]?.children[0]), RuleParser.__parseValue(tp.children[0]?.children[1])]
73
+ }
74
+ }
75
+ static __parseValue(child){
76
+ const type = child.type
77
+ switch(type){
78
+ case 'string': {
79
+ const str = child.text
80
+ return str.slice(1, -1)
81
+ }
82
+ case 'number':
83
+ return parseFloat(child.text)
84
+ case 'number_tod': {
85
+ const tokens = child.text.split(':')
86
+ if (tokens.length !== 2) {
87
+ throw new Error(`Invalid time of day, ${child.text} should be ##:##`)
88
+ }
89
+ const hours = parseInt(tokens[0])
90
+ const minutes = parseInt(tokens[1])
91
+ const tod = hours * 100 + minutes
92
+ const ret = { hours, minutes, tod }
93
+ if (!isNaN(tod) && ret.hours >= 0 && ret.hours <= 24 && ret.minutes >= 0 && ret.minutes < 60) {
94
+ return ret
95
+ }
96
+ throw new Error(`Invalid time of day, ${child.text} -> [${tokens.join(', ')}] -> ${hours}h${minutes}m -> ${tod}`)
97
+ }
98
+ case 'number_time': {
99
+ const nt = child
100
+ const mult = parseFloat(nt.children[0].text)
101
+ switch(nt.children[1].text){
102
+ case 'seconds':
103
+ case 'second':
104
+ return mult
105
+ case 'minutes':
106
+ case 'minute':
107
+ case 'mins':
108
+ case 'min':
109
+ return mult * 60
110
+ case 'hours':
111
+ case 'hour':
112
+ return mult * 60 * 60
113
+ case 'days':
114
+ case 'day':
115
+ return mult * 60 * 60 * 24
116
+ case 'weeks':
117
+ case 'week':
118
+ return mult * 60 * 60 * 24 * 7
119
+ }
120
+ throw new Error(`Invalid exponent ${nt.children[1].text}`)
121
+ }
122
+ case 'true':
123
+ return true
124
+ case 'false':
125
+ return false
126
+ case 'array': {
127
+ const ret = []
128
+ for(const c of child.children){
129
+ ret.push(RuleParser.__parseValue(c.children[0]))
130
+ }
131
+ return ret;
132
+ }
133
+ default:
134
+ throw new Error(`Unknown value type ${type}`)
135
+ }
136
+ }
137
+ static _parseValue(value){
138
+ const child = value.children[0]
139
+
140
+ const type = child.type
141
+ switch(type){
142
+ case 'time_period': {
143
+ const tp = child.children[0]
144
+ return RuleParser._parseTimePeriod(tp)
145
+ }
146
+ default:
147
+ return ['Value', RuleParser.__parseValue(child)]
148
+ }
149
+ }
150
+ static _parseSimpleResult(result){
151
+ assert(result.children.length == 1)
152
+ const child = result.children[0]
153
+ const type = child.type
154
+ switch(type){
155
+ case 'fcall':
156
+ return RuleParser._parseFcall(child)
157
+ case 'value':
158
+ return RuleParser._parseValue(child)
159
+ }
160
+ return null
161
+ }
162
+ static _parseArithmeticResult(result){
163
+ assert(result.children.length == 3)
164
+ const partA = this._parseSimpleResult(result.children[0])
165
+ const operatorFn = ArithmeticOperators[result.children[1].text]
166
+ const partB = this.__parseResult(result, 2)
167
+
168
+ return [operatorFn, partA, partB]
169
+ }
170
+
171
+ static __parseResult(result, idx){
172
+ const child = result.children[idx]
173
+ const type = child.type
174
+ switch(type){
175
+ case 'simple_result':
176
+ return RuleParser._parseSimpleResult(child)
177
+ case 'arithmetic_result':
178
+ return RuleParser._parseArithmeticResult(child)
179
+ }
180
+
181
+ throw new Error(`Unknown result node ${type}`)
182
+ }
183
+ static _parseResult(result){
184
+ assert(result.children.length == 1)
185
+
186
+ return RuleParser.__parseResult(result, 0)
187
+ }
188
+ static _parseStdExpression(expr){
189
+ assert(expr.type === 'standard_expression')
190
+ switch(expr.children.length){
191
+ case 1:
192
+ return RuleParser._parseResult(expr.children[0])
193
+ case 2: {
194
+ const rhs = expr.children[1]
195
+ switch(rhs.type){
196
+ case 'between_tod':
197
+ case 'between_number':
198
+ case 'between':
199
+ return ['Between', RuleParser._parseResult(expr.children[0]), ['Value', RuleParser.__parseValue(rhs.children[0].children[0])], ['Value', RuleParser.__parseValue(rhs.children[0].children[1])]]
200
+ case 'basic_rhs':
201
+ return [OperatorFn[rhs.children[0].text], RuleParser._parseResult(expr.children[0]), RuleParser._parseResult(rhs.children[1])]
202
+ case 'eq_approx': {
203
+ const rhsValue = RuleParser._parseResult(rhs.children[1])
204
+ assert(rhsValue[0] === 'Value')
205
+ const ret = ['Between', RuleParser._parseResult(expr.children[0]), ['Value', rhsValue[1] - Epsilon], ['Value', rhsValue[1] + Epsilon]]
206
+ if(rhs.children[0].text === '!='){
207
+ return ['Not', ret]
208
+ }
209
+ return ret
210
+ }
211
+
212
+ default:
213
+ throw new Error(`unable to parse std expression, unknown rhs type ${rhs.type}`)
214
+ }
215
+ }
216
+
217
+ default:
218
+ throw new Error(`unable to parse std expression, unknown number of children ${expr.children.length}`)
219
+ }
220
+ }
221
+ static buildLogical(members, fn){
222
+ return [fn, ...members]
223
+ }
224
+ static _buildExpressionGroup(ast){
225
+ let ret = []
226
+ let currentLogical = null
227
+ for(const expr of ast.children){
228
+ if(expr.type == 'logical_operator') {
229
+ const logicalOperator = expr.text.trim()
230
+ const operatorFn = LogicalOperators[logicalOperator]
231
+ assert(operatorFn, `Unknown logical operator ${logicalOperator}`)
232
+ if(currentLogical === null || currentLogical !== operatorFn){
233
+ if(ret.length > 1){
234
+ ret = [RuleParser.buildLogical(ret, currentLogical)]
235
+ }
236
+ currentLogical = operatorFn
237
+ }
238
+ }else{
239
+ ret.push(RuleParser._exprToIL(expr))
240
+ }
241
+ }
242
+ if(ret.length == 0){
243
+ throw new Error('invalid rule')
244
+ }
245
+ if(ret.length == 1){
246
+ return ret[0]
247
+ }
248
+ return RuleParser.buildLogical(ret, currentLogical)
249
+ }
250
+ static _parseParenthesisExpression(expr){
251
+ return RuleParser._buildExpressionGroup(expr.children[0])
252
+ }
253
+ static _exprToIL(expr){
254
+ assert(expr.type === 'expression')
255
+ assert(expr.children.length === 1)
256
+ const eInner = expr.children[0]
257
+ switch(eInner.type){
258
+ case 'standard_expression':
259
+ return RuleParser._parseStdExpression(eInner)
260
+ case 'not_expression': {
261
+ const child = eInner.children[0]
262
+ let result
263
+ switch(child.type){
264
+ case 'parenthesis_expression':
265
+ result = RuleParser._parseParenthesisExpression(child)
266
+ break;
267
+ default:
268
+ result = RuleParser._parseResult(child)
269
+ }
270
+ return ['Not', result]
271
+ }
272
+ case 'parenthesis_expression':
273
+ return RuleParser._parseParenthesisExpression(eInner)
274
+ default:
275
+ throw new Error(`unknown type of expression ${eInner.type}`)
276
+ }
277
+ }
278
+ static toIL(txt){
279
+ const ast = RuleParser.toAst(txt)
280
+ if(!ast) throw new Error(`failed to parse ${txt}`)
281
+ return RuleParser._buildExpressionGroup(ast)
282
+ }
283
+ }
284
+ module.exports = RuleParser
package/.eslintrc.yml DELETED
@@ -1,8 +0,0 @@
1
- env:
2
- node: true
3
- es2021: true
4
- extends: 'eslint:recommended'
5
- parserOptions:
6
- ecmaVersion: 12
7
- sourceType: module
8
- rules: {}
@@ -1,69 +0,0 @@
1
- name: Build
2
- on:
3
- push:
4
-
5
- jobs:
6
- check_eslint:
7
- runs-on: ubuntu-latest
8
- steps:
9
- - uses: actions/checkout@v2
10
- - name: Install modules
11
- run: |
12
- sudo npm install -g eslint@8
13
- - run: eslint index.js --ext .js,.jsx,.ts,.tsx
14
- test:
15
- runs-on: ubuntu-latest
16
- steps:
17
- - name: Checkout repository
18
- uses: actions/checkout@master
19
- - name: Set up Node.js
20
- uses: actions/setup-node@master
21
- with:
22
- node-version: 17
23
- - run: |
24
- npm install
25
- - run: |
26
- npm run-script test
27
- npm:
28
- name: npm-publish
29
- if: "github.event_name == 'push'"
30
- runs-on: ubuntu-latest
31
- needs: [check_eslint,test]
32
- steps:
33
- - name: Checkout repository
34
- uses: actions/checkout@master
35
- - name: Set up Node.js
36
- uses: actions/setup-node@master
37
- with:
38
- node-version: 16
39
- registry-url: 'https://registry.npmjs.org'
40
- - name: Install modules
41
- run: |
42
- npm install
43
- - name: Test
44
- run: npm test
45
- - run: |
46
- VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,' | sed -e 's/^v//')
47
- jq '.version="'"$VERSION"'"' package.json > /tmp/a
48
- mv /tmp/a package.json
49
- if: "startsWith(github.ref, 'refs/tags/v')"
50
- - run: |
51
- # npm login --scope=@halleyassist --registry=https://registry.npmjs.org/
52
- npm whoami
53
- env:
54
- NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
55
- - run: |
56
- npm publish
57
- env:
58
- NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
59
- if: "startsWith(github.ref, 'refs/tags/v')"
60
- - uses: actions/setup-node@v2
61
- with:
62
- node-version: 16
63
- registry-url: https://npm.pkg.github.com/
64
- if: "startsWith(github.ref, 'refs/tags/v')"
65
- - run: |
66
- npm publish
67
- env:
68
- NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
69
- if: "startsWith(github.ref, 'refs/tags/v')"
@@ -1,214 +0,0 @@
1
- const
2
- RuleParser = require("../src/RuleParser"),
3
- { expect } = require('chai')
4
-
5
- describe("RuleParser", function () {
6
- it("parse basic", function () {
7
- expect(RuleParser.toIL("1 > 2")).to.be.eql(["Gt", ["Value", 1], ["Value", 2]])
8
- expect(RuleParser.toIL("AFunction(1,2) > 3")).to.be.eql(["Gt", ["AFunction", ["Value", 1], ["Value", 2]], ["Value", 3]])
9
- expect(RuleParser.toIL("AFunction(1,2)")).to.be.eql(["AFunction", ["Value", 1], ["Value", 2]])
10
- expect(RuleParser.toIL("AFunction(\"string\",2) == \"string\"")).to.be.eql(["Eq", ["AFunction", ["Value", "string"], ["Value", 2]], ["Value", "string"]])
11
- })
12
- it("parse logic expression", function () {
13
- expect(RuleParser.toIL("AFunction()")).to.be.eql(["AFunction"])
14
- expect(RuleParser.toIL("!AFunction()")).to.be.eql(["Not", ["AFunction"]])
15
- expect(RuleParser.toIL("! AFunction()")).to.be.eql(["Not", ["AFunction"]])
16
- expect(RuleParser.toIL("not AFunction()")).to.be.eql(["Not", ["AFunction"]])
17
- })
18
- it("parse function no args", function () {
19
- expect(RuleParser.toIL("AFunction() > 3")).to.be.eql(["Gt", ["AFunction"], ["Value", 3]])
20
- })
21
- it("parse number starting with 0", function () {
22
- expect(RuleParser.toIL("AFunction() > 03")).to.be.eql(["Gt", ["AFunction"], ["Value", 3]])
23
- })
24
- it("should throw if additional junk", function () {
25
- expect(function () {
26
- RuleParser.toIL("AFunction() > 3 abc")
27
- }).to.throw()
28
- })
29
- it("parse array", function () {
30
- expect(RuleParser.toIL("AFunction([1,2])")).to.be.eql(["AFunction", ["Value", [1, 2]]])
31
- })
32
- it("should be able to parse all basic comparison operators", function () {
33
- const operators = [
34
- '==', '=', "!=", ">=", ">", "<=", '<'
35
- ]
36
- for (const op of operators) {
37
- const expression1 = `RoomDuration("Room 1") ${op} 5`
38
- RuleParser.toIL(expression1)
39
- const expression2 = `RoomDuration("Room 1")${op}5`
40
- RuleParser.toIL(expression2)
41
- }
42
- })
43
- it("should be able to parse the between operator", function () {
44
- const expression1 = `RoomDuration("Room 1") between 1 and 5`
45
- expect(RuleParser.toIL(expression1)).to.be.eql(['Between', ["RoomDuration", ["Value", "Room 1"]], ["Value", 1], ["Value", 5]])
46
- })
47
- it("should be able to parse the equals approximately operator", function () {
48
- const expression1 = `RoomDuration("Room 1") == ~1`
49
- expect(RuleParser.toIL(expression1)).to.be.eql([
50
- 'Between',
51
- ['RoomDuration', ['Value', 'Room 1']],
52
- ['Value', 0.99],
53
- ['Value', 1.01]
54
- ])
55
- })
56
- it("should be able to parse the approximately not equals operator", function () {
57
- const expression1 = `RoomDuration("Room 1") !=~1`
58
- expect(RuleParser.toIL(expression1)).to.be.eql([
59
- 'Not', ['Between',
60
- ['RoomDuration', ['Value', 'Room 1']],
61
- ['Value', 0.99],
62
- ['Value', 1.01]
63
- ]])
64
- })
65
- it("should be able to parse all time units", function () {
66
- const unit = [
67
- 'second', 'minute', 'hour', 'day', 'week'
68
- ]
69
- for (const u of unit) {
70
- const expression1 = `RoomDuration(1 ${u})`
71
- RuleParser.toIL(expression1)
72
- const expression2 = `RoomDuration(1 ${u}s)`
73
- RuleParser.toIL(expression2)
74
- }
75
- })
76
- it("should be able to parse all time tp constants", function () {
77
- const unit = [
78
- 'today'/*, "yesterday"*/
79
- ]
80
- for (const u of unit) {
81
- const expression1 = `RoomDuration(${u})`
82
- const il = RuleParser.toIL(expression1)
83
- expect(il).to.be.eql(['RoomDuration', ['TimePeriodConst', u]])
84
- }
85
- })
86
- it("should be able to parse tp between", function () {
87
- const expression1 = `RoomDuration(BETWEEN 01:00 and 24:00)`
88
- const il = RuleParser.toIL(expression1)
89
- const oneAm = {hours: 1, minutes: 0, tod: 100}
90
- const midnight = {hours: 24, minutes: 0, tod: 2400}
91
- expect(il).to.be.eql(['RoomDuration', ['TimePeriodBetween', oneAm, midnight]])
92
- })
93
- it("should be able to parse parenthesis", function () {
94
- const expression1 = `(A()==2 and B()==1) and (C(2) && D(1))`
95
- const il = RuleParser.toIL(expression1)
96
- expect(il).to.be.eql([
97
- 'And',
98
- ['And', ['Eq', ["A"], ["Value", 2]], ['Eq', ["B"], ["Value", 1]]],
99
- ['And', ['C', ["Value", 2]], ['D', ["Value", 1]]]
100
- ])
101
- })
102
- it("should be able to parse not expressions (1)", function () {
103
- const expression1 = `!D(1)`
104
- let il = RuleParser.toIL(expression1)
105
- expect(il).to.be.eql(['Not', ['D', ['Value', 1]]])
106
- const expression2 = `! D(1)&& !D(2)`
107
- il = RuleParser.toIL(expression2)
108
- expect(il).to.be.eql(['And', ['Not', ['D', ['Value', 1]]], ['Not', ['D', ['Value', 2]]]])
109
- const expression3 = `(! D(1) && !D(3)) && !D(2)`
110
- il = RuleParser.toIL(expression3)
111
- expect(il).to.be.eql([
112
- 'And',
113
- ['And', ['Not', ['D', ['Value', 1]]], ['Not', ['D', ['Value', 3]]]],
114
- ['Not', ['D', ['Value', 2]]]
115
- ])
116
- })
117
- it("should be able to parse not expressions (2)", function () {
118
- const expression1 = `(A()==2 and B()==1) and (C(2) && !D(1))`
119
- const il = RuleParser.toIL(expression1)
120
- expect(il).to.be.eql([
121
- 'And',
122
- ['And', ['Eq', ["A"], ["Value", 2]], ['Eq', ["B"], ["Value", 1]]],
123
- ['And', ['C', ["Value", 2]], ['Not', ['D', ["Value", 1]]]]
124
- ])
125
- })
126
- it("should be able to parse not expressions (3)", function () {
127
- const expression1 = `(A()==2 and B()==1) and ! (C(2) && D(1))`
128
- const il = RuleParser.toIL(expression1)
129
- expect(il).to.be.eql([
130
- 'And',
131
- ['And', ['Eq', ["A"], ["Value", 2]], ['Eq', ["B"], ["Value", 1]]],
132
- ['Not', ['And', ['C', ["Value", 2]], ['D', ["Value", 1]]]]
133
- ])
134
- })
135
- it("should be able to parse or expressions (1)", function () {
136
- const expression1 = `(A()==2 and B()==1) or (C(2) && D(1))`
137
- const il = RuleParser.toIL(expression1)
138
- expect(il).to.be.eql([
139
- 'Or',
140
- ['And', ['Eq', ["A"], ["Value", 2]], ['Eq', ["B"], ["Value", 1]]],
141
- ['And', ['C', ["Value", 2]], ['D', ["Value", 1]]]
142
- ])
143
- })
144
- it("should be able to parse or expressions (2)", function () {
145
- const expression1 = `(A()==2 or B()==1) and (C(2) || D(1))`
146
- const il = RuleParser.toIL(expression1)
147
- expect(il).to.be.eql([
148
- 'And',
149
- ['Or', ['Eq', ["A"], ["Value", 2]], ['Eq', ["B"], ["Value", 1]]],
150
- ['Or', ['C', ["Value", 2]], ['D', ["Value", 1]]]
151
- ])
152
- })
153
- it("should be able to parse or expressions (3)", function () {
154
- const expression1 = `A() or !(C(2) && D(1))`
155
- const il = RuleParser.toIL(expression1)
156
- expect(il).to.be.eql([
157
- 'Or',
158
- ["A"],
159
- ['Not', ['And', ['C', ["Value", 2]], ['D', ["Value", 1]]]]
160
- ])
161
- })
162
- it("should be able to parse or expressions (4)", function () {
163
- const expression1 = `A() and !(C(2) || D(1))`
164
- const il = RuleParser.toIL(expression1)
165
- expect(il).to.be.eql([
166
- 'And',
167
- ["A"],
168
- ['Not', ['Or', ['C', ["Value", 2]], ['D', ["Value", 1]]]]
169
- ])
170
- })
171
-
172
- it("should be able to parse real or expressions (1)", function () {
173
- const expression1 = "HasCapableSensor(\"weight\") && !((TimeOfDay() BETWEEN 0800 AND 1200 && Event(\"kind\") == \"measurement\" && EventHasData(\"weight\") && TimeLastTrueSet(\"slot1\")) || TimeLastTrueCheck(\"slot1\") < 5 minutes)"
174
- const il = RuleParser.toIL(expression1)
175
- expect(il).to.be.eql(
176
- ["And",
177
- ["HasCapableSensor", ["Value", "weight"]],
178
- ["Not", ["Or",
179
- ["And", ["Between", ["TimeOfDay"], ["Value", 800], ["Value", 1200]], ["Eq", ["Event", ["Value", "kind"]], ["Value", "measurement"]], ["EventHasData", ["Value", "weight"]], ["TimeLastTrueSet", ["Value", "slot1"]]],
180
- ["Lt", ["TimeLastTrueCheck", ["Value", "slot1"]], ["Value", 300]]
181
- ]]])
182
- })
183
- it("should be able to parse arithmetic (1)", function () {
184
- const expression1 = `A() + D(1) == 2`
185
- const il = RuleParser.toIL(expression1)
186
- expect(il).to.be.eql(["Eq",["MathAdd",["A"],["D",["Value",1]]],["Value",2]])
187
- })
188
- it("should be able to parse arithmetic (2)", function () {
189
- const expression1 = `A() + D(1) * 4 == 2 * A()`
190
- const il = RuleParser.toIL(expression1)
191
- expect(il).to.be.eql(["Eq",["MathAdd",["A"],["MathMul",["D",["Value",1]],["Value",4]]],["MathMul",["Value",2],["A"]]])
192
- })
193
- it("should be able to parse arithmetic (3)", function () {
194
- const expression1 = `A() + D(1)`
195
- const il = RuleParser.toIL(expression1)
196
- expect(il).to.be.eql(["MathAdd",["A"],["D",["Value",1]]])
197
- })
198
-
199
- it("should be able to parse && in arguments", function () {
200
- const expression1 = `A(B() && C())`
201
- const il = RuleParser.toIL(expression1)
202
- expect(il).to.be.eql(["A",["And",["B"],["C"]]])
203
- })
204
- it("should be able to parse default value operator", function () {
205
- const expression1 = `A() ?? 1`
206
- const il = RuleParser.toIL(expression1)
207
- expect(il).to.be.eql(["Default",["A"],["Value",1]])
208
- })
209
- it("should be able to parse default value operator in an expression", function () {
210
- const expression1 = `A() ?? 1 < 20`
211
- const il = RuleParser.toIL(expression1)
212
- expect(il).to.be.eql(["Lt",["Default",["A"],["Value",1]],["Value",20]])
213
- })
214
- });