@malloydata/malloy-filter 0.0.237-dev250221201621

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.
Files changed (87) hide show
  1. package/README.md +81 -0
  2. package/SAMPLES.md +381 -0
  3. package/SERIALIZE_SAMPLES.md +300 -0
  4. package/dist/a_simple_parser.d.ts +1 -0
  5. package/dist/a_simple_parser.js +20 -0
  6. package/dist/a_simple_parser.js.map +1 -0
  7. package/dist/a_simple_serializer.d.ts +1 -0
  8. package/dist/a_simple_serializer.js +31 -0
  9. package/dist/a_simple_serializer.js.map +1 -0
  10. package/dist/base_parser.d.ts +13 -0
  11. package/dist/base_parser.js +33 -0
  12. package/dist/base_parser.js.map +1 -0
  13. package/dist/base_serializer.d.ts +6 -0
  14. package/dist/base_serializer.js +11 -0
  15. package/dist/base_serializer.js.map +1 -0
  16. package/dist/boolean_parser.d.ts +7 -0
  17. package/dist/boolean_parser.js +59 -0
  18. package/dist/boolean_parser.js.map +1 -0
  19. package/dist/boolean_serializer.d.ts +8 -0
  20. package/dist/boolean_serializer.js +31 -0
  21. package/dist/boolean_serializer.js.map +1 -0
  22. package/dist/clause_types.d.ts +70 -0
  23. package/dist/clause_types.js +3 -0
  24. package/dist/clause_types.js.map +1 -0
  25. package/dist/date_parser.d.ts +22 -0
  26. package/dist/date_parser.js +315 -0
  27. package/dist/date_parser.js.map +1 -0
  28. package/dist/date_serializer.d.ts +10 -0
  29. package/dist/date_serializer.js +100 -0
  30. package/dist/date_serializer.js.map +1 -0
  31. package/dist/filter_parser.d.ts +12 -0
  32. package/dist/filter_parser.js +66 -0
  33. package/dist/filter_parser.js.map +1 -0
  34. package/dist/filter_serializer.d.ts +13 -0
  35. package/dist/filter_serializer.js +43 -0
  36. package/dist/filter_serializer.js.map +1 -0
  37. package/dist/filter_types.d.ts +10 -0
  38. package/dist/filter_types.js +3 -0
  39. package/dist/filter_types.js.map +1 -0
  40. package/dist/generate_samples.d.ts +1 -0
  41. package/dist/generate_samples.js +344 -0
  42. package/dist/generate_samples.js.map +1 -0
  43. package/dist/number_parser.d.ts +20 -0
  44. package/dist/number_parser.js +275 -0
  45. package/dist/number_parser.js.map +1 -0
  46. package/dist/number_serializer.d.ts +11 -0
  47. package/dist/number_serializer.js +76 -0
  48. package/dist/number_serializer.js.map +1 -0
  49. package/dist/string_parser.d.ts +18 -0
  50. package/dist/string_parser.js +198 -0
  51. package/dist/string_parser.js.map +1 -0
  52. package/dist/string_serializer.d.ts +11 -0
  53. package/dist/string_serializer.js +77 -0
  54. package/dist/string_serializer.js.map +1 -0
  55. package/dist/token_types.d.ts +7 -0
  56. package/dist/token_types.js +3 -0
  57. package/dist/token_types.js.map +1 -0
  58. package/dist/tokenizer.d.ts +52 -0
  59. package/dist/tokenizer.js +263 -0
  60. package/dist/tokenizer.js.map +1 -0
  61. package/dist/tokenizer.spec.d.ts +1 -0
  62. package/dist/tokenizer.spec.js +255 -0
  63. package/dist/tokenizer.spec.js.map +1 -0
  64. package/jest.config.js +3 -0
  65. package/package.json +21 -0
  66. package/src/DEVELOPING.md +26 -0
  67. package/src/a_simple_parser.ts +22 -0
  68. package/src/a_simple_serializer.ts +40 -0
  69. package/src/base_parser.ts +45 -0
  70. package/src/base_serializer.ts +9 -0
  71. package/src/boolean_parser.ts +60 -0
  72. package/src/boolean_serializer.ts +32 -0
  73. package/src/clause_types.ts +160 -0
  74. package/src/date_parser.ts +413 -0
  75. package/src/date_serializer.ts +114 -0
  76. package/src/filter_parser.ts +68 -0
  77. package/src/filter_serializer.ts +49 -0
  78. package/src/filter_types.ts +12 -0
  79. package/src/generate_samples.ts +387 -0
  80. package/src/number_parser.ts +308 -0
  81. package/src/number_serializer.ts +96 -0
  82. package/src/string_parser.ts +193 -0
  83. package/src/string_serializer.ts +87 -0
  84. package/src/token_types.ts +7 -0
  85. package/src/tokenizer.spec.ts +273 -0
  86. package/src/tokenizer.ts +320 -0
  87. package/tsconfig.json +14 -0
