@halleyassist/rule-parser 1.0.11 → 1.0.13

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.11",
3
+ "version": "1.0.13",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleParser.production.js",
6
6
  "scripts": {
package/src/RuleParser.js CHANGED
@@ -68,7 +68,7 @@ class RuleParser {
68
68
  ParserCache = new Parser(ParserRules, {debug: false})
69
69
  }
70
70
 
71
- ret = ParserCache.getAST(txt, 'statement_main');
71
+ ret = ParserCache.getAST(txt.trim(), 'statement_main');
72
72
 
73
73
  if(ret){
74
74
  return ret.children[0]
@@ -91,25 +91,22 @@ class RuleParser {
91
91
  return ret
92
92
  }
93
93
  static _parseDowRange(dowRange) {
94
- const dow = [];
95
94
  // dow_range can have 1 or 2 children (single day or range)
96
95
  if (dowRange.children.length === 1) {
97
- // Single day: ON MONDAY
98
- dow.push(normalizeDow(dowRange.children[0].text));
96
+ // Single day: ON MONDAY - return just the day string
97
+ return { start: normalizeDow(dowRange.children[0].text), end: normalizeDow(dowRange.children[0].text) };
99
98
  } else if (dowRange.children.length === 2) {
100
- // Range: ON MONDAY TO WEDNESDAY
101
- dow.push(normalizeDow(dowRange.children[0].text));
102
- dow.push(normalizeDow(dowRange.children[1].text));
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) };
103
101
  } else {
104
102
  throw new Error(`Invalid dow_range with ${dowRange.children.length} children`);
105
103
  }
106
- return dow;
107
104
  }
108
105
  static _addDowToTods(startTod, endTod, dowRange) {
109
106
  if (dowRange && dowRange.type === 'dow_range') {
110
107
  const dow = RuleParser._parseDowRange(dowRange)
111
- startTod.dow = dow
112
- endTod.dow = dow
108
+ startTod.dow = dow.start
109
+ endTod.dow = dow.end
113
110
  }
114
111
  }
