@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,413 @@
1
+ import {SpecialToken, Tokenizer, TokenizerParams} from './tokenizer';
2
+ import {
3
+ DateRange,
4
+ DatePrefix,
5
+ DateMoment,
6
+ DateMomentNow,
7
+ DateMomentInterval,
8
+ DateMomentNumberInterval,
9
+ DateMomentNumberUnit,
10
+ DateMomentNumber,
11
+ DateTimeUnit,
12
+ DateWeekday,
13
+ DateMomentIntervalOperator,
14
+ DateMomentNumberIntervalOperator,
15
+ DateMomentNumberUnitOperator,
16
+ DateMomentNumberOperator,
17
+ DateMomentNowOperator,
18
+ DateClause,
19
+ Clause,
20
+ } from './clause_types';
21
+ import {BaseParser} from './base_parser';
22
+ import {Token} from './token_types';
23
+ import {FilterParserResponse, FilterError} from './filter_types';
24
+
25
+ export class DateParser extends BaseParser {
26
+ private static readonly yearRegex: RegExp = /[%_]/;
27
+ private static readonly negatedStartRegex: RegExp = /^-(.+)$/;
28
+
29
+ constructor(input: string) {
30
+ super(input);
31
+ }
32
+
33
+ private tokenize(): void {
34
+ const specialSubstrings: SpecialToken[] = [{type: ',', value: ','}];
35
+ const specialWords: SpecialToken[] = [
36
+ {
37
+ type: 'UNITOFTIME',
38
+ value: /^(second|minute|hour|day|week|month|quarter|year)s?$/i,
39
+ ignoreCase: true,
40
+ },
41
+ {
42
+ type: 'DAYOFWEEK',
43
+ value: /^(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$/i,
44
+ ignoreCase: true,
45
+ },
46
+ {type: 'DATE', value: /^\d\d\d\d-\d\d-\d\d$/},
47
+ {type: 'DATE', value: /^\d\d\d\d-\d\d$/},
48
+ {type: 'TIME', value: /^\d\d:\d\d:\d\d\.\d+$/},
49
+ {type: 'TIME', value: /^\d\d:\d\d:\d\d$/},
50
+ {type: 'TIME', value: /^\d\d:\d\d$/},
51
+ {type: 'NOTNULL', value: '-null', ignoreCase: true},
52
+ {type: 'NULL', value: 'null', ignoreCase: true},
53
+ {type: 'PREFIX', value: /^(before|after)/i, ignoreCase: true},
54
+ {type: 'TODAY', value: 'today', ignoreCase: true},
55
+ {type: 'YESTERDAY', value: 'yesterday', ignoreCase: true},
56
+ {type: 'TOMORROW', value: 'tomorrow', ignoreCase: true},
57
+ {type: 'NOW', value: 'now', ignoreCase: true},
58
+ {type: 'THIS', value: 'this', ignoreCase: true},
59
+ {type: 'LAST', value: 'last', ignoreCase: true},
60
+ {type: 'NEXT', value: 'next', ignoreCase: true},
61
+ {type: 'AGO', value: 'ago', ignoreCase: true},
62
+ {type: 'FROM', value: 'from', ignoreCase: true},
63
+ {type: 'FOR', value: 'for', ignoreCase: true},
64
+ {type: 'TO', value: 'to', ignoreCase: true},
65
+ {type: 'YEARORNUMBER', value: /^\d\d\d\d$/}, // Years are ambiguous, and require special handling.
66
+ {type: 'NUMBER', value: /^[\d.]+/, ignoreCase: true},
67
+ ];
68
+ const params: TokenizerParams = {
69
+ trimWordWhitespace: true,
70
+ splitOnWhitespace: true,
71
+ specialSubstrings,
72
+ specialWords,
73
+ };
74
+
75
+ const tokenizer = new Tokenizer(this.inputString, params);
76
+ this.tokens = tokenizer.parse();
77
+ // console.log('Tokens before moments ', ...this.tokens);
78
+ this.tokens = this.mergeMomentTokens();
79
+ }
80
+
81
+ private mergeMomentTokens(): Token[] {
82
+ const output: Token[] = [];
83
+ this.index = 0;
84
+ while (this.index < this.tokens.length) {
85
+ if (
86
+ this.matchAndMerge('LAST|UNITOFTIME', this.tokens, output) ||
87
+ this.matchAndMerge('LAST|DAYOFWEEK', this.tokens, output) ||
88
+ this.matchAndMerge('LAST|NUMBER|UNITOFTIME', this.tokens, output) ||
89
+ this.matchAndMerge(
90
+ 'LAST|YEARORNUMBER|UNITOFTIME',
91
+ this.tokens,
92
+ output
93
+ ) ||
94
+ this.matchAndMerge('THIS|UNITOFTIME', this.tokens, output) ||
95
+ this.matchAndMerge('NEXT|UNITOFTIME', this.tokens, output) ||
96
+ this.matchAndMerge('NEXT|DAYOFWEEK', this.tokens, output) ||
97
+ this.matchAndMerge('NEXT|NUMBER|UNITOFTIME', this.tokens, output) ||
98
+ this.matchAndMerge(
99
+ 'NEXT|YEARORNUMBER|UNITOFTIME',
100
+ this.tokens,
101
+ output
102
+ ) ||
103
+ this.matchAndMerge('NUMBER|UNITOFTIME|AGO', this.tokens, output) ||
104
+ this.matchAndMerge(
105
+ 'YEARORNUMBER|UNITOFTIME|AGO',
106
+ this.tokens,
107
+ output
108
+ ) ||
109
+ this.matchAndMerge('NUMBER|UNITOFTIME|FROM|NOW', this.tokens, output) ||
110
+ this.matchAndMerge(
111
+ 'YEARORNUMBER|UNITOFTIME|FROM|NOW',
112
+ this.tokens,
113
+ output
114
+ ) ||
115
+ this.matchAndMerge('NUMBER|UNITOFTIME', this.tokens, output) ||
116
+ this.matchAndMerge('YEARORNUMBER|UNITOFTIME', this.tokens, output) ||
117
+ this.matchAndMerge('DATE|TIME', this.tokens, output) ||
118
+ this.matchAndMerge('TODAY', this.tokens, output) ||
119
+ this.matchAndMerge('YESTERDAY', this.tokens, output) ||
120
+ this.matchAndMerge('TOMORROW', this.tokens, output) ||
121
+ this.matchAndMerge('DATE', this.tokens, output) ||
122
+ this.matchAndMerge('YEARORNUMBER', this.tokens, output) ||
123
+ this.matchAndMerge('NOW', this.tokens, output)
124
+ ) {
125
+ continue;
126
+ } else {
127
+ output.push(this.tokens[this.index]);
128
+ this.index++;
129
+ }
130
+ }
131
+ return output;
132
+ }
133
+
134
+ private matchAndMerge(
135
+ types: string,
136
+ tokens: Token[],
137
+ output: Token[]
138
+ ): boolean {
139
+ const idx = this.index;
140
+ const matchedTokens = Tokenizer.matchTypes(types, tokens, idx);
141
+ if (matchedTokens) {
142
+ output.push({
143
+ type: 'MOMENT:' + types,
144
+ value: '',
145
+ values: matchedTokens,
146
+ startIndex: matchedTokens[0].startIndex,
147
+ endIndex: matchedTokens[matchedTokens.length - 1].endIndex,
148
+ });
149
+ this.index += matchedTokens.length;
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+
155
+ public parse(): FilterParserResponse {
156
+ this.tokenize();
157
+ let prefix: DatePrefix | undefined = undefined;
158
+ const clauses: DateClause[] = [];
159
+ const errors: FilterError[] = [];
160
+ this.index = 0;
161
+ while (this.index < this.tokens.length) {
162
+ const token = this.getNext();
163
+ if (token.type === ',') {
164
+ if (prefix) {
165
+ errors.push({
166
+ message: 'Invalid ' + prefix,
167
+ startIndex: token.startIndex,
168
+ endIndex: token.endIndex,
169
+ });
170
+ }
171
+ this.index++;
172
+ } else if (token.type === 'PREFIX') {
173
+ prefix = token.value as DatePrefix;
174
+ this.index++;
175
+ } else if (this.handleRange(clauses)) {
176
+ if (prefix) {
177
+ errors.push({
178
+ message: 'Invalid ' + prefix,
179
+ startIndex: token.startIndex,
180
+ endIndex: token.endIndex,
181
+ });
182
+ this.index++;
183
+ }
184
+ } else if (this.handleMoment(prefix, clauses)) {
185
+ prefix = undefined;
186
+ } else {
187
+ errors.push({
188
+ message: 'Invalid token ' + token.value,
189
+ startIndex: token.startIndex,
190
+ endIndex: token.endIndex,
191
+ });
192
+ prefix = undefined;
193
+ this.index++;
194
+ }
195
+ }
196
+ return {clauses, errors};
197
+ }
198
+
199
+ // LAST|DAYOFWEEK
200
+ private static createMomentInterval(
201
+ prefix: DatePrefix | undefined,
202
+ tokens: Token[]
203
+ ): DateMomentInterval {
204
+ const operator: DateMomentIntervalOperator = tokens[0]
205
+ .type as DateMomentIntervalOperator;
206
+ const unit: DateTimeUnit | DateWeekday = tokens[1].value as
207
+ | DateTimeUnit
208
+ | DateWeekday;
209
+ const moment: DateMomentInterval = {operator, unit};
210
+ if (prefix) {
211
+ moment.prefix = prefix;
212
+ }
213
+ return moment;
214
+ }
215
+
216
+ // LAST|NUMBER|UNITOFTIME
217
+ private static createMomentNumberInterval(
218
+ prefix: DatePrefix | undefined,
219
+ tokens: Token[]
220
+ ): DateMomentNumberInterval {
221
+ const type0 = tokens[0].type;
222
+ const operator: DateMomentNumberIntervalOperator =
223
+ type0 === 'LAST' ? 'LASTN' : 'NEXTN';
224
+ const value: string = tokens[1].value;
225
+ const unit: DateTimeUnit = tokens[2].value as DateTimeUnit;
226
+ const moment: DateMomentNumberInterval = {operator, value, unit};
227
+ if (prefix) {
228
+ moment.prefix = prefix;
229
+ }
230
+ return moment;
231
+ }
232
+
233
+ // NUMBER|UNITOFTIME|AGO
234
+ private static createMomentNumberIntervalAgo(
235
+ prefix: DatePrefix | undefined,
236
+ tokens: Token[]
237
+ ): DateMomentNumberInterval {
238
+ const operator: DateMomentNumberIntervalOperator = 'AGO';
239
+ const value: string = tokens[0].value;
240
+ const unit: DateTimeUnit = tokens[1].value as DateTimeUnit;
241
+ const moment: DateMomentNumberInterval = {operator, value, unit};
242
+ if (prefix) {
243
+ moment.prefix = prefix;
244
+ }
245
+ return moment;
246
+ }
247
+
248
+ // NUMBER|UNITOFTIME|FROM|NOW
249
+ private static createMomentNumberIntervalFromNow(
250
+ prefix: DatePrefix | undefined,
251
+ tokens: Token[]
252
+ ): DateMomentNumberInterval | undefined {
253
+ const operator: DateMomentNumberIntervalOperator = 'FROMNOW';
254
+ const value: string = tokens[0].value;
255
+ const unit: DateTimeUnit = tokens[1].value as DateTimeUnit;
256
+ const moment: DateMomentNumberInterval = {operator, value, unit};
257
+ if (prefix) {
258
+ moment.prefix = prefix;
259
+ }
260
+ return moment;
261
+ }
262
+
263
+ // NUMBER|UNITOFTIME
264
+ private static createMomentNumberUnit(
265
+ prefix: DatePrefix | undefined,
266
+ tokens: Token[]
267
+ ): DateMomentNumberUnit {
268
+ const operator: DateMomentNumberUnitOperator = 'TIMEBLOCK';
269
+ const value: string = tokens[0].value;
270
+ const unit: DateTimeUnit = tokens[1].value as DateTimeUnit;
271
+ const moment: DateMomentNumberUnit = {operator, value, unit};
272
+ if (prefix) {
273
+ moment.prefix = prefix;
274
+ }
275
+ return moment;
276
+ }
277
+
278
+ // DATE DATE|TIME
279
+ private static createMomentNumber(
280
+ prefix: DatePrefix | undefined,
281
+ tokens: Token[]
282
+ ): DateMomentNumber {
283
+ const operator: DateMomentNumberOperator =
284
+ tokens.length === 2 ? 'DATETIME' : 'DATE';
285
+ const moment: DateMomentNumber = {operator, date: tokens[0].value};
286
+ if (tokens.length === 2) {
287
+ moment.time = tokens[1].value;
288
+ }
289
+ if (prefix) {
290
+ moment.prefix = prefix;
291
+ }
292
+ return moment;
293
+ }
294
+
295
+ // NOW YESTERDAY TODAY TOMORROW
296
+ private static createMomentNow(
297
+ prefix: DatePrefix | undefined,
298
+ tokens: Token[]
299
+ ): DateMomentNow {
300
+ const operator: DateMomentNowOperator = tokens[0]
301
+ .type as DateMomentNowOperator;
302
+ const moment: DateMomentNow = {operator};
303
+ if (prefix) {
304
+ moment.prefix = prefix;
305
+ }
306
+ return moment;
307
+ }
308
+
309
+ private static createMomentFromToken(
310
+ prefix: DatePrefix | undefined,
311
+ token: Token
312
+ ): DateMoment | undefined {
313
+ const tokens: Token[] = token.values || [];
314
+ switch (token.type) {
315
+ case 'MOMENT:LAST|UNITOFTIME':
316
+ case 'MOMENT:LAST|DAYOFWEEK':
317
+ case 'MOMENT:THIS|UNITOFTIME':
318
+ case 'MOMENT:NEXT|UNITOFTIME':
319
+ case 'MOMENT:NEXT|DAYOFWEEK':
320
+ return this.createMomentInterval(prefix, tokens);
321
+ case 'MOMENT:LAST|NUMBER|UNITOFTIME':
322
+ case 'MOMENT:LAST|YEARORNUMBER|UNITOFTIME':
323
+ case 'MOMENT:NEXT|NUMBER|UNITOFTIME':
324
+ case 'MOMENT:NEXT|YEARORNUMBER|UNITOFTIME':
325
+ return this.createMomentNumberInterval(prefix, tokens);
326
+ case 'MOMENT:NUMBER|UNITOFTIME|AGO':
327
+ case 'MOMENT:YEARORNUMBER|UNITOFTIME|AGO':
328
+ return this.createMomentNumberIntervalAgo(prefix, tokens);
329
+ case 'MOMENT:NUMBER|UNITOFTIME|FROM|NOW':
330
+ case 'MOMENT:YEARORNUMBER|UNITOFTIME|FROM|NOW':
331
+ return this.createMomentNumberIntervalFromNow(prefix, tokens);
332
+ case 'MOMENT:NUMBER|UNITOFTIME':
333
+ case 'MOMENT:YEARORNUMBER|UNITOFTIME':
334
+ return this.createMomentNumberUnit(prefix, tokens);
335
+ case 'MOMENT:DATE|TIME':
336
+ case 'MOMENT:DATE':
337
+ case 'MOMENT:YEARORNUMBER':
338
+ return this.createMomentNumber(prefix, tokens);
339
+ case 'MOMENT:NOW':
340
+ case 'MOMENT:TODAY':
341
+ case 'MOMENT:YESTERDAY':
342
+ case 'MOMENT:TOMORROW':
343
+ return this.createMomentNow(prefix, tokens);
344
+ default:
345
+ return undefined;
346
+ }
347
+ }
348
+
349
+ private isMatchingToken(
350
+ position: number,
351
+ value: string,
352
+ exactMatch: boolean
353
+ ): boolean {
354
+ if (position < 0 || position >= this.tokens.length) {
355
+ return false;
356
+ }
357
+ return exactMatch
358
+ ? this.tokens[position].type === value
359
+ : this.tokens[position].type.startsWith(value);
360
+ }
361
+
362
+ private handleRange(clauses: Clause[]): boolean {
363
+ if (
364
+ this.isMatchingToken(this.index, 'MOMENT', false) &&
365
+ (this.isMatchingToken(this.index + 1, 'TO', true) ||
366
+ this.isMatchingToken(this.index + 1, 'FOR', true)) &&
367
+ this.isMatchingToken(this.index + 2, 'MOMENT', false)
368
+ ) {
369
+ const startMoment: DateMoment | undefined =
370
+ DateParser.createMomentFromToken(undefined, this.tokens[this.index]);
371
+ const endMoment: DateMoment | undefined =
372
+ DateParser.createMomentFromToken(
373
+ undefined,
374
+ this.tokens[this.index + 2]
375
+ );
376
+ const operator: 'TO' | 'FOR' = this.tokens[this.index + 1].type as
377
+ | 'TO'
378
+ | 'FOR';
379
+ this.index += 3;
380
+ if (startMoment === undefined || endMoment === undefined) {
381
+ return false;
382
+ }
383
+ const dateRange: DateRange = {
384
+ start: startMoment,
385
+ operator,
386
+ end: endMoment,
387
+ };
388
+ clauses.push(dateRange);
389
+ return true;
390
+ }
391
+ return false;
392
+ }
393
+
394
+ private handleMoment(
395
+ prefix: DatePrefix | undefined,
396
+ clauses: DateClause[]
397
+ ): boolean {
398
+ const token: Token = this.getNext();
399
+ if (token.type.startsWith('MOMENT')) {
400
+ const clause: DateMoment | undefined = DateParser.createMomentFromToken(
401
+ prefix,
402
+ token
403
+ );
404
+ this.index++;
405
+ if (clause === undefined) {
406
+ return false;
407
+ }
408
+ clauses.push(clause);
409
+ return true;
410
+ }
411
+ return false;
412
+ }
413
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ DateRange,
3
+ DateMoment,
4
+ DateMomentInterval,
5
+ DateMomentNumberInterval,
6
+ DateMomentNumberUnit,
7
+ DateMomentNumber,
8
+ Clause,
9
+ } from './clause_types';
10
+ import {BaseSerializer} from './base_serializer';
11
+
12
+ export class DateSerializer extends BaseSerializer {
13
+ constructor(clauses: Clause[]) {
14
+ super(clauses);
15
+ }
16
+
17
+ public serialize(): string {
18
+ const result = DateSerializer.clausesToString(this.clauses);
19
+ return result.trim().replace(/,$/, '');
20
+ }
21
+
22
+ private static dateMomentToString(operator: string, clause: Clause): string {
23
+ if (
24
+ operator === 'NOW' ||
25
+ operator === 'TODAY' ||
26
+ operator === 'YESTERDAY' ||
27
+ operator === 'TOMORROW'
28
+ ) {
29
+ const custom: DateMoment = clause as DateMoment;
30
+ return custom.prefix ? custom.prefix + ' ' + operator : operator;
31
+ } else if (
32
+ operator === 'LAST' ||
33
+ operator === 'THIS' ||
34
+ operator === 'NEXT'
35
+ ) {
36
+ const custom: DateMomentInterval = clause as DateMomentInterval;
37
+ let value = custom.operator + ' ' + custom.unit;
38
+ if (custom.prefix) {
39
+ value = custom.prefix + ' ' + value;
40
+ }
41
+ return value;
42
+ } else if (operator === 'LASTN' || operator === 'NEXTN') {
43
+ const custom: DateMomentNumberInterval =
44
+ clause as DateMomentNumberInterval;
45
+ operator = operator.substring(0, 4); // Strip "N"
46
+ let value = operator + ' ' + custom.value + ' ' + custom.unit;
47
+ if (custom.prefix) {
48
+ value = custom.prefix + ' ' + value;
49
+ }
50
+ return value;
51
+ } else if (operator === 'AGO' || operator === 'FROMNOW') {
52
+ const custom: DateMomentNumberInterval =
53
+ clause as DateMomentNumberInterval;
54
+ if (operator === 'FROMNOW') operator = 'FROM NOW';
55
+ let value = custom.value + ' ' + custom.unit + ' ' + operator;
56
+ if (custom.prefix) {
57
+ value = custom.prefix + ' ' + value;
58
+ }
59
+ return value;
60
+ } else if (operator === 'TIMEBLOCK') {
61
+ const custom: DateMomentNumberUnit = clause as DateMomentNumberUnit;
62
+ let value = custom.value + ' ' + custom.unit;
63
+ if (custom.prefix) {
64
+ value = custom.prefix + ' ' + value;
65
+ }
66
+ return value;
67
+ } else if (operator === 'DATE' || operator === 'DATETIME') {
68
+ const custom: DateMomentNumber = clause as DateMomentNumber;
69
+ let value = custom.date;
70
+ if (custom.time) {
71
+ value = value + ' ' + custom.time;
72
+ }
73
+ if (custom.prefix) {
74
+ value = custom.prefix + ' ' + value;
75
+ }
76
+ return value;
77
+ }
78
+ return '';
79
+ }
80
+
81
+ private static dateRangeToString(
82
+ operator: 'TO' | 'FOR',
83
+ clause: DateRange
84
+ ): string {
85
+ return (
86
+ DateSerializer.dateMomentToString(clause.start.operator, clause.start) +
87
+ ' ' +
88
+ clause.operator +
89
+ ' ' +
90
+ DateSerializer.dateMomentToString(clause.end.operator, clause.end)
91
+ );
92
+ }
93
+
94
+ private static clauseToString(operator: string, clause: Clause): string {
95
+ if (operator === 'TO' || operator === 'FOR') {
96
+ const custom = clause as DateRange;
97
+ return DateSerializer.dateRangeToString(operator, custom);
98
+ }
99
+ return DateSerializer.dateMomentToString(operator, clause);
100
+ }
101
+
102
+ private static clausesToString(clauses: Clause[]): string {
103
+ let result = '';
104
+ for (const clause of clauses) {
105
+ if ('operator' in clause) {
106
+ result += DateSerializer.clauseToString(clause.operator, clause);
107
+ result += ', ';
108
+ } else {
109
+ throw new Error('Invalid date clause ' + JSON.stringify(clause));
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+ }
@@ -0,0 +1,68 @@
1
+ import {FilterError, FilterParserResponse} from './filter_types';
2
+ import {Token} from './token_types';
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
+
9
+ export type FilterType = 'boolean' | 'number' | 'string' | 'date';
10
+
11
+ export class FilterParser {
12
+ constructor(
13
+ private input: string,
14
+ private type: FilterType
15
+ ) {
16
+ this.input = input;
17
+ this.type = type;
18
+ }
19
+
20
+ private initParser(): BaseParser {
21
+ switch (this.type) {
22
+ case 'boolean':
23
+ return new BooleanParser(this.input);
24
+ case 'number':
25
+ return new NumberParser(this.input);
26
+ case 'string':
27
+ return new StringParser(this.input);
28
+ case 'date':
29
+ return new DateParser(this.input);
30
+ }
31
+ }
32
+
33
+ /* eslint-disable no-console */
34
+ public getTokens(): Token[] {
35
+ let tokens: Token[] = [];
36
+ try {
37
+ const parser = this.initParser();
38
+ tokens = parser.getTokens();
39
+ } catch (ex: Error | unknown) {
40
+ if (ex instanceof Error) console.error('Error: ', ex.message, '\n');
41
+ else {
42
+ console.error('Unknown error: ', ex, '\n');
43
+ }
44
+ }
45
+ return tokens;
46
+ }
47
+ /* eslint-enable no-console */
48
+
49
+ private makeErrorMessage(message: string): FilterError {
50
+ return {message, startIndex: 0, endIndex: this.input.length};
51
+ }
52
+
53
+ public parse(): FilterParserResponse {
54
+ try {
55
+ const parser = this.initParser();
56
+ return parser.parse();
57
+ } catch (ex: Error | unknown) {
58
+ if (ex instanceof Error) {
59
+ return {clauses: [], errors: [this.makeErrorMessage(ex.message)]};
60
+ } else {
61
+ return {
62
+ clauses: [],
63
+ errors: [this.makeErrorMessage('Unknown error ' + ex)],
64
+ };
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,49 @@
1
+ import {Clause} from './clause_types';
2
+ import {BooleanSerializer} from './boolean_serializer';
3
+ import {StringSerializer} from './string_serializer';
4
+ import {NumberSerializer} from './number_serializer';
5
+ import {DateSerializer} from './date_serializer';
6
+ import {BaseSerializer} from './base_serializer';
7
+
8
+ export type FilterType = 'boolean' | 'number' | 'string' | 'date';
9
+
10
+ export interface FilterSerializerResponse {
11
+ result: string;
12
+ error?: string;
13
+ }
14
+
15
+ export class FilterSerializer {
16
+ constructor(
17
+ private input: Clause[],
18
+ private type: FilterType
19
+ ) {
20
+ this.input = input;
21
+ this.type = type;
22
+ }
23
+
24
+ private initSerializer(): BaseSerializer {
25
+ switch (this.type) {
26
+ case 'boolean':
27
+ return new BooleanSerializer(this.input);
28
+ case 'number':
29
+ return new NumberSerializer(this.input);
30
+ case 'string':
31
+ return new StringSerializer(this.input);
32
+ case 'date':
33
+ return new DateSerializer(this.input);
34
+ }
35
+ }
36
+
37
+ public serialize(): FilterSerializerResponse {
38
+ try {
39
+ const serializer = this.initSerializer();
40
+ return {result: serializer.serialize()};
41
+ } catch (ex: Error | unknown) {
42
+ if (ex instanceof Error) {
43
+ return {result: '', error: ex.message};
44
+ } else {
45
+ return {result: '', error: 'Unknown error ' + ex};
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,12 @@
1
+ import {Clause} from './clause_types';
2
+
3
+ export interface FilterError {
4
+ message: string;
5
+ startIndex: number;
6
+ endIndex: number;
7
+ }
8
+
9
+ export interface FilterParserResponse {
10
+ clauses: Clause[];
11
+ errors: FilterError[];
12
+ }