@@ -0,0 +1,387 @@
1
+ import {Clause} from './clause_types';
2
+ import {SpecialToken, Tokenizer, TokenizerParams} from './tokenizer';
3
+ import {BooleanParser} from './boolean_parser';
4
+ import {StringParser} from './string_parser';
5
+ import {NumberParser} from './number_parser';
6
+ import {DateParser} from './date_parser';
7
+ import {BaseParser} from './base_parser';
8
+ import {BooleanSerializer} from './boolean_serializer';
9
+ import {StringSerializer} from './string_serializer';
10
+ import {NumberSerializer} from './number_serializer';
11
+ import {DateSerializer} from './date_serializer';
12
+ import {BaseSerializer} from './base_serializer';
13
+
14
+ const numberExamples = [
15
+ '5',
16
+ '!=5',
17
+ '1, 3, null , 7',
18
+ '<1, >=100 ',
19
+ '>=1',
20
+ ' <= 10 ',
21
+ 'NULL',
22
+ ' -NULL',
23
+ '(1, 7)',
24
+ '[-5, 90]',
25
+ ' != ( 12, 20 ] ',
26
+ '[.12e-20, 20.0e3)',
27
+ '[0,9],[20,29]',
28
+ '[0,10], 20, NULL, ( 72, 82 ] ',
29
+ ', notanumber,, "null", apple pear orange, nulle, nnull, >=,',
30
+ '[cat, 100], <cat',
31
+ '-5.5 to 10',
32
+ ];
33
+
34
+ const stringExamples = [
35
+ 'CAT, DOG,mouse ',
36
+ '-CAT,-DOG , -mouse',
37
+ ' CAT,-"DOG",m o u s e',
38
+ '-CAT,-DOG,mouse, bird, zebra, -horse, -goat',
39
+ 'Missing ,NULL',
40
+ 'CAT%, D%OG, %ous%, %ira_f%, %_oat, ',
41
+ '-CAT%,-D%OG,-%mouse,-%zebra%',
42
+ 'CAT%,-CATALOG',
43
+ '%,_,%%,%a%',
44
+ '%\\_X',
45
+ '_\\_X',
46
+ '_CAT,D_G,mouse_',
47
+ '\\_CAT,D\\%G,\\mouse',
48
+ 'CAT,-NULL',
49
+ 'CAT,-"NULL"',
50
+ 'CAT,NULL',
51
+ 'EMPTY',
52
+ '-EMPTY',
53
+ 'CAT,-EMPTY',
54
+ '"CAT,DOG\',mo`use,zeb\'\'\'ra,g"""t,g\\"ir\\`af\\\'e',
55
+ 'CAT\\,DOG',
56
+ 'CAT,DOG,-, - ',
57
+ '--CAT,DOG,\\',
58
+ 'CAT\\ DOG',
59
+ '_\\_CAT',
60
+ '\\NULL',
61
+ '\\-NULL',
62
+ '-N\\ULL',
63
+ 'CA--,D-G', // _ = 'CA--' OR _ = 'D-G'
64
+ ' hello world, foo="bar baz" , qux=quux',
65
+ 'one ,Null , Empty,E M P T Y Y,EEmpty, emptIEs',
66
+ '',
67
+ ];
68
+
69
+ const booleanExamples = [
70
+ 'true',
71
+ 'FALSE',
72
+ 'null',
73
+ '-NULL',
74
+ ' True , faLSE,NULl,-null',
75
+ "-'null'",
76
+ '10',
77
+ 'nnull',
78
+ ' truee ',
79
+ ];
80
+
81
+ const dateExamples = [
82
+ 'this month',
83
+ '3 days',
84
+ '3 days ago',
85
+ '3 months ago for 2 days',
86
+ 'after 2025 seconds',
87
+ '2025 weeks ago',
88
+ 'before 3 days ago',
89
+ 'before 2025-08-30 08:30:20',
90
+ 'after 2025-10-05',
91
+ '2025-08-30 12:00:00 to 2025-09-18 14:00:00',
92
+ 'this year',
93
+ 'next tuesday',
94
+ '7 years from now',
95
+ '2025-01-01 12:00:00 for 3 days',
96
+ '2020-08-12',
97
+ '2020-08',
98
+ 'today',
99
+ 'yesterday',
100
+ 'tomorrow',
101
+ 'TODay,Yesterday, TOMORROW , ,TODay ,,',
102
+ '2010 to 2011, 2015 to 2016 , 2018, 2020 ',
103
+ 'next week',
104
+ 'now',
105
+ 'now to next month',
106
+ ' yyesterday ', // Typo
107
+ 'before', // Bad syntax
108
+ 'for', // Bad syntax
109
+ '7', // Bad syntax
110
+ 'from now', // Bad syntax
111
+ '',
112
+ ];
113
+
114
+ /* eslint-disable @typescript-eslint/no-unused-vars */
115
+ /* eslint-disable no-console */
116
+
117
+ function testTokenizerSingle(str: string, params: TokenizerParams): void {
118
+ const tokenizer = new Tokenizer(str, params);
119
+ console.log(str, ' -> ', tokenizer.parse(), '\n');
120
+ }
121
+
122
+ function testTokenizerString() {
123
+ // Test string parser tokenizer. Do not split on whitespace
124
+ const specialSubstrings: SpecialToken[] = [{type: ',', value: ','}];
125
+ const specialWords: SpecialToken[] = [];
126
+ const params: TokenizerParams = {
127
+ trimWordWhitespace: true,
128
+ combineAdjacentWords: true,
129
+ specialSubstrings,
130
+ specialWords,
131
+ };
132
+ const examples = [
133
+ 'CAT, DOG,mouse ',
134
+ '-CAT,-DOG , -mouse',
135
+ ' CAT,-"DOG",m o u s e',
136
+ '-CAT,-DOG,mouse, bird, zebra, -horse, -goat',
137
+ 'Missing ,NULL',
138
+ 'CAT%,D%OG',
139
+ 'CAT%,-CATALOG',
140
+ '_CAT,D_G',
141
+ 'CAT,-NULL',
142
+ 'CAT,-"NULL"',
143
+ 'EMPTY',
144
+ '-EMPTY',
145
+ 'CAT,-EMPTY',
146
+ '"CAT,DOG"',
147
+ 'CAT\\,DOG',
148
+ 'CAT,DOG,-, - ',
149
+ '--CAT,DOG,\\',
150
+ 'CA--,D-G', // _ = 'CA--' OR _ = 'D-G'
151
+ ' hello world, foo="bar baz" , qux=quux',
152
+ 'one ,Null , Empty,E M P T Y Y,EEmpty, emptIEs',
153
+ '',
154
+ ];
155
+ for (const example of examples) {
156
+ testTokenizerSingle(example, params);
157
+ }
158
+ }
159
+
160
+ function testTokenizerNumber() {
161
+ // Test number parser tokenizer, split on whitespace
162
+ const specialSubstrings: SpecialToken[] = [
163
+ {type: ',', value: ','},
164
+ {type: '[', value: '['},
165
+ {type: ']', value: ']'},
166
+ {type: '(', value: '('},
167
+ {type: ')', value: ')'},
168
+ {type: '<=', value: '<='},
169
+ {type: '>=', value: '>='},
170
+ {type: '!=', value: '!='},
171
+ {type: '=', value: '='},
172
+ {type: '>', value: '>'},
173
+ {type: '<', value: '<'},
174
+ ];
175
+ const specialWords: SpecialToken[] = [
176
+ {type: 'TO', value: 'to', ignoreCase: true},
177
+ {type: '-NULL', value: '-null', ignoreCase: true},
178
+ {type: 'NULL', value: 'null', ignoreCase: true},
179
+ ];
180
+ const params: TokenizerParams = {
181
+ trimWordWhitespace: true,
182
+ combineAdjacentWords: false,
183
+ splitOnWhitespace: true,
184
+ specialSubstrings,
185
+ specialWords,
186
+ };
187
+ const examples = [
188
+ '5',
189
+ '!=5',
190
+ '1, 3, null , 7',
191
+ '<1, >=100 ',
192
+ '-5.5 to 10',
193
+ '>=1',
194
+ ' <= 10 ',
195
+ 'NULL',
196
+ ' -NULL',
197
+ '(1, 7)',
198
+ '[-5, 90]',
199
+ ' ( 12, 20 ] ',
200
+ '[.12e-20, 20.0e3)',
201
+ '[0,9],[20,29]',
202
+ '[0,10], 20, NULL, ( 72, 82 ] ',
203
+ ', notanumber,, -"-null", apple pear orange, nulle, nnull',
204
+ ];
205
+ for (const example of examples) {
206
+ testTokenizerSingle(example, params);
207
+ }
208
+ }
209
+
210
+ function testParserSingle(
211
+ str: string,
212
+ parser: BaseParser,
213
+ outputFormatter?: (clauses: Clause[]) => string
214
+ ): void {
215
+ console.log('Input: ', str);
216
+ try {
217
+ const response = parser.parse();
218
+ // console.log('Tokens: ', parser.getTokens());
219
+ if (response.clauses && response.clauses.length > 0) {
220
+ if (outputFormatter) {
221
+ console.log('Output: ', outputFormatter(response.clauses));
222
+ } else {
223
+ console.log('Output: ', ...response.clauses);
224
+ }
225
+ }
226
+ if (response.errors && response.errors.length > 0) {
227
+ console.log('Errors: ', ...response.errors);
228
+ }
229
+ } catch (ex: Error | unknown) {
230
+ if (ex instanceof Error) console.error('Thrown Error: ', ex.message);
231
+ else {
232
+ console.error('Thrown Unknown error: ', ex);
233
+ }
234
+ }
235
+ console.log('');
236
+ }
237
+
238
+ function testSerializerRoundtrip(
239
+ str: string,
240
+ parser: BaseParser,
241
+ serializerFunc: (clauses: Clause[]) => BaseSerializer
242
+ ): void {
243
+ console.log('Input: ' + str);
244
+ try {
245
+ const response = parser.parse();
246
+ // console.log('Clause: ', ...response.clauses, '\n');
247
+ if (response.clauses && response.clauses.length > 0) {
248
+ const result = serializerFunc(response.clauses || []).serialize();
249
+ console.log('Output: ' + result);
250
+ }
251
+ if (response.errors && response.errors.length > 0) {
252
+ console.log('Errors: ', ...response.errors);
253
+ }
254
+ } catch (ex: Error | unknown) {
255
+ if (ex instanceof Error) console.error('Thrown Error: ', ex.message);
256
+ else {
257
+ console.error('Thrown Unknown error: ', ex);
258
+ }
259
+ }
260
+ console.log('');
261
+ }
262
+
263
+ function testNumberParser() {
264
+ for (const example of numberExamples) {
265
+ const parser = new NumberParser(example);
266
+ testParserSingle(example, parser);
267
+ }
268
+ }
269
+
270
+ function testStringParser() {
271
+ for (const example of stringExamples) {
272
+ const parser = new StringParser(example);
273
+ testParserSingle(example, parser);
274
+ }
275
+ }
276
+
277
+ function testBooleanParser() {
278
+ for (const example of booleanExamples) {
279
+ const parser = new BooleanParser(example);
280
+ testParserSingle(example, parser);
281
+ }
282
+ }
283
+
284
+ function jsonFormatter(clauses: Clause[]): string {
285
+ let str = '';
286
+ for (const clause of clauses) {
287
+ str += JSON.stringify(clause, null, ' ');
288
+ }
289
+ return str;
290
+ }
291
+
292
+ function testDateParser() {
293
+ for (const example of dateExamples) {
294
+ const parser = new DateParser(example);
295
+ testParserSingle(example, parser);
296
+ //testParserSingle(example, parser, jsonFormatter);
297
+ }
298
+ }
299
+
300
+ function testNumberSerializer(): void {
301
+ const examples = [
302
+ [{operator: '>', value: 10}],
303
+ [{startOperator: '>=', startValue: 20, endOperator: '<=', endValue: 30}],
304
+ ];
305
+ for (const example of numberExamples) {
306
+ testSerializerRoundtrip(
307
+ example,
308
+ new NumberParser(example),
309
+ clauses => new NumberSerializer(clauses)
310
+ );
311
+ }
312
+ }
313
+
314
+ function testStringSerializer(): void {
315
+ for (const example of stringExamples) {
316
+ testSerializerRoundtrip(
317
+ example,
318
+ new StringParser(example),
319
+ clauses => new StringSerializer(clauses)
320
+ );
321
+ }
322
+ }
323
+
324
+ function testBooleanSerializer(): void {
325
+ const examples = [[{operator: 'TRUE'}]];
326
+ for (const example of booleanExamples) {
327
+ testSerializerRoundtrip(
328
+ example,
329
+ new BooleanParser(example),
330
+ clauses => new BooleanSerializer(clauses)
331
+ );
332
+ }
333
+ }
334
+
335
+ function testDateSerializer(): void {
336
+ const examples = [
337
+ [{prefix: 'BEFORE', values: [{type: 'day', value: 'yesterday'}]}],
338
+ [
339
+ {
340
+ start: [{type: 'day', value: 'today'}],
341
+ operator: 'TO',
342
+ end: [{type: 'day', value: 'tomorrow'}],
343
+ },
344
+ ],
345
+ [{type: 'day', value: 'today'}],
346
+ ];
347
+ for (const example of dateExamples) {
348
+ testSerializerRoundtrip(
349
+ example,
350
+ new DateParser(example),
351
+ clauses => new DateSerializer(clauses)
352
+ );
353
+ }
354
+ }
355
+
356
+ function printHeader(title: string): void {
357
+ console.log(
358
+ '\n-------------------------------------------------------------------------'
359
+ );
360
+ console.log('## ', title, '\n');
361
+ }
362
+
363
+ // Comment or uncomment the following function calls to disable/enable examples.
364
+ function generateSamples() {
365
+ //printHeader('Tokenizer');
366
+ //testTokenizerString();
367
+ //testTokenizerNumber();
368
+ //testTokenizerMatchTypes();
369
+ //printHeader('Numbers');
370
+ //testNumberParser();
371
+ //printHeader('Strings');
372
+ //testStringParser();
373
+ //printHeader('Booleans');
374
+ //testBooleanParser();
375
+ //printHeader('Dates and Times');
376
+ //testDateParser();
377
+ printHeader('Number Serializer');
378
+ testNumberSerializer();
379
+ printHeader('String Serializer');
380
+ testStringSerializer();
381
+ printHeader('Boolean Serializer');
382
+ testBooleanSerializer();
383
+ printHeader('Date and Time Serializer');
384
+ testDateSerializer();
385
+ }
386
+
387
+ generateSamples();
@@ -0,0 +1,308 @@
1
+ import {SpecialToken, Tokenizer, TokenizerParams} from './tokenizer';
2
+ import {
3
+ NumberCondition,
4
+ NumberRange,
5
+ NumberOperator,
6
+ NumberRangeOperator,
7
+ NumberClause,
8
+ Clause,
9
+ } from './clause_types';
10
+ import {BaseParser} from './base_parser';
11
+ import {Token} from './token_types';
12
+ import {FilterParserResponse, FilterError} from './filter_types';
13
+
14
+ export class NumberParser extends BaseParser {
15
+ constructor(input: string) {
16
+ super(input);
17
+ }
18
+
19
+ private tokenize(): void {
20
+ const specialSubstrings: SpecialToken[] = [
21
+ {type: ',', value: ','},
22
+ {type: '[', value: '['},
23
+ {type: ']', value: ']'},
24
+ {type: '(', value: '('},
25
+ {type: ')', value: ')'},
26
+ {type: '<=', value: '<='},
27
+ {type: '>=', value: '>='},
28
+ {type: '!=', value: '!='},
29
+ {type: '=', value: '='},
30
+ {type: '>', value: '>'},
31
+ {type: '<', value: '<'},
32
+ ];
33
+ const specialWords = [
34
+ {type: 'NOTNULL', value: '-null', ignoreCase: true},
35
+ {type: 'NULL', value: 'null', ignoreCase: true},
36
+ ];
37
+ const params: TokenizerParams = {
38
+ trimWordWhitespace: true,
39
+ combineAdjacentWords: false,
40
+ splitOnWhitespace: true,
41
+ specialSubstrings,
42
+ specialWords,
43
+ };
44
+
45
+ const tokenizer = new Tokenizer(this.inputString, params);
46
+ this.tokens = tokenizer.parse();
47
+ }
48
+
49
+ public parse(): FilterParserResponse {
50
+ this.index = 0;
51
+ this.tokenize();
52
+ let clauses: NumberClause[] = [];
53
+ const errors: FilterError[] = [];
54
+ while (this.index < this.tokens.length) {
55
+ const token = this.getNext();
56
+ if (token.type === ',') {
57
+ this.index++;
58
+ } else if (this.isRangeStart(this.index)) {
59
+ clauses = this.checkRange(token, false, clauses, errors);
60
+ } else if (token.type === '!=' && this.isRangeStart(this.index + 1)) {
61
+ this.index++;
62
+ clauses = this.checkRange(token, true, clauses, errors);
63
+ } else if (this.checkNull(clauses)) {
64
+ this.index++;
65
+ } else if (
66
+ this.checkNumericExpression('<=', clauses) ||
67
+ this.checkNumericExpression('>=', clauses) ||
68
+ this.checkNumericExpression('<', clauses) ||
69
+ this.checkNumericExpression('>', clauses) ||
70
+ this.checkNumericExpression('!=', clauses) ||
71
+ this.checkNumericExpression('=', clauses)
72
+ ) {
73
+ this.index += 2;
74
+ } else if (this.checkSimpleNumber(clauses)) {
75
+ this.index++;
76
+ } else {
77
+ errors.push({
78
+ message: 'Invalid expression',
79
+ startIndex: token.startIndex,
80
+ endIndex: token.endIndex,
81
+ });
82
+ this.index++;
83
+ }
84
+ }
85
+ return {clauses: NumberParser.groupClauses(clauses), errors};
86
+ }
87
+
88
+ private static groupClauses(clauses: NumberClause[]): NumberClause[] {
89
+ if (clauses.length < 2) {
90
+ return clauses;
91
+ }
92
+ let previous: NumberClause | undefined = undefined;
93
+ const outputs: NumberClause[] = [];
94
+ for (let i = 0; i < clauses.length; i++) {
95
+ const clause: NumberClause = clauses[i];
96
+ if (clause.operator === 'range') {
97
+ previous = undefined;
98
+ outputs.push(clause);
99
+ } else if (
100
+ previous !== undefined &&
101
+ previous.operator === clause.operator
102
+ ) {
103
+ previous.values.push(...clause.values);
104
+ } else {
105
+ previous = clause;
106
+ outputs.push(clause);
107
+ }
108
+ }
109
+ return outputs;
110
+ }
111
+
112
+ private matchTokens(candidates: string[]): boolean {
113
+ return BaseParser.matchTokenTypes(candidates, this.index, this.tokens);
114
+ }
115
+
116
+ private checkRange(
117
+ token: Token,
118
+ negated: boolean,
119
+ clauses: NumberClause[],
120
+ errors: FilterError[]
121
+ ): NumberClause[] {
122
+ if (this.matchTokens(['[', 'word', ',', 'word', ']'])) {
123
+ return negated
124
+ ? this.consumeRange('<', '>', clauses, errors)
125
+ : this.consumeRange('>=', '<=', clauses, errors);
126
+ } else if (this.matchTokens(['[', 'word', ',', 'word', ')'])) {
127
+ return negated
128
+ ? this.consumeRange('<', '>=', clauses, errors)
129
+ : this.consumeRange('>=', '<', clauses, errors);
130
+ } else if (this.matchTokens(['(', 'word', ',', 'word', ']'])) {
131
+ return negated
132
+ ? this.consumeRange('<=', '>', clauses, errors)
133
+ : this.consumeRange('>', '<=', clauses, errors);
134
+ } else if (this.matchTokens(['(', 'word', ',', 'word', ')'])) {
135
+ return negated
136
+ ? this.consumeRange('<=', '>=', clauses, errors)
137
+ : this.consumeRange('>', '<', clauses, errors);
138
+ } else {
139
+ errors.push({
140
+ message: 'Invalid range expression',
141
+ startIndex: token.startIndex,
142
+ endIndex: token.endIndex,
143
+ });
144
+ this.index++;
145
+ }
146
+ return clauses;
147
+ }
148
+
149
+ private isRangeStart(position: number): boolean {
150
+ if (position < 0 || position >= this.tokens.length) {
151
+ return false;
152
+ }
153
+ const tokenType = this.tokens[position].type;
154
+ return tokenType === '[' || tokenType === '(';
155
+ }
156
+
157
+ private static parseNumber(value: string): number {
158
+ if (value.toUpperCase() === 'inf') {
159
+ return Infinity;
160
+ } else if (value.toUpperCase() === '-inf') {
161
+ return -Infinity;
162
+ } else {
163
+ return Number(value);
164
+ }
165
+ }
166
+
167
+ private static isValidNumber(value: number): boolean {
168
+ return Number.isNaN(value) === false;
169
+ }
170
+
171
+ private consumeRange(
172
+ startOperator: NumberRangeOperator,
173
+ endOperator: NumberRangeOperator,
174
+ clauses: NumberClause[],
175
+ errors: FilterError[]
176
+ ): NumberClause[] {
177
+ const startToken = this.getAt(this.index + 1);
178
+ const endToken = this.getAt(this.index + 3);
179
+ const startValue: number = NumberParser.parseNumber(startToken.value);
180
+ const endValue: number = NumberParser.parseNumber(endToken.value);
181
+ if (!NumberParser.isValidNumber(startValue)) {
182
+ errors.push({
183
+ message: 'Invalid number',
184
+ startIndex: startToken.startIndex,
185
+ endIndex: startToken.endIndex,
186
+ });
187
+ } else if (!NumberParser.isValidNumber(endValue)) {
188
+ errors.push({
189
+ message: 'Invalid number',
190
+ startIndex: endToken.startIndex,
191
+ endIndex: endToken.endIndex,
192
+ });
193
+ } else {
194
+ const clause: NumberRange = {
195
+ operator: 'range',
196
+ startOperator: startOperator,
197
+ startValue: startValue,
198
+ endOperator: endOperator,
199
+ endValue: endValue,
200
+ };
201
+ clauses.push(clause);
202
+ }
203
+ this.index += 5;
204
+ return clauses;
205
+ }
206
+
207
+ private static getNegatedType(tokenType: NumberOperator): NumberOperator {
208
+ switch (tokenType) {
209
+ case '<=':
210
+ return '>';
211
+ case '>=':
212
+ return '<';
213
+ case '<':
214
+ return '>';
215
+ case '>':
216
+ return '<';
217
+ case '=':
218
+ return '!=';
219
+ case '!=':
220
+ return '=';
221
+ }
222
+ }
223
+
224
+ private static getNumberOperator(
225
+ tokenType: string
226
+ ): NumberOperator | undefined {
227
+ switch (tokenType) {
228
+ case '<=':
229
+ return '<=';
230
+ case '>=':
231
+ return '>=';
232
+ case '<':
233
+ return '<';
234
+ case '>':
235
+ return '>';
236
+ case '=':
237
+ return '=';
238
+ case '!=':
239
+ return '!=';
240
+ }
241
+ return undefined;
242
+ }
243
+
244
+ private static getNumberRnageOperator(
245
+ tokenType: string
246
+ ): NumberRangeOperator | undefined {
247
+ switch (tokenType) {
248
+ case '<=':
249
+ return '<=';
250
+ case '>=':
251
+ return '>=';
252
+ case '<':
253
+ return '<';
254
+ case '>':
255
+ return '>';
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ private checkNumericExpression(
261
+ tokenType: NumberOperator,
262
+ clauses: NumberClause[]
263
+ ): boolean {
264
+ if (
265
+ this.getNext().type === tokenType &&
266
+ this.index < this.tokens.length - 1
267
+ ) {
268
+ const numericValue: number = NumberParser.parseNumber(
269
+ this.getAt(this.index + 1).value
270
+ );
271
+ if (!NumberParser.isValidNumber(numericValue)) {
272
+ return false;
273
+ }
274
+ const clause: NumberCondition = {
275
+ operator: tokenType,
276
+ values: [numericValue],
277
+ };
278
+ clauses.push(clause);
279
+ return true;
280
+ }
281
+ return false;
282
+ }
283
+
284
+ private checkSimpleNumber(clauses: Clause[]): boolean {
285
+ if (this.getNext().type === 'word') {
286
+ const numericValue: number = NumberParser.parseNumber(
287
+ this.getAt(this.index).value
288
+ );
289
+ if (!NumberParser.isValidNumber(numericValue)) {
290
+ return false;
291
+ }
292
+ const clause: NumberCondition = {operator: '=', values: [numericValue]};
293
+ clauses.push(clause);
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+
299
+ private checkNull(clauses: Clause[]): boolean {
300
+ if (this.getNext().type === 'NULL' || this.getNext().type === 'NOTNULL') {
301
+ const tokenType = this.getNext().type === 'NULL' ? '=' : '!=';
302
+ const clause: NumberCondition = {operator: tokenType, values: [null]};
303
+ clauses.push(clause);
304
+ return true;
305
+ }
306
+ return false;
307
+ }
308
+ }