115
112
  static _parseTimePeriod(tp){
@@ -140,27 +137,47 @@ class RuleParser {
140
137
  }
141
138
  return ["TimePeriodConst", tp.text]
142
139
  case 'time_period_ago_between': {
143
- // time_period_ago_between has children[0] = number_time, children[1] = between_tod_only
144
- const betweenTodOnly = tp.children[1]
140
+ // time_period_ago_between has: number_time (WS+ number_time)* WS+ AGO WS+ between_tod_only
141
+ // We need to extract all number_time children and sum them up, then return TimePeriodBetweenAgo
142
+ let totalSeconds = 0
143
+ let betweenTodOnly = null
144
+
145
+ // Find all number_time children and the between_tod_only child
146
+ for (let i = 0; i < tp.children.length; i++) {
147
+ if (tp.children[i].type === 'number_time') {
148
+ totalSeconds += RuleParser.__parseValue(tp.children[i])
149
+ } else if (tp.children[i].type === 'between_tod_only') {
150
+ betweenTodOnly = tp.children[i]
151
+ }
152
+ }
153
+
154
+ // This should always be present based on the grammar, but check defensively
155
+ if (!betweenTodOnly) {
156
+ throw new Error('time_period_ago_between requires between_tod_only child')
157
+ }
158
+
145
159
  const betweenTod = betweenTodOnly.children[0]
146
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
147
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
160
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
161
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
148
162
 
149
163
  // Check if there's a dow_range at betweenTod.children[2]
164
+ // Note: startTod and endTod should always be objects from number_tod parsing
150
165
  if (betweenTod.children.length > 2) {
151
166
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
152
167
  }
153
168
 
154
- return ["TimePeriodBetween", startTod, endTod]
169
+ return ["TimePeriodBetweenAgo", totalSeconds, startTod, endTod]
155
170
  }
156
171
  case 'between_tod_only': {
157
172
  // between_tod_only has children[0] = between_tod node
158
173
  const betweenTod = tp.children[0]
159
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
160
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
174
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
175
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
161
176
 
162
177
  // Check if there's a dow_range at betweenTod.children[2]
163
178
  if (betweenTod.children.length > 2) {
179
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
180
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
164
181
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
165
182
  }
166
183
 
@@ -176,8 +193,13 @@ class RuleParser {
176
193
  // If DOW filters are provided, append them as additional parameters
177
194
  if (betweenNumberTime.children.length > 2 && betweenNumberTime.children[2].type === 'dow_range') {
178
195
  const dow = RuleParser._parseDowRange(betweenNumberTime.children[2])
179
- // Append DOW as additional arguments: ["TimePeriodBetween", start, end, "MONDAY"] or ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
180
- return ["TimePeriodBetween", startValue, endValue, ...dow]
196
+ if (dow.start === dow.end) {
197
+ // Single day: ["TimePeriodBetween", start, end, "MONDAY"]
198
+ return ["TimePeriodBetween", startValue, endValue, dow.start]
199
+ } else {
200
+ // Range: ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
201
+ return ["TimePeriodBetween", startValue, endValue, dow.start, dow.end]
202
+ }
181
203
  }
182
204
 
183
205
  return ["TimePeriodBetween", startValue, endValue]
@@ -285,12 +307,43 @@ class RuleParser {
285
307
  }
286
308
  throw new Error(`Unknown arithmetic operand type ${type}`)
287
309
  }
310
+ static _isConstantValue(expr){
311
+ // Check if an expression is a constant value
312
+ return Array.isArray(expr) && expr.length === 2 && expr[0] === 'Value' && typeof expr[1] === 'number'
313
+ }
314
+
315
+ static _evaluateConstantArithmetic(operator, leftValue, rightValue){
316
+ // Evaluate constant arithmetic operations at parse time
317
+ switch(operator){
318
+ case 'MathAdd':
319
+ return leftValue + rightValue
320
+ case 'MathSub':
321
+ return leftValue - rightValue
322
+ case 'MathMul':
323
+ return leftValue * rightValue
324
+ case 'MathDiv':
325
+ return leftValue / rightValue
326
+ case 'MathMod':
327
+ return leftValue % rightValue
328
+ default:
329
+ return null
330
+ }
331
+ }
332
+
288
333
  static _parseArithmeticResult(result){
289
334
  assert(result.children.length == 3)
290
335
  const partA = RuleParser._parseArithmeticOperand(result.children[0])
291
336
  const operatorFn = ArithmeticOperators[result.children[1].text]
292
337
  const partB = RuleParser.__parseArithmeticResult(result, 2)
293
338
 
339
+ // Compile out constant expressions
340
+ if (RuleParser._isConstantValue(partA) && RuleParser._isConstantValue(partB)) {
341
+ const result = RuleParser._evaluateConstantArithmetic(operatorFn, partA[1], partB[1])
342
+ if (result !== null) {
343
+ return ['Value', result]
344
+ }
345
+ }
346
+
294
347
  return [operatorFn, partA, partB]
295
348
  }
296
349
 
@@ -68,7 +68,7 @@ class RuleParser {
68
68
  ParserCache = new Parser(ParserRules, {debug: false})
69
69
  }
70
70
 
71
- ret = ParserCache.getAST(txt, 'statement_main');
71
+ ret = ParserCache.getAST(txt.trim(), 'statement_main');
72
72
 
73
73
  if(ret){
74
74
  return ret.children[0]
@@ -91,25 +91,22 @@ class RuleParser {
91
91
  return ret
92
92
  }
93
93
  static _parseDowRange(dowRange) {
94
- const dow = [];
95
94
  // dow_range can have 1 or 2 children (single day or range)
96
95
  if (dowRange.children.length === 1) {
97
- // Single day: ON MONDAY
98
- dow.push(normalizeDow(dowRange.children[0].text));
96
+ // Single day: ON MONDAY - return just the day string
97
+ return { start: normalizeDow(dowRange.children[0].text), end: normalizeDow(dowRange.children[0].text) };
99
98
  } else if (dowRange.children.length === 2) {
100
- // Range: ON MONDAY TO WEDNESDAY
101
- dow.push(normalizeDow(dowRange.children[0].text));
102
- dow.push(normalizeDow(dowRange.children[1].text));
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) };
103
101
  } else {
104
102
  throw new Error(`Invalid dow_range with ${dowRange.children.length} children`);
105
103
  }
106
- return dow;
107
104
  }
108
105
  static _addDowToTods(startTod, endTod, dowRange) {
109
106
  if (dowRange && dowRange.type === 'dow_range') {
110
107
  const dow = RuleParser._parseDowRange(dowRange)
111
- startTod.dow = dow
112
- endTod.dow = dow
108
+ startTod.dow = dow.start
109
+ endTod.dow = dow.end
113
110
  }
114
111
  }
115
112
  static _parseTimePeriod(tp){
@@ -140,27 +137,47 @@ class RuleParser {
140
137
  }
141
138
  return ["TimePeriodConst", tp.text]
142
139
  case 'time_period_ago_between': {
143
- // time_period_ago_between has children[0] = number_time, children[1] = between_tod_only
144
- const betweenTodOnly = tp.children[1]
140
+ // time_period_ago_between has: number_time (WS+ number_time)* WS+ AGO WS+ between_tod_only
141
+ // We need to extract all number_time children and sum them up, then return TimePeriodBetweenAgo
142
+ let totalSeconds = 0
143
+ let betweenTodOnly = null
144
+
145
+ // Find all number_time children and the between_tod_only child
146
+ for (let i = 0; i < tp.children.length; i++) {
147
+ if (tp.children[i].type === 'number_time') {
148
+ totalSeconds += RuleParser.__parseValue(tp.children[i])
149
+ } else if (tp.children[i].type === 'between_tod_only') {
150
+ betweenTodOnly = tp.children[i]
151
+ }
152
+ }
153
+
154
+ // This should always be present based on the grammar, but check defensively
155
+ if (!betweenTodOnly) {
156
+ throw new Error('time_period_ago_between requires between_tod_only child')
157
+ }
158
+
145
159
  const betweenTod = betweenTodOnly.children[0]
146
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
147
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
160
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
161
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
148
162
 
149
163
  // Check if there's a dow_range at betweenTod.children[2]
164
+ // Note: startTod and endTod should always be objects from number_tod parsing
150
165
  if (betweenTod.children.length > 2) {
151
166
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
152
167
  }
153
168
 
154
- return ["TimePeriodBetween", startTod, endTod]
169
+ return ["TimePeriodBetweenAgo", totalSeconds, startTod, endTod]
155
170
  }
156
171
  case 'between_tod_only': {
157
172
  // between_tod_only has children[0] = between_tod node
158
173
  const betweenTod = tp.children[0]
159
- const startTod = RuleParser.__parseValue(betweenTod.children[0])
160
- const endTod = RuleParser.__parseValue(betweenTod.children[1])
174
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
175
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
161
176
 
162
177
  // Check if there's a dow_range at betweenTod.children[2]
163
178
  if (betweenTod.children.length > 2) {
179
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
180
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
164
181
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
165
182
  }
166
183
 
@@ -176,8 +193,13 @@ class RuleParser {
176
193
  // If DOW filters are provided, append them as additional parameters
177
194
  if (betweenNumberTime.children.length > 2 && betweenNumberTime.children[2].type === 'dow_range') {
178
195
  const dow = RuleParser._parseDowRange(betweenNumberTime.children[2])
179
- // Append DOW as additional arguments: ["TimePeriodBetween", start, end, "MONDAY"] or ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
180
- return ["TimePeriodBetween", startValue, endValue, ...dow]
196
+ if (dow.start === dow.end) {
197
+ // Single day: ["TimePeriodBetween", start, end, "MONDAY"]
198
+ return ["TimePeriodBetween", startValue, endValue, dow.start]
199
+ } else {
200
+ // Range: ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
201
+ return ["TimePeriodBetween", startValue, endValue, dow.start, dow.end]
202
+ }
181
203
  }
182
204
 
183
205
  return ["TimePeriodBetween", startValue, endValue]
@@ -285,12 +307,43 @@ class RuleParser {
285
307
  }
286
308
  throw new Error(`Unknown arithmetic operand type ${type}`)
287
309
  }
310
+ static _isConstantValue(expr){
311
+ // Check if an expression is a constant value
312
+ return Array.isArray(expr) && expr.length === 2 && expr[0] === 'Value' && typeof expr[1] === 'number'
313
+ }
314
+
315
+ static _evaluateConstantArithmetic(operator, leftValue, rightValue){
316
+ // Evaluate constant arithmetic operations at parse time
317
+ switch(operator){
318
+ case 'MathAdd':
319
+ return leftValue + rightValue
320
+ case 'MathSub':
321
+ return leftValue - rightValue
322
+ case 'MathMul':
323
+ return leftValue * rightValue
324
+ case 'MathDiv':
325
+ return leftValue / rightValue
326
+ case 'MathMod':
327
+ return leftValue % rightValue
328
+ default:
329
+ return null
330
+ }
331
+ }
332
+
288
333
  static _parseArithmeticResult(result){
289
334
  assert(result.children.length == 3)
290
335
  const partA = RuleParser._parseArithmeticOperand(result.children[0])
291
336
  const operatorFn = ArithmeticOperators[result.children[1].text]
292
337
  const partB = RuleParser.__parseArithmeticResult(result, 2)
293
338
 
339
+ // Compile out constant expressions
340
+ if (RuleParser._isConstantValue(partA) && RuleParser._isConstantValue(partB)) {
341
+ const result = RuleParser._evaluateConstantArithmetic(operatorFn, partA[1], partB[1])
342
+ if (result !== null) {
343
+ return ['Value', result]
344
+ }
345
+ }
346
+
294
347
  return [operatorFn, partA, partB]
295
348
  }
296
349