@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 +4 -0
- package/package.json +1 -1
- package/src/RuleParser.js +72 -19
- package/src/RuleParser.production.js +72 -19
package/README.md
CHANGED
package/package.json
CHANGED
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
|
-
|
|
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
|
|
101
|
-
|
|
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
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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 ["
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
101
|
-
|
|
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
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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 ["
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
|