@halleyassist/rule-parser 0.1.1

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/.eslintrc.yml ADDED
@@ -0,0 +1,8 @@
1
+ env:
2
+ node: true
3
+ es2021: true
4
+ extends: 'eslint:recommended'
5
+ parserOptions:
6
+ ecmaVersion: 12
7
+ sourceType: module
8
+ rules: {}
@@ -0,0 +1,69 @@
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')"
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # rule-parser
2
+
3
+ The grammar for HalleyAssist rules
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./src/RuleParser')
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@halleyassist/rule-parser",
3
+ "version": "0.1.1",
4
+ "description": "The grammar for HalleyAssist rules",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/HalleyAssist/rule-parser.git"
12
+ },
13
+ "author": "",
14
+ "license": "ISC",
15
+ "bugs": {
16
+ "url": "https://github.com/HalleyAssist/rule-parser/issues"
17
+ },
18
+ "homepage": "https://github.com/HalleyAssist/rule-parser#readme",
19
+ "dependencies": {
20
+ "ebnf": "^1.9.1"
21
+ },
22
+ "devDependencies": {
23
+ "chai": "^4.4.1",
24
+ "mocha": "^10.4.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "registry": "https://registry.npmjs.org/"
29
+ }
30
+ }
@@ -0,0 +1,359 @@
1
+ const { Grammars, Parser } = require('ebnf'),
2
+ assert = require('assert')
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'
83
+
84
+ const ArithmeticOperators = {
85
+ "+": 'MathAdd',
86
+ "-": 'MathSub',
87
+ "/": 'MathDiv',
88
+ "*": 'MathMul',
89
+ "%": 'MathMod',
90
+ "??": "Default"
91
+ }
92
+
93
+ const OperatorFn = {
94
+ ">": "Gt",
95
+ "<": "Lt",
96
+ ">=": "Gte",
97
+ "<=": "Lte",
98
+ "==": "Eq",
99
+ "=": "Eq",
100
+ "!=": "Neq"
101
+ }
102
+
103
+ const LogicalOperators = {
104
+ "&&": 'And',
105
+ "AND": 'And',
106
+ "and": 'And',
107
+ "||": 'Or',
108
+ "OR": 'Or',
109
+ "or": 'Or',
110
+ }
111
+
112
+ const Epsilon = 0.01
113
+
114
+ class RuleParser {
115
+ static toAst(txt){
116
+ let ret
117
+ //if(process.env.NODE_ENV === 'test') {
118
+ // parser.debug = true
119
+ //}
120
+ ret = parser.getAST(txt, target);
121
+
122
+ if(ret){
123
+ return ret.children[0]
124
+ }
125
+ }
126
+ static _parseArgument(argument){
127
+ assert(argument.type === 'argument')
128
+ const child = argument.children[0]
129
+ return RuleParser._buildExpressionGroup(child)
130
+ }
131
+ static _parseFcall(fcall){
132
+ const fname = fcall.children[0]
133
+ const ret = [fname.text]
134
+ if(fcall.children.length != 1){
135
+ const args = fcall.children[1]
136
+ for(const a of args.children){
137
+ ret.push(RuleParser._parseArgument(a))
138
+ }
139
+ }
140
+ return ret
141
+ }
142
+ static _parseTimePeriod(tp){
143
+ switch(tp.type){
144
+ case 'time_period_const':
145
+ return ["TimePeriodConst", tp.text]
146
+ case 'between':
147
+ return ["TimePeriodBetween", RuleParser.__parseValue(tp.children[0]?.children[0]), RuleParser.__parseValue(tp.children[0]?.children[1])]
148
+ }
149
+ }
150
+ static __parseValue(child){
151
+ const type = child.type
152
+ switch(type){
153
+ case 'string': {
154
+ const str = child.text
155
+ return str.slice(1, -1)
156
+ }
157
+ case 'number':
158
+ return parseFloat(child.text)
159
+ case 'number_tod': {
160
+ const tokens = child.text.split(':')
161
+ if (tokens.length !== 2) {
162
+ throw new Error(`Invalid time of day, ${child.text} should be ##:##`)
163
+ }
164
+ const hours = parseInt(tokens[0])
165
+ const minutes = parseInt(tokens[1])
166
+ const tod = hours * 100 + minutes
167
+ const ret = { hours, minutes, tod }
168
+ if (!isNaN(tod) && ret.hours >= 0 && ret.hours <= 24 && ret.minutes >= 0 && ret.minutes < 60) {
169
+ return ret
170
+ }
171
+ throw new Error(`Invalid time of day, ${child.text} -> [${tokens.join(', ')}] -> ${hours}h${minutes}m -> ${tod}`)
172
+ }
173
+ case 'number_time': {
174
+ const nt = child
175
+ const mult = parseFloat(nt.children[0].text)
176
+ switch(nt.children[1].text){
177
+ case 'seconds':
178
+ case 'second':
179
+ return mult
180
+ case 'minutes':
181
+ case 'minute':
182
+ case 'mins':
183
+ case 'min':
184
+ return mult * 60
185
+ case 'hours':
186
+ case 'hour':
187
+ return mult * 60 * 60
188
+ case 'days':
189
+ case 'day':
190
+ return mult * 60 * 60 * 24
191
+ case 'weeks':
192
+ case 'week':
193
+ return mult * 60 * 60 * 24 * 7
194
+ }
195
+ throw new Error(`Invalid exponent ${nt.children[1].text}`)
196
+ }
197
+ case 'true':
198
+ return true
199
+ case 'false':
200
+ return false
201
+ case 'array': {
202
+ const ret = []
203
+ for(const c of child.children){
204
+ ret.push(RuleParser.__parseValue(c.children[0]))
205
+ }
206
+ return ret;
207
+ }
208
+ default:
209
+ throw new Error(`Unknown value type ${type}`)
210
+ }
211
+ }
212
+ static _parseValue(value){
213
+ const child = value.children[0]
214
+
215
+ const type = child.type
216
+ switch(type){
217
+ case 'time_period': {
218
+ const tp = child.children[0]
219
+ return RuleParser._parseTimePeriod(tp)
220
+ }
221
+ default:
222
+ return ['Value', RuleParser.__parseValue(child)]
223
+ }
224
+ }
225
+ static _parseSimpleResult(result){
226
+ assert(result.children.length == 1)
227
+ const child = result.children[0]
228
+ const type = child.type
229
+ switch(type){
230
+ case 'fcall':
231
+ return RuleParser._parseFcall(child)
232
+ case 'value':
233
+ return RuleParser._parseValue(child)
234
+ }
235
+ return null
236
+ }
237
+ static _parseArithmeticResult(result){
238
+ assert(result.children.length == 3)
239
+ const partA = this._parseSimpleResult(result.children[0])
240
+ const operatorFn = ArithmeticOperators[result.children[1].text]
241
+ const partB = this.__parseResult(result, 2)
242
+
243
+ return [operatorFn, partA, partB]
244
+ }
245
+
246
+ static __parseResult(result, idx){
247
+ const child = result.children[idx]
248
+ const type = child.type
249
+ switch(type){
250
+ case 'simple_result':
251
+ return RuleParser._parseSimpleResult(child)
252
+ case 'arithmetic_result':
253
+ return RuleParser._parseArithmeticResult(child)
254
+ }
255
+
256
+ throw new Error(`Unknown result node ${type}`)
257
+ }
258
+ static _parseResult(result){
259
+ assert(result.children.length == 1)
260
+
261
+ return RuleParser.__parseResult(result, 0)
262
+ }
263
+ static _parseStdExpression(expr){
264
+ assert(expr.type === 'standard_expression')
265
+ switch(expr.children.length){
266
+ case 1:
267
+ return RuleParser._parseResult(expr.children[0])
268
+ case 2: {
269
+ const rhs = expr.children[1]
270
+ switch(rhs.type){
271
+ case 'between_tod':
272
+ case 'between_number':
273
+ case 'between':
274
+ return ['Between', RuleParser._parseResult(expr.children[0]), ['Value', RuleParser.__parseValue(rhs.children[0].children[0])], ['Value', RuleParser.__parseValue(rhs.children[0].children[1])]]
275
+ case 'basic_rhs':
276
+ return [OperatorFn[rhs.children[0].text], RuleParser._parseResult(expr.children[0]), RuleParser._parseResult(rhs.children[1])]
277
+ case 'eq_approx': {
278
+ const rhsValue = RuleParser._parseResult(rhs.children[1])
279
+ assert(rhsValue[0] === 'Value')
280
+ const ret = ['Between', RuleParser._parseResult(expr.children[0]), ['Value', rhsValue[1] - Epsilon], ['Value', rhsValue[1] + Epsilon]]
281
+ if(rhs.children[0].text === '!='){
282
+ return ['Not', ret]
283
+ }
284
+ return ret
285
+ }
286
+
287
+ default:
288
+ throw new Error(`unable to parse std expression, unknown rhs type ${rhs.type}`)
289
+ }
290
+ }
291
+
292
+ default:
293
+ throw new Error(`unable to parse std expression, unknown number of children ${expr.children.length}`)
294
+ }
295
+ }
296
+ static buildLogical(members, fn){
297
+ return [fn, ...members]
298
+ }
299
+ static _buildExpressionGroup(ast){
300
+ let ret = []
301
+ let currentLogical = null
302
+ for(const expr of ast.children){
303
+ if(expr.type == 'logical_operator') {
304
+ const logicalOperator = expr.text.trim()
305
+ const operatorFn = LogicalOperators[logicalOperator]
306
+ assert(operatorFn, `Unknown logical operator ${logicalOperator}`)
307
+ if(currentLogical === null || currentLogical !== operatorFn){
308
+ if(ret.length > 1){
309
+ ret = [RuleParser.buildLogical(ret, currentLogical)]
310
+ }
311
+ currentLogical = operatorFn
312
+ }
313
+ }else{
314
+ ret.push(RuleParser._exprToIL(expr))
315
+ }
316
+ }
317
+ if(ret.length == 0){
318
+ throw new Error('invalid rule')
319
+ }
320
+ if(ret.length == 1){
321
+ return ret[0]
322
+ }
323
+ return RuleParser.buildLogical(ret, currentLogical)
324
+ }
325
+ static _parseParenthesisExpression(expr){
326
+ return RuleParser._buildExpressionGroup(expr.children[0])
327
+ }
328
+ static _exprToIL(expr){
329
+ assert(expr.type === 'expression')
330
+ assert(expr.children.length === 1)
331
+ const eInner = expr.children[0]
332
+ switch(eInner.type){
333
+ case 'standard_expression':
334
+ return RuleParser._parseStdExpression(eInner)
335
+ case 'not_expression': {
336
+ const child = eInner.children[0]
337
+ let result
338
+ switch(child.type){
339
+ case 'parenthesis_expression':
340
+ result = RuleParser._parseParenthesisExpression(child)
341
+ break;
342
+ default:
343
+ result = RuleParser._parseResult(child)
344
+ }
345
+ return ['Not', result]
346
+ }
347
+ case 'parenthesis_expression':
348
+ return RuleParser._parseParenthesisExpression(eInner)
349
+ default:
350
+ throw new Error(`unknown type of expression ${eInner.type}`)
351
+ }
352
+ }
353
+ static toIL(txt){
354
+ const ast = RuleParser.toAst(txt)
355
+ if(!ast) throw new Error(`failed to parse ${txt}`)
356
+ return RuleParser._buildExpressionGroup(ast)
357
+ }
358
+ }
359
+ module.exports = RuleParser
@@ -0,0 +1,214 @@
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
+ });