@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.
- package/README.md +81 -0
- package/SAMPLES.md +381 -0
- package/SERIALIZE_SAMPLES.md +300 -0
- package/dist/a_simple_parser.d.ts +1 -0
- package/dist/a_simple_parser.js +20 -0
- package/dist/a_simple_parser.js.map +1 -0
- package/dist/a_simple_serializer.d.ts +1 -0
- package/dist/a_simple_serializer.js +31 -0
- package/dist/a_simple_serializer.js.map +1 -0
- package/dist/base_parser.d.ts +13 -0
- package/dist/base_parser.js +33 -0
- package/dist/base_parser.js.map +1 -0
- package/dist/base_serializer.d.ts +6 -0
- package/dist/base_serializer.js +11 -0
- package/dist/base_serializer.js.map +1 -0
- package/dist/boolean_parser.d.ts +7 -0
- package/dist/boolean_parser.js +59 -0
- package/dist/boolean_parser.js.map +1 -0
- package/dist/boolean_serializer.d.ts +8 -0
- package/dist/boolean_serializer.js +31 -0
- package/dist/boolean_serializer.js.map +1 -0
- package/dist/clause_types.d.ts +70 -0
- package/dist/clause_types.js +3 -0
- package/dist/clause_types.js.map +1 -0
- package/dist/date_parser.d.ts +22 -0
- package/dist/date_parser.js +315 -0
- package/dist/date_parser.js.map +1 -0
- package/dist/date_serializer.d.ts +10 -0
- package/dist/date_serializer.js +100 -0
- package/dist/date_serializer.js.map +1 -0
- package/dist/filter_parser.d.ts +12 -0
- package/dist/filter_parser.js +66 -0
- package/dist/filter_parser.js.map +1 -0
- package/dist/filter_serializer.d.ts +13 -0
- package/dist/filter_serializer.js +43 -0
- package/dist/filter_serializer.js.map +1 -0
- package/dist/filter_types.d.ts +10 -0
- package/dist/filter_types.js +3 -0
- package/dist/filter_types.js.map +1 -0
- package/dist/generate_samples.d.ts +1 -0
- package/dist/generate_samples.js +344 -0
- package/dist/generate_samples.js.map +1 -0
- package/dist/number_parser.d.ts +20 -0
- package/dist/number_parser.js +275 -0
- package/dist/number_parser.js.map +1 -0
- package/dist/number_serializer.d.ts +11 -0
- package/dist/number_serializer.js +76 -0
- package/dist/number_serializer.js.map +1 -0
- package/dist/string_parser.d.ts +18 -0
- package/dist/string_parser.js +198 -0
- package/dist/string_parser.js.map +1 -0
- package/dist/string_serializer.d.ts +11 -0
- package/dist/string_serializer.js +77 -0
- package/dist/string_serializer.js.map +1 -0
- package/dist/token_types.d.ts +7 -0
- package/dist/token_types.js +3 -0
- package/dist/token_types.js.map +1 -0
- package/dist/tokenizer.d.ts +52 -0
- package/dist/tokenizer.js +263 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/tokenizer.spec.d.ts +1 -0
- package/dist/tokenizer.spec.js +255 -0
- package/dist/tokenizer.spec.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +21 -0
- package/src/DEVELOPING.md +26 -0
- package/src/a_simple_parser.ts +22 -0
- package/src/a_simple_serializer.ts +40 -0
- package/src/base_parser.ts +45 -0
- package/src/base_serializer.ts +9 -0
- package/src/boolean_parser.ts +60 -0
- package/src/boolean_serializer.ts +32 -0
- package/src/clause_types.ts +160 -0
- package/src/date_parser.ts +413 -0
- package/src/date_serializer.ts +114 -0
- package/src/filter_parser.ts +68 -0
- package/src/filter_serializer.ts +49 -0
- package/src/filter_types.ts +12 -0
- package/src/generate_samples.ts +387 -0
- package/src/number_parser.ts +308 -0
- package/src/number_serializer.ts +96 -0
- package/src/string_parser.ts +193 -0
- package/src/string_serializer.ts +87 -0
- package/src/token_types.ts +7 -0
- package/src/tokenizer.spec.ts +273 -0
- package/src/tokenizer.ts +320 -0
- 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
|
+
}
|