@halleyassist/rule-parser 1.0.12 → 1.0.14

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,10 +1,11 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-parser",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleParser.production.js",
6
6
  "scripts": {
7
- "test": "mocha"
7
+ "test": "mocha",
8
+ "build": "node ./bin/package.js"
8
9
  },
9
10
  "repository": {
10
11
  "type": "git",
@@ -30,7 +30,7 @@ END_ARRAY ::= WS* #x5D WS* /* ] right square bracket */
30
30
  END_OBJECT ::= WS* #x7D WS* /* } right curly bracket */
31
31
  NAME_SEPARATOR ::= WS* #x3A WS* /* : colon */
32
32
  VALUE_SEPARATOR ::= WS* #x2C WS* /* , comma */
33
- WS ::= [#x20#x09#x0A#x0D]+ /* Space | Tab | \n | \r */
33
+ WS ::= [#x20#x09#x0A#x0D] /* Space | Tab | \n | \r */
34
34
 
35
35
  operator ::= GTE | LTE | GT | LT | EQ | NEQ
36
36
  eq_operator ::= EQ | NEQ
@@ -41,8 +41,8 @@ END_ARGUMENT ::= ")"
41
41
  BEGIN_PARENTHESIS ::= "("
42
42
  END_PARENTHESIS ::= ")"
43
43
 
44
- argument ::= statement WS* ("," WS*)?
45
- arguments ::= argument*
44
+ argument ::= statement WS*
45
+ arguments ::= (argument (WS* "," WS* argument)*)?
46
46
  fname ::= [a-zA-z0-9]+
47
47
  fcall ::= fname WS* BEGIN_ARGUMENT WS* arguments? END_ARGUMENT
48
48
 
package/src/RuleParser.js CHANGED
@@ -1,9 +1,12 @@
1
1
  const {Parser} = require('ebnf/dist/Parser.js'),
2
+ {ParsingError} = require('ebnf'),
2
3
  assert = require('assert')
3
4
 
4
5
  let ParserRules = require('./RuleParser.ebnf.js')
5
6
  let ParserCache;
6
7
 
8
+ const { ErrorAnalyzer } = require('./errors/ErrorAnalyzer');
9
+
7
10
  const ArithmeticOperators = {
8
11
  "+": 'MathAdd',
9
12
  "-": 'MathSub',
@@ -68,11 +71,22 @@ class RuleParser {
68
71
  ParserCache = new Parser(ParserRules, {debug: false})
69
72
  }
70
73
 
71
- ret = ParserCache.getAST(txt, 'statement_main');
74
+ try {
75
+ ret = ParserCache.getAST(txt.trim(), 'statement_main');
76
+ } catch (e) {
77
+ // If ebnf throws ParsingError, convert it to RuleParseError with helpful error code
78
+ if (e instanceof ParsingError) {
79
+ throw ErrorAnalyzer.analyzeParseFailure(txt, e);
80
+ }
81
+ throw e;
82
+ }
72
83
 
73
84
  if(ret){
74
85
  return ret.children[0]
75
86
  }
87
+
88
+ // If parsing failed without throwing (shouldn't happen with new ebnf), throw error
89
+ throw ErrorAnalyzer.analyzeParseFailure(txt);
76
90
  }
77
91
  static _parseArgument(argument){
78
92
  assert(argument.type === 'argument')
@@ -137,20 +151,36 @@ class RuleParser {
137
151
  }
138
152
  return ["TimePeriodConst", tp.text]
139
153
  case 'time_period_ago_between': {
140
- // time_period_ago_between has children[0] = number_time, children[1] = between_tod_only
141
- const betweenTodOnly = tp.children[1]
154
+ // time_period_ago_between has: number_time (WS+ number_time)* WS+ AGO WS+ between_tod_only
155
+ // We need to extract all number_time children and sum them up, then return TimePeriodBetweenAgo
156
+ let totalSeconds = 0
157
+ let betweenTodOnly = null
158
+
159
+ // Find all number_time children and the between_tod_only child
160
+ for (let i = 0; i < tp.children.length; i++) {
161
+ if (tp.children[i].type === 'number_time') {
162
+ totalSeconds += RuleParser.__parseValue(tp.children[i])
163
+ } else if (tp.children[i].type === 'between_tod_only') {
164
+ betweenTodOnly = tp.children[i]
165
+ }
166
+ }
167
+
168
+ // This should always be present based on the grammar, but check defensively
169
+ if (!betweenTodOnly) {
170
+ throw new Error('time_period_ago_between requires between_tod_only child')
171
+ }
172
+
142
173
  const betweenTod = betweenTodOnly.children[0]
143
174
  let startTod = RuleParser.__parseValue(betweenTod.children[0])
144
175
  let endTod = RuleParser.__parseValue(betweenTod.children[1])
145
176
 
146
177
  // Check if there's a dow_range at betweenTod.children[2]
178
+ // Note: startTod and endTod should always be objects from number_tod parsing
147
179
  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}
150
180
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
151
181
  }
152
182
 
153
- return ["TimePeriodBetween", startTod, endTod]
183
+ return ["TimePeriodBetweenAgo", totalSeconds, startTod, endTod]
154
184
  }
155
185
  case 'between_tod_only': {
156
186
  // between_tod_only has children[0] = between_tod node
@@ -482,9 +512,72 @@ class RuleParser {
482
512
  }
483
513
  }
484
514
  static toIL(txt){
485
- const ast = RuleParser.toAst(txt)
486
- if(!ast) throw new Error(`failed to parse ${txt}`)
487
- return RuleParser._buildExpressionGroup(ast)
515
+ try {
516
+ const ast = RuleParser.toAst(txt)
517
+ if(!ast) throw new Error(`failed to parse ${txt}`)
518
+ return RuleParser._buildExpressionGroup(ast)
519
+ } catch (e) {
520
+ // If it's already a RuleParseError, just re-throw it
521
+ if (e.name === 'RuleParseError') {
522
+ throw e;
523
+ }
524
+
525
+ // Check if it's a validation error we can map to a specific code
526
+ if (e.message && e.message.includes('Invalid time of day')) {
527
+ // Extract the invalid time from the error message
528
+ const match = e.message.match(/Invalid time of day[,:]?\s*([0-9:]+)/);
529
+ const badTod = match ? match[1] : 'invalid';
530
+ const { ParsingError } = require('ebnf');
531
+ const { RuleParseError } = require('./errors/RuleParseError');
532
+
533
+ // Calculate position (simplified - at end of input)
534
+ const lines = txt.trim().split('\n');
535
+ const position = {
536
+ line: lines.length,
537
+ column: lines[lines.length - 1].length + 1,
538
+ offset: txt.trim().length
539
+ };
540
+
541
+ throw new RuleParseError(
542
+ "BAD_TOD",
543
+ `Invalid time of day: ${badTod}`,
544
+ "Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
545
+ position,
546
+ badTod,
547
+ ["HH:MM"],
548
+ txt.trim().substring(Math.max(0, txt.trim().length - 50))
549
+ );
550
+ }
551
+
552
+ // Check if it's a day of week error
553
+ if (e.message && e.message.includes('Invalid day of week')) {
554
+ const match = e.message.match(/Invalid day of week[,:]?\s*(\w+)/);
555
+ const badDow = match ? match[1] : 'invalid';
556
+ const { RuleParseError } = require('./errors/RuleParseError');
557
+
558
+ const lines = txt.trim().split('\n');
559
+ const position = {
560
+ line: lines.length,
561
+ column: lines[lines.length - 1].length + 1,
562
+ offset: txt.trim().length
563
+ };
564
+
565
+ throw new RuleParseError(
566
+ "BAD_DOW",
567
+ `Invalid day of week: ${badDow}`,
568
+ "Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
569
+ position,
570
+ badDow,
571
+ ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
572
+ txt.trim().substring(Math.max(0, txt.trim().length - 50))
573
+ );
574
+ }
575
+
576
+ // For other errors, re-throw
577
+ throw e;
578
+ }
488
579
  }
489
580
  }
490
581
  module.exports = RuleParser
582
+ module.exports.ParsingError = require('ebnf').ParsingError
583
+ module.exports.RuleParseError = require('./errors/RuleParseError').RuleParseError
@@ -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_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]/]]}]
1
+ module.exports=[{"name":"statement_main","bnf":[["statement","EOF"]]},{"name":"logical_operator","bnf":[["AND"],["OR"]]},{"name":"%statement[2]","bnf":[["logical_operator","expression"]],"fragment":true},{"name":"statement","bnf":[["expression","%statement[2]*"]]},{"name":"expression","bnf":[["not_expression"],["standard_expression"],["parenthesis_expression"]]},{"name":"parenthesis_expression","bnf":[["BEGIN_PARENTHESIS","WS*","statement","WS*","END_PARENTHESIS"]]},{"name":"%not_expression[2]","bnf":[["result"],["parenthesis_expression"]],"fragment":true},{"name":"not_expression","bnf":[["NOT","%not_expression[2]"]]},{"name":"%standard_expression[2][1]","bnf":[["WS*","eq_approx"]],"fragment":true},{"name":"%standard_expression[2][2]","bnf":[["WS*","basic_rhs"]],"fragment":true},{"name":"%standard_expression[2][3][1]","bnf":[["WS+","IS"]],"fragment":true},{"name":"%standard_expression[2][3]","bnf":[["%standard_expression[2][3][1]?","WS+","between"]],"fragment":true},{"name":"%standard_expression[2]","bnf":[["%standard_expression[2][1]"],["%standard_expression[2][2]"],["%standard_expression[2][3]"]],"fragment":true},{"name":"standard_expression","bnf":[["result","%standard_expression[2]?"]]},{"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_result[5]","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","WS*","arithmetic_operator","WS*","%arithmetic_result[5]"]]},{"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":"WS","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"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":"argument","bnf":[["statement","WS*"]]},{"name":"%arguments[1][2]","bnf":[["WS*","\",\"","WS*","argument"]],"fragment":true},{"name":"%arguments[1]","bnf":[["argument","%arguments[1][2]*"]],"fragment":true},{"name":"arguments","bnf":[["%arguments[1]?"]]},{"name":"%fname[1]","bnf":[[/[a-zA-z0-9]/]]},{"name":"fname","bnf":[["%fname[1]+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_number[1]","bnf":[["number_time"],["number"]],"fragment":true},{"name":"%between_number[2][1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_number[2][2]","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number[2]","bnf":[["%between_number[2][1]"],["%between_number[2][2]"]],"fragment":true},{"name":"%between_number[3]","bnf":[["number_time"],["number"]],"fragment":true},{"name":"between_number","bnf":[["%between_number[1]","%between_number[2]","%between_number[3]"]]},{"name":"%between_number_time[2][1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_number_time[2][2]","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number_time[2]","bnf":[["%between_number_time[2][1]"],["%between_number_time[2][2]"]],"fragment":true},{"name":"%between_number_time[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_number_time","bnf":[["number_time","%between_number_time[2]","number_time","%between_number_time[4]?"]]},{"name":"%between_tod[2][1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod[2]","bnf":[["%between_tod[2][1]"]],"fragment":true},{"name":"%between_tod[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["number_tod","%between_tod[2]","number_tod","%between_tod[4]?"]]},{"name":"%between[3]","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between[3]"]]},{"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_range[4]","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow","%dow_range[4]?"]]},{"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":"%AND[1]","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND[2]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND[1]"],["%AND[2]"]]},{"name":"%OR[1]","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR[2]","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR[1]"],["%OR[2]"]]},{"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":"%NOT[1]","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT[2]","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT[1]"],["%NOT[2]"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%array[2][2]","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array[2]","bnf":[["value","%array[2][2]*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array[2]?","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":"%number[2][1]","bnf":[[/[0-9]/]]},{"name":"%number[2]","bnf":[["%number[2][1]+"]],"fragment":true},{"name":"%number[3][2]","bnf":[[/[0-9]/]]},{"name":"%number[3]","bnf":[["\".\"","%number[3][2]+"]],"fragment":true},{"name":"%number[4][2]","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%number[4][3][2]","bnf":[[/[0-9]/]]},{"name":"%number[4][3]","bnf":[["\"0\""],[/[1-9]/,"%number[4][3][2]*"]],"fragment":true},{"name":"%number[4]","bnf":[["\"e\"","%number[4][2]?","%number[4][3]"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number[2]","%number[3]?","%number[4]?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%number_tod[1][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[1]","bnf":[["%number_tod[1][1]+"]],"fragment":true},{"name":"%number_tod[3][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[3]","bnf":[["%number_tod[3][1]+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod[1]","\":\"","%number_tod[3]"]]},{"name":"%time_period_ago[2]","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time","%time_period_ago[2]*","WS+","AGO"]]},{"name":"%time_period_ago_between[2]","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time","%time_period_ago_between[2]*","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":"%string[2][1]","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%string[2][2]","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string[2]","bnf":[["%string[2][1]"],[/\x5C/,"%string[2][2]"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string[2]*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
@@ -1,9 +1,12 @@
1
1
  const {Parser} = require('ebnf/dist/Parser.js'),
2
+ {ParsingError} = require('ebnf'),
2
3
  assert = require('assert')
3
4
 
4
5
  let ParserRules = require('./RuleParser.production.ebnf.js')
5
6
  let ParserCache;
6
7
 
8
+ const { ErrorAnalyzer } = require('./errors/ErrorAnalyzer');
9
+
7
10
  const ArithmeticOperators = {
8
11
  "+": 'MathAdd',
9
12
  "-": 'MathSub',
@@ -68,11 +71,22 @@ class RuleParser {
68
71
  ParserCache = new Parser(ParserRules, {debug: false})
69
72
  }
70
73
 
71
- ret = ParserCache.getAST(txt, 'statement_main');
74
+ try {
75
+ ret = ParserCache.getAST(txt.trim(), 'statement_main');
76
+ } catch (e) {
77
+ // If ebnf throws ParsingError, convert it to RuleParseError with helpful error code
78
+ if (e instanceof ParsingError) {
79
+ throw ErrorAnalyzer.analyzeParseFailure(txt, e);
80
+ }
81
+ throw e;
82
+ }
72
83
 
73
84
  if(ret){
74
85
  return ret.children[0]
75
86
  }
87
+
88
+ // If parsing failed without throwing (shouldn't happen with new ebnf), throw error
89
+ throw ErrorAnalyzer.analyzeParseFailure(txt);
76
90
  }
77
91
  static _parseArgument(argument){
78
92
  assert(argument.type === 'argument')
@@ -137,20 +151,36 @@ class RuleParser {
137
151
  }
138
152
  return ["TimePeriodConst", tp.text]
139
153
  case 'time_period_ago_between': {
140
- // time_period_ago_between has children[0] = number_time, children[1] = between_tod_only
141
- const betweenTodOnly = tp.children[1]
154
+ // time_period_ago_between has: number_time (WS+ number_time)* WS+ AGO WS+ between_tod_only
155
+ // We need to extract all number_time children and sum them up, then return TimePeriodBetweenAgo
156
+ let totalSeconds = 0
157
+ let betweenTodOnly = null
158
+
159
+ // Find all number_time children and the between_tod_only child
160
+ for (let i = 0; i < tp.children.length; i++) {
161
+ if (tp.children[i].type === 'number_time') {
162
+ totalSeconds += RuleParser.__parseValue(tp.children[i])
163
+ } else if (tp.children[i].type === 'between_tod_only') {
164
+ betweenTodOnly = tp.children[i]
165
+ }
166
+ }
167
+
168
+ // This should always be present based on the grammar, but check defensively
169
+ if (!betweenTodOnly) {
170
+ throw new Error('time_period_ago_between requires between_tod_only child')
171
+ }
172
+
142
173
  const betweenTod = betweenTodOnly.children[0]
143
174
  let startTod = RuleParser.__parseValue(betweenTod.children[0])
144
175
  let endTod = RuleParser.__parseValue(betweenTod.children[1])
145
176
 
146
177
  // Check if there's a dow_range at betweenTod.children[2]
178
+ // Note: startTod and endTod should always be objects from number_tod parsing
147
179
  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}
150
180
  RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
151
181
  }
152
182
 
153
- return ["TimePeriodBetween", startTod, endTod]
183
+ return ["TimePeriodBetweenAgo", totalSeconds, startTod, endTod]
154
184
  }
155
185
  case 'between_tod_only': {
156
186
  // between_tod_only has children[0] = between_tod node
@@ -482,9 +512,72 @@ class RuleParser {
482
512
  }
483
513
  }
484
514
  static toIL(txt){
485
- const ast = RuleParser.toAst(txt)
486
- if(!ast) throw new Error(`failed to parse ${txt}`)
487
- return RuleParser._buildExpressionGroup(ast)
515
+ try {
516
+ const ast = RuleParser.toAst(txt)
517
+ if(!ast) throw new Error(`failed to parse ${txt}`)
518
+ return RuleParser._buildExpressionGroup(ast)
519
+ } catch (e) {
520
+ // If it's already a RuleParseError, just re-throw it
521
+ if (e.name === 'RuleParseError') {
522
+ throw e;
523
+ }
524
+
525
+ // Check if it's a validation error we can map to a specific code
526
+ if (e.message && e.message.includes('Invalid time of day')) {
527
+ // Extract the invalid time from the error message
528
+ const match = e.message.match(/Invalid time of day[,:]?\s*([0-9:]+)/);
529
+ const badTod = match ? match[1] : 'invalid';
530
+ const { ParsingError } = require('ebnf');
531
+ const { RuleParseError } = require('./errors/RuleParseError');
532
+
533
+ // Calculate position (simplified - at end of input)
534
+ const lines = txt.trim().split('\n');
535
+ const position = {
536
+ line: lines.length,
537
+ column: lines[lines.length - 1].length + 1,
538
+ offset: txt.trim().length
539
+ };
540
+
541
+ throw new RuleParseError(
542
+ "BAD_TOD",
543
+ `Invalid time of day: ${badTod}`,
544
+ "Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
545
+ position,
546
+ badTod,
547
+ ["HH:MM"],
548
+ txt.trim().substring(Math.max(0, txt.trim().length - 50))
549
+ );
550
+ }
551
+
552
+ // Check if it's a day of week error
553
+ if (e.message && e.message.includes('Invalid day of week')) {
554
+ const match = e.message.match(/Invalid day of week[,:]?\s*(\w+)/);
555
+ const badDow = match ? match[1] : 'invalid';
556
+ const { RuleParseError } = require('./errors/RuleParseError');
557
+
558
+ const lines = txt.trim().split('\n');
559
+ const position = {
560
+ line: lines.length,
561
+ column: lines[lines.length - 1].length + 1,
562
+ offset: txt.trim().length
563
+ };
564
+
565
+ throw new RuleParseError(
566
+ "BAD_DOW",
567
+ `Invalid day of week: ${badDow}`,
568
+ "Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
569
+ position,
570
+ badDow,
571
+ ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
572
+ txt.trim().substring(Math.max(0, txt.trim().length - 50))
573
+ );
574
+ }
575
+
576
+ // For other errors, re-throw
577
+ throw e;
578
+ }
488
579
  }
489
580
  }
490
581
  module.exports = RuleParser
582
+ module.exports.ParsingError = require('ebnf').ParsingError
583
+ module.exports.RuleParseError = require('./errors/RuleParseError').RuleParseError