@halleyassist/rule-parser 1.0.10 → 1.0.12

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/README.md CHANGED
@@ -1,3 +1,7 @@
1
1
  # rule-parser
2
2
 
3
3
  The grammar for HalleyAssist rules
4
+
5
+ ## Documentation
6
+
7
+ - [Built-in Functions](BUILTIN_FUNCTIONS.md) - Complete documentation of all built-in functions, operators, and data structures used in the rule parser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-parser",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleParser.production.js",
6
6
  "scripts": {
@@ -47,10 +47,10 @@ fname ::= [a-zA-z0-9]+
47
47
  fcall ::= fname WS* BEGIN_ARGUMENT WS* arguments? END_ARGUMENT
48
48
 
49
49
  between_number ||= (number_time | number) ((WS+ "AND" WS+) | (WS* "-" WS*)) (number_time | number)
50
- between_number_time ||= number_time ((WS+ "AND" WS+) | (WS* "-" WS*)) number_time
50
+ between_number_time ||= number_time ((WS+ "AND" WS+) | (WS* "-" WS*)) number_time (WS+ dow_range)?
51
51
  between_tod ||= number_tod ((WS+ "AND" WS+)) number_tod (WS+ dow_range)?
52
52
  between ||= "BETWEEN" WS+ (between_number | between_tod)
53
- dow ||= "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" | "SATURDAY" | "SUNDAY"
53
+ dow ||= "MONDAY" | "MON" | "TUESDAY" | "TUE" | "WEDNESDAY" | "WED" | "THURSDAY" | "THU" | "THUR" | "FRIDAY" | "FRI" | "SATURDAY" | "SAT" | "SUNDAY" | "SUN"
54
54
  dow_range ||= "ON" WS+ dow (WS+ "TO" WS+ dow)?
55
55
  between_time_only ||= "BETWEEN" WS+ between_number_time
56
56
  between_tod_only ||= "BETWEEN" WS+ between_tod
package/src/RuleParser.js CHANGED
@@ -30,6 +30,34 @@ const LogicalOperators = {
30
30
  "OR": 'Or',
31
31
  }
32
32
 
33
+ // Map abbreviations to canonical uppercase full form
34
+ const DOW_MAP = {
35
+ 'MON': 'MONDAY',
36
+ 'TUE': 'TUESDAY',
37
+ 'WED': 'WEDNESDAY',
38
+ 'THU': 'THURSDAY',
39
+ 'THUR': 'THURSDAY',
40
+ 'FRI': 'FRIDAY',
41
+ 'SAT': 'SATURDAY',
42
+ 'SUN': 'SUNDAY',
43
+ };
44
+
45
+ // Valid full day names
46
+ const VALID_DAYS = new Set(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']);
47
+
48
+ const normalizeDow = (text) => {
49
+ const upper = text.toUpperCase();
50
+ // Check if it's an abbreviation first
51
+ if (upper in DOW_MAP) {
52
+ return DOW_MAP[upper];
53
+ }
54
+ // Otherwise, check if it's a valid full name
55
+ if (VALID_DAYS.has(upper)) {
56
+ return upper;
57
+ }
58
+ throw new Error(`Invalid day of week: ${text}`);
59
+ };
60
+
33
61
  const Epsilon = 0.01
34
62
 
35
63
  class RuleParser {
@@ -63,27 +91,22 @@ class RuleParser {
63
91
  return ret
64
92
  }
65
93
  static _parseDowRange(dowRange) {
66
- const dow = []
67
-
68
94
  // dow_range can have 1 or 2 children (single day or range)
69
95
  if (dowRange.children.length === 1) {
70
- // Single day: ON MONDAY
71
- dow.push(dowRange.children[0].text.toLowerCase())
96
+ // Single day: ON MONDAY - return just the day string
97
+ return { start: normalizeDow(dowRange.children[0].text), end: normalizeDow(dowRange.children[0].text) };
72
98
  } else if (dowRange.children.length === 2) {
73
- // Range: ON MONDAY TO WEDNESDAY
74
- dow.push(dowRange.children[0].text.toLowerCase())
75
- dow.push(dowRange.children[1].text.toLowerCase())
99
+ // Range: ON MONDAY TO FRIDAY - return both start and end days
100
+ return { start: normalizeDow(dowRange.children[0].text), end: normalizeDow(dowRange.children[1].text) };
76
101
  } else {
77
- throw new Error(`Invalid dow_range with ${dowRange.children.length} children`)
102
+ throw new Error(`Invalid dow_range with ${dowRange.children.length} children`);
78
103
  }
79
-
80
- return dow
81
104
  }
82
105
  static _addDowToTods(startTod, endTod, dowRange) {
83
106
  if (dowRange && dowRange.type === 'dow_range') {
84
107
  const dow = RuleParser._parseDowRange(dowRange)
85
- startTod.dow = dow
86
- endTod.dow = dow
108
+ startTod.dow = dow.start
109
+ endTod.dow = dow.end
87
110
  }
88
111
  }
89
112
  static _parseTimePeriod(tp){
@@ -117,11 +140,13 @@ class RuleParser {
117
140
  // time_period_ago_between has children[0] = number_time, children[1] = between_tod_only
118
141
  const betweenTodOnly = tp.children[1]
119
142
  const betweenTod = betweenTodOnly.children[0]
120
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
121
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
143
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
144
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
122
145
 
123
146
  // Check if there's a dow_range at betweenTod.children[2]
124
147
  if (betweenTod.children.length > 2) {
148
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
149
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
125
150
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
126
151
  }
127
152
 
@@ -130,21 +155,36 @@ class RuleParser {
130
155
  case 'between_tod_only': {
131
156
  // between_tod_only has children[0] = between_tod node
132
157
  const betweenTod = tp.children[0]
133
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
134
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
158
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
159
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
135
160
 
136
161
  // Check if there's a dow_range at betweenTod.children[2]
137
162
  if (betweenTod.children.length > 2) {
163
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
164
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
138
165
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
139
166
  }
140
167
 
141
168
  return ["TimePeriodBetween", startTod, endTod]
142
169
  }
143
170
  case 'between_time_only': {
144
- // between_number_only has children[0] = between_number node
145
- const betweenNumber = tp.children[0]
146
- const startValue = RuleParser.__parseValue(betweenNumber.children[0])
147
- const endValue = RuleParser.__parseValue(betweenNumber.children[1])
171
+ // between_time_only has children[0] = between_number_time node
172
+ const betweenNumberTime = tp.children[0]
173
+ const startValue = RuleParser.__parseValue(betweenNumberTime.children[0])
174
+ const endValue = RuleParser.__parseValue(betweenNumberTime.children[1])
175
+
176
+ // Check if there's a dow_range at betweenNumberTime.children[2]
177
+ // If DOW filters are provided, append them as additional parameters
178
+ if (betweenNumberTime.children.length > 2 && betweenNumberTime.children[2].type === 'dow_range') {
179
+ const dow = RuleParser._parseDowRange(betweenNumberTime.children[2])
180
+ if (dow.start === dow.end) {
181
+ // Single day: ["TimePeriodBetween", start, end, "MONDAY"]
182
+ return ["TimePeriodBetween", startValue, endValue, dow.start]
183
+ } else {
184
+ // Range: ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
185
+ return ["TimePeriodBetween", startValue, endValue, dow.start, dow.end]
186
+ }
187
+ }
148
188
 
149
189
  return ["TimePeriodBetween", startValue, endValue]
150
190
  }
@@ -251,12 +291,43 @@ class RuleParser {
251
291
  }
252
292
  throw new Error(`Unknown arithmetic operand type ${type}`)
253
293
  }
294
+ static _isConstantValue(expr){
295
+ // Check if an expression is a constant value
296
+ return Array.isArray(expr) && expr.length === 2 && expr[0] === 'Value' && typeof expr[1] === 'number'
297
+ }
298
+
299
+ static _evaluateConstantArithmetic(operator, leftValue, rightValue){
300
+ // Evaluate constant arithmetic operations at parse time
301
+ switch(operator){
302
+ case 'MathAdd':
303
+ return leftValue + rightValue
304
+ case 'MathSub':
305
+ return leftValue - rightValue
306
+ case 'MathMul':
307
+ return leftValue * rightValue
308
+ case 'MathDiv':
309
+ return leftValue / rightValue
310
+ case 'MathMod':
311
+ return leftValue % rightValue
312
+ default:
313
+ return null
314
+ }
315
+ }
316
+
254
317
  static _parseArithmeticResult(result){
255
318
  assert(result.children.length == 3)
256
319
  const partA = RuleParser._parseArithmeticOperand(result.children[0])
257
320
  const operatorFn = ArithmeticOperators[result.children[1].text]
258
321
  const partB = RuleParser.__parseArithmeticResult(result, 2)
259
322
 
323
+ // Compile out constant expressions
324
+ if (RuleParser._isConstantValue(partA) && RuleParser._isConstantValue(partB)) {
325
+ const result = RuleParser._evaluateConstantArithmetic(operatorFn, partA[1], partB[1])
326
+ if (result !== null) {
327
+ return ['Value', result]
328
+ }
329
+ }
330
+
260
331
  return [operatorFn, partA, partB]
261
332
  }
262
333
 
@@ -1 +1 @@
1
- module.exports=[{"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_operand","bnf":[["fcall"],["number_time"],["number"]]},{"name":"%arithmetic_result7","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","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"],["time_period"],["number_time"],["number"],["number_tod"],["string"]]},{"name":"BEGIN_ARRAY","bnf":[["WS*",/\x5B/,"WS*"]]},{"name":"BEGIN_OBJECT","bnf":[["WS*",/\x7B/,"WS*"]]},{"name":"END_ARRAY","bnf":[["WS*",/\x5D/,"WS*"]]},{"name":"END_OBJECT","bnf":[["WS*",/\x7D/,"WS*"]]},{"name":"NAME_SEPARATOR","bnf":[["WS*",/\x3A/,"WS*"]]},{"name":"VALUE_SEPARATOR","bnf":[["WS*",/\x2C/,"WS*"]]},{"name":"%WS8","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"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":[[/[a-zA-z0-9]/]]},{"name":"fname","bnf":[["%fname10+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_number11","bnf":[["number_time"],["number"]],"fragment":true},{"name":"%%between_number1213","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%%between_number1214","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number12","bnf":[["%%between_number1213"],["%%between_number1214"]],"fragment":true},{"name":"%between_number15","bnf":[["number_time"],["number"]],"fragment":true},{"name":"between_number","bnf":[["%between_number11","%between_number12","%between_number15"]]},{"name":"%%between_number_time1617","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%%between_number_time1618","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number_time16","bnf":[["%%between_number_time1617"],["%%between_number_time1618"]],"fragment":true},{"name":"between_number_time","bnf":[["number_time","%between_number_time16","number_time"]]},{"name":"%%between_tod1920","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod19","bnf":[["%%between_tod1920"]],"fragment":true},{"name":"%between_tod21","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["number_tod","%between_tod19","number_tod","%between_tod21?"]]},{"name":"%between22","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between22"]]},{"name":"dow","bnf":[[/[Mm]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Uu]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ww]/,/[Ee]/,/[Dd]/,/[Nn]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ff]/,/[Rr]/,/[Ii]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Aa]/,/[Tt]/,/[Uu]/,/[Rr]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Uu]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/]]},{"name":"%dow_range23","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow","%dow_range23?"]]},{"name":"between_time_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_number_time"]]},{"name":"between_tod_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_tod"]]},{"name":"%AND24","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND25","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND24"],["%AND25"]]},{"name":"%OR26","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR27","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR26"],["%OR27"]]},{"name":"AGO","bnf":[[/[Aa]/,/[Gg]/,/[Oo]/]]},{"name":"GT","bnf":[["\">\""]]},{"name":"LT","bnf":[["\"<\""]]},{"name":"GTE","bnf":[["\">=\""]]},{"name":"LTE","bnf":[["\"<=\""]]},{"name":"IS","bnf":[[/[Ii]/,/[Ss]/]]},{"name":"EQ","bnf":[["\"==\""],["\"=\""]]},{"name":"NEQ","bnf":[["\"!=\""]]},{"name":"%NOT28","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT29","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT28"],["%NOT29"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%%array3031","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array30","bnf":[["value","%%array3031*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array30?","END_ARRAY"]]},{"name":"unit","bnf":[[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/,/[Ss]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/,/[Ss]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/,/[Ss]/],[/[Dd]/,/[Aa]/,/[Yy]/,/[Ss]/],[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/],[/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/]]},{"name":"%%number3233","bnf":[[/[0-9]/]]},{"name":"%number32","bnf":[["%%number3233+"]],"fragment":true},{"name":"%%number3435","bnf":[[/[0-9]/]]},{"name":"%number34","bnf":[["\".\"","%%number3435+"]],"fragment":true},{"name":"%%number3637","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%%%number363839","bnf":[[/[0-9]/]]},{"name":"%%number3638","bnf":[["\"0\""],[/[1-9]/,"%%%number363839*"]],"fragment":true},{"name":"%number36","bnf":[["\"e\"","%%number3637?","%%number3638"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number32","%number34?","%number36?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%%number_tod4041","bnf":[[/[0-9]/]]},{"name":"%number_tod40","bnf":[["%%number_tod4041+"]],"fragment":true},{"name":"%%number_tod4243","bnf":[[/[0-9]/]]},{"name":"%number_tod42","bnf":[["%%number_tod4243+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod40","\":\"","%number_tod42"]]},{"name":"%time_period_ago44","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time","%time_period_ago44*","WS+","AGO"]]},{"name":"%time_period_ago_between45","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time","%time_period_ago_between45*","WS+","AGO","WS+","between_tod_only"]]},{"name":"time_period_const","bnf":[[/[Tt]/,/[Oo]/,/[Dd]/,/[Aa]/,/[Yy]/],["time_period_ago"]]},{"name":"time_period","bnf":[["time_period_ago_between"],["time_period_const"],["between_tod_only"],["between_time_only"]]},{"name":"%%string4647","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%%string4648","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string46","bnf":[["%%string4647"],[/\x5C/,"%%string4648"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string46*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
1
+ module.exports=[{"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_operand","bnf":[["fcall"],["number_time"],["number"]]},{"name":"%arithmetic_result7","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","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"],["time_period"],["number_time"],["number"],["number_tod"],["string"]]},{"name":"BEGIN_ARRAY","bnf":[["WS*",/\x5B/,"WS*"]]},{"name":"BEGIN_OBJECT","bnf":[["WS*",/\x7B/,"WS*"]]},{"name":"END_ARRAY","bnf":[["WS*",/\x5D/,"WS*"]]},{"name":"END_OBJECT","bnf":[["WS*",/\x7D/,"WS*"]]},{"name":"NAME_SEPARATOR","bnf":[["WS*",/\x3A/,"WS*"]]},{"name":"VALUE_SEPARATOR","bnf":[["WS*",/\x2C/,"WS*"]]},{"name":"%WS8","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"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":[[/[a-zA-z0-9]/]]},{"name":"fname","bnf":[["%fname10+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_number11","bnf":[["number_time"],["number"]],"fragment":true},{"name":"%%between_number1213","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%%between_number1214","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number12","bnf":[["%%between_number1213"],["%%between_number1214"]],"fragment":true},{"name":"%between_number15","bnf":[["number_time"],["number"]],"fragment":true},{"name":"between_number","bnf":[["%between_number11","%between_number12","%between_number15"]]},{"name":"%%between_number_time1617","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%%between_number_time1618","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number_time16","bnf":[["%%between_number_time1617"],["%%between_number_time1618"]],"fragment":true},{"name":"%between_number_time19","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_number_time","bnf":[["number_time","%between_number_time16","number_time","%between_number_time19?"]]},{"name":"%%between_tod2021","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod20","bnf":[["%%between_tod2021"]],"fragment":true},{"name":"%between_tod22","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["number_tod","%between_tod20","number_tod","%between_tod22?"]]},{"name":"%between23","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between23"]]},{"name":"dow","bnf":[[/[Mm]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Oo]/,/[Nn]/],[/[Tt]/,/[Uu]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Uu]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Dd]/,/[Nn]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ww]/,/[Ee]/,/[Dd]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Hh]/,/[Uu]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/],[/[Ff]/,/[Rr]/,/[Ii]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ff]/,/[Rr]/,/[Ii]/],[/[Ss]/,/[Aa]/,/[Tt]/,/[Uu]/,/[Rr]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Aa]/,/[Tt]/],[/[Ss]/,/[Uu]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Uu]/,/[Nn]/]]},{"name":"%dow_range24","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow","%dow_range24?"]]},{"name":"between_time_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_number_time"]]},{"name":"between_tod_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_tod"]]},{"name":"%AND25","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND26","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND25"],["%AND26"]]},{"name":"%OR27","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR28","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR27"],["%OR28"]]},{"name":"AGO","bnf":[[/[Aa]/,/[Gg]/,/[Oo]/]]},{"name":"GT","bnf":[["\">\""]]},{"name":"LT","bnf":[["\"<\""]]},{"name":"GTE","bnf":[["\">=\""]]},{"name":"LTE","bnf":[["\"<=\""]]},{"name":"IS","bnf":[[/[Ii]/,/[Ss]/]]},{"name":"EQ","bnf":[["\"==\""],["\"=\""]]},{"name":"NEQ","bnf":[["\"!=\""]]},{"name":"%NOT29","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT30","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT29"],["%NOT30"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%%array3132","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array31","bnf":[["value","%%array3132*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array31?","END_ARRAY"]]},{"name":"unit","bnf":[[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/,/[Ss]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/,/[Ss]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/,/[Ss]/],[/[Dd]/,/[Aa]/,/[Yy]/,/[Ss]/],[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/],[/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/]]},{"name":"%%number3334","bnf":[[/[0-9]/]]},{"name":"%number33","bnf":[["%%number3334+"]],"fragment":true},{"name":"%%number3536","bnf":[[/[0-9]/]]},{"name":"%number35","bnf":[["\".\"","%%number3536+"]],"fragment":true},{"name":"%%number3738","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%%%number373940","bnf":[[/[0-9]/]]},{"name":"%%number3739","bnf":[["\"0\""],[/[1-9]/,"%%%number373940*"]],"fragment":true},{"name":"%number37","bnf":[["\"e\"","%%number3738?","%%number3739"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number33","%number35?","%number37?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%%number_tod4142","bnf":[[/[0-9]/]]},{"name":"%number_tod41","bnf":[["%%number_tod4142+"]],"fragment":true},{"name":"%%number_tod4344","bnf":[[/[0-9]/]]},{"name":"%number_tod43","bnf":[["%%number_tod4344+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod41","\":\"","%number_tod43"]]},{"name":"%time_period_ago45","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time","%time_period_ago45*","WS+","AGO"]]},{"name":"%time_period_ago_between46","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time","%time_period_ago_between46*","WS+","AGO","WS+","between_tod_only"]]},{"name":"time_period_const","bnf":[[/[Tt]/,/[Oo]/,/[Dd]/,/[Aa]/,/[Yy]/],["time_period_ago"]]},{"name":"time_period","bnf":[["time_period_ago_between"],["time_period_const"],["between_tod_only"],["between_time_only"]]},{"name":"%%string4748","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%%string4749","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string47","bnf":[["%%string4748"],[/\x5C/,"%%string4749"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string47*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
@@ -30,6 +30,34 @@ const LogicalOperators = {
30
30
  "OR": 'Or',
31
31
  }
32
32
 
33
+ // Map abbreviations to canonical uppercase full form
34
+ const DOW_MAP = {
35
+ 'MON': 'MONDAY',
36
+ 'TUE': 'TUESDAY',
37
+ 'WED': 'WEDNESDAY',
38
+ 'THU': 'THURSDAY',
39
+ 'THUR': 'THURSDAY',
40
+ 'FRI': 'FRIDAY',
41
+ 'SAT': 'SATURDAY',
42
+ 'SUN': 'SUNDAY',
43
+ };
44
+
45
+ // Valid full day names
46
+ const VALID_DAYS = new Set(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']);
47
+
48
+ const normalizeDow = (text) => {
49
+ const upper = text.toUpperCase();
50
+ // Check if it's an abbreviation first
51
+ if (upper in DOW_MAP) {
52
+ return DOW_MAP[upper];
53
+ }
54
+ // Otherwise, check if it's a valid full name
55
+ if (VALID_DAYS.has(upper)) {
56
+ return upper;
57
+ }
58
+ throw new Error(`Invalid day of week: ${text}`);
59
+ };
60
+
33
61
  const Epsilon = 0.01
34
62
 
35
63
  class RuleParser {
@@ -63,27 +91,22 @@ class RuleParser {
63
91
  return ret
64
92
  }
65
93
  static _parseDowRange(dowRange) {
66
- const dow = []
67
-
68
94
  // dow_range can have 1 or 2 children (single day or range)
69
95
  if (dowRange.children.length === 1) {
70
- // Single day: ON MONDAY
71
- dow.push(dowRange.children[0].text.toLowerCase())
96
+ // Single day: ON MONDAY - return just the day string
97
+ return { start: normalizeDow(dowRange.children[0].text), end: normalizeDow(dowRange.children[0].text) };
72
98
  } else if (dowRange.children.length === 2) {
73
- // Range: ON MONDAY TO WEDNESDAY
74
- dow.push(dowRange.children[0].text.toLowerCase())
75
- dow.push(dowRange.children[1].text.toLowerCase())
99
+ // Range: ON MONDAY TO FRIDAY - return both start and end days
100
+ return { start: normalizeDow(dowRange.children[0].text), end: normalizeDow(dowRange.children[1].text) };
76
101
  } else {
77
- throw new Error(`Invalid dow_range with ${dowRange.children.length} children`)
102
+ throw new Error(`Invalid dow_range with ${dowRange.children.length} children`);
78
103
  }
79
-
80
- return dow
81
104
  }
82
105
  static _addDowToTods(startTod, endTod, dowRange) {
83
106
  if (dowRange && dowRange.type === 'dow_range') {
84
107
  const dow = RuleParser._parseDowRange(dowRange)
85
- startTod.dow = dow
86
- endTod.dow = dow
108
+ startTod.dow = dow.start
109
+ endTod.dow = dow.end
87
110
  }
88
111
  }
89
112
  static _parseTimePeriod(tp){
@@ -117,11 +140,13 @@ class RuleParser {
117
140
  // time_period_ago_between has children[0] = number_time, children[1] = between_tod_only
118
141
  const betweenTodOnly = tp.children[1]
119
142
  const betweenTod = betweenTodOnly.children[0]
120
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
121
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
143
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
144
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
122
145
 
123
146
  // Check if there's a dow_range at betweenTod.children[2]
124
147
  if (betweenTod.children.length > 2) {
148
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
149
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
125
150
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
126
151
  }
127
152
 
@@ -130,21 +155,36 @@ class RuleParser {
130
155
  case 'between_tod_only': {
131
156
  // between_tod_only has children[0] = between_tod node
132
157
  const betweenTod = tp.children[0]
133
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
134
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
158
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
159
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
135
160
 
136
161
  // Check if there's a dow_range at betweenTod.children[2]
137
162
  if (betweenTod.children.length > 2) {
163
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
164
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
138
165
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
139
166
  }
140
167
 
141
168
  return ["TimePeriodBetween", startTod, endTod]
142
169
  }
143
170
  case 'between_time_only': {
144
- // between_number_only has children[0] = between_number node
145
- const betweenNumber = tp.children[0]
146
- const startValue = RuleParser.__parseValue(betweenNumber.children[0])
147
- const endValue = RuleParser.__parseValue(betweenNumber.children[1])
171
+ // between_time_only has children[0] = between_number_time node
172
+ const betweenNumberTime = tp.children[0]
173
+ const startValue = RuleParser.__parseValue(betweenNumberTime.children[0])
174
+ const endValue = RuleParser.__parseValue(betweenNumberTime.children[1])
175
+
176
+ // Check if there's a dow_range at betweenNumberTime.children[2]
177
+ // If DOW filters are provided, append them as additional parameters
178
+ if (betweenNumberTime.children.length > 2 && betweenNumberTime.children[2].type === 'dow_range') {
179
+ const dow = RuleParser._parseDowRange(betweenNumberTime.children[2])
180
+ if (dow.start === dow.end) {
181
+ // Single day: ["TimePeriodBetween", start, end, "MONDAY"]
182
+ return ["TimePeriodBetween", startValue, endValue, dow.start]
183
+ } else {
184
+ // Range: ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
185
+ return ["TimePeriodBetween", startValue, endValue, dow.start, dow.end]
186
+ }
187
+ }
148
188
 
149
189
  return ["TimePeriodBetween", startValue, endValue]
150
190
  }
@@ -251,12 +291,43 @@ class RuleParser {
251
291
  }
252
292
  throw new Error(`Unknown arithmetic operand type ${type}`)
253
293
  }
294
+ static _isConstantValue(expr){
295
+ // Check if an expression is a constant value
296
+ return Array.isArray(expr) && expr.length === 2 && expr[0] === 'Value' && typeof expr[1] === 'number'
297
+ }
298
+
299
+ static _evaluateConstantArithmetic(operator, leftValue, rightValue){
300
+ // Evaluate constant arithmetic operations at parse time
301
+ switch(operator){
302
+ case 'MathAdd':
303
+ return leftValue + rightValue
304
+ case 'MathSub':
305
+ return leftValue - rightValue
306
+ case 'MathMul':
307
+ return leftValue * rightValue
308
+ case 'MathDiv':
309
+ return leftValue / rightValue
310
+ case 'MathMod':
311
+ return leftValue % rightValue
312
+ default:
313
+ return null
314
+ }
315
+ }
316
+
254
317
  static _parseArithmeticResult(result){
255
318
  assert(result.children.length == 3)
256
319
  const partA = RuleParser._parseArithmeticOperand(result.children[0])
257
320
  const operatorFn = ArithmeticOperators[result.children[1].text]
258
321
  const partB = RuleParser.__parseArithmeticResult(result, 2)
259
322
 
323
+ // Compile out constant expressions
324
+ if (RuleParser._isConstantValue(partA) && RuleParser._isConstantValue(partB)) {
325
+ const result = RuleParser._evaluateConstantArithmetic(operatorFn, partA[1], partB[1])
326
+ if (result !== null) {
327
+ return ['Value', result]
328
+ }
329
+ }
330
+
260
331
  return [operatorFn, partA, partB]
261
332
  }
262
333