@briza/illogical 2.1.0 → 2.2.0
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/lib/illogical.cjs +251 -75
- package/lib/illogical.esm.js +251 -75
- package/package.json +21 -21
- package/types/common/type-check.d.ts +5 -0
- package/types/common/util.d.ts +6 -0
- package/types/expression/arithmetic/dateArithmeticTypeCheck.d.ts +2 -0
- package/types/expression/arithmetic/index.d.ts +2 -0
- package/types/expression/arithmetic/mutateDateWithDuration.d.ts +2 -0
package/lib/illogical.cjs
CHANGED
|
@@ -121,6 +121,71 @@ function areAllResults(values) {
|
|
|
121
121
|
function areAllNumbers(results) {
|
|
122
122
|
return results.every(isNumber);
|
|
123
123
|
}
|
|
124
|
+
function isReference(operand) {
|
|
125
|
+
return operand.constructor.name === 'Reference';
|
|
126
|
+
}
|
|
127
|
+
function isValue(operand) {
|
|
128
|
+
return operand.constructor.name === 'Value';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Convert a value to number if possible, otherwise return undefined
|
|
133
|
+
* @param value value to be converted to number
|
|
134
|
+
*/
|
|
135
|
+
const toNumber = value => {
|
|
136
|
+
const isValueNumber = isNumber(value);
|
|
137
|
+
if (isValueNumber) {
|
|
138
|
+
return value;
|
|
139
|
+
} else if (isString(value)) {
|
|
140
|
+
if (value.match(/^\d+\.\d+$/)) {
|
|
141
|
+
return parseFloat(value);
|
|
142
|
+
} else if (value.match(/^0$|^[1-9]\d*$/)) {
|
|
143
|
+
return parseInt(value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convert a value to string if possible, otherwise return undefined
|
|
151
|
+
* @param value value to be converted to string
|
|
152
|
+
*/
|
|
153
|
+
const toString = value => {
|
|
154
|
+
if (isNumber(value)) {
|
|
155
|
+
return `${value}`;
|
|
156
|
+
} else if (isString(value)) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Convert a value to number if it's type is string, otherwise return NaN
|
|
163
|
+
* @param value value to be converted to number
|
|
164
|
+
*/
|
|
165
|
+
const toDateNumber = value => {
|
|
166
|
+
if (isString(value)) {
|
|
167
|
+
return Date.parse(value);
|
|
168
|
+
}
|
|
169
|
+
return NaN;
|
|
170
|
+
};
|
|
171
|
+
const formatDateNumber = dateNumber => new Date(dateNumber).toISOString().split('T')[0];
|
|
172
|
+
const toDateDuration = value => {
|
|
173
|
+
if (!isString(value)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const isDateDuration = value.match(/^([1-9]\d*)([dmy])$/);
|
|
177
|
+
if (!isDateDuration) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const unit = isDateDuration[2];
|
|
181
|
+
if (unit !== 'd' && unit !== 'm' && unit !== 'y') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
amount: parseInt(isDateDuration[1], 10),
|
|
186
|
+
unit
|
|
187
|
+
};
|
|
188
|
+
};
|
|
124
189
|
|
|
125
190
|
/**
|
|
126
191
|
* Abstract arithmetic expression
|
|
@@ -159,6 +224,21 @@ class Arithmetic {
|
|
|
159
224
|
}
|
|
160
225
|
return presentValues;
|
|
161
226
|
}
|
|
227
|
+
getDateCalculationResults(results) {
|
|
228
|
+
const [date, ...durations] = results;
|
|
229
|
+
const dateNumber = toDateNumber(date);
|
|
230
|
+
if (isNaN(dateNumber)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
const parsedDurations = durations.flatMap(d => {
|
|
234
|
+
const parsedDuration = toDateDuration(d);
|
|
235
|
+
return parsedDuration ? [parsedDuration] : [];
|
|
236
|
+
});
|
|
237
|
+
if (parsedDurations.length !== durations.length) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return [dateNumber, ...parsedDurations];
|
|
241
|
+
}
|
|
162
242
|
|
|
163
243
|
/**
|
|
164
244
|
* Performs the arithmetic operation on the operands evaluated values.
|
|
@@ -280,6 +360,63 @@ class Multiply extends Arithmetic {
|
|
|
280
360
|
}
|
|
281
361
|
}
|
|
282
362
|
|
|
363
|
+
const dateArithmeticTypeCheck = (...operands) => {
|
|
364
|
+
const [first, ...rest] = operands;
|
|
365
|
+
const restAreAllReferences = rest.every(op => isReference(op));
|
|
366
|
+
const values = rest.filter(op => isValue(op));
|
|
367
|
+
const valuesAreAllDurations = values.every(op => !!toDateDuration(op.evaluate()));
|
|
368
|
+
const valuesAreAllNumbers = values.every(op => isNumber(op.evaluate()));
|
|
369
|
+
if (isReference(first)) {
|
|
370
|
+
if (!restAreAllReferences && !valuesAreAllDurations && !valuesAreAllNumbers) {
|
|
371
|
+
throw new Error('sum expression value literals should be all numbers or all date durations');
|
|
372
|
+
}
|
|
373
|
+
} else if (isValue(first)) {
|
|
374
|
+
if (isNumber(first.evaluate())) {
|
|
375
|
+
if (!restAreAllReferences && !valuesAreAllNumbers) {
|
|
376
|
+
throw new Error('sum expression value literals should be all numbers');
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
if (!isNaN(toDateNumber(first.evaluate()))) {
|
|
380
|
+
if (!restAreAllReferences && !valuesAreAllDurations) {
|
|
381
|
+
throw new Error('sum expression value literals should be all date durations');
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
throw new Error(
|
|
385
|
+
// eslint-disable-next-line max-len
|
|
386
|
+
'sum expression value literals should be all numbers or starting with an iso date string followed by date durations');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const mutateDateWithDuration = (dateNumber, duration, operator) => {
|
|
393
|
+
const {
|
|
394
|
+
amount,
|
|
395
|
+
unit
|
|
396
|
+
} = duration;
|
|
397
|
+
const date = new Date(dateNumber);
|
|
398
|
+
let targetYear = date.getUTCFullYear();
|
|
399
|
+
let targetMonthIndex = date.getUTCMonth();
|
|
400
|
+
const day = date.getUTCDate();
|
|
401
|
+
let targetDay = day;
|
|
402
|
+
if (unit === 'd') {
|
|
403
|
+
const targetDate = new Date(targetYear, targetMonthIndex, targetDay);
|
|
404
|
+
targetDate.setDate(operator === 'sum' ? targetDate.getDate() + amount : targetDate.getDate() - amount);
|
|
405
|
+
targetYear = targetDate.getFullYear();
|
|
406
|
+
targetMonthIndex = targetDate.getMonth();
|
|
407
|
+
targetDay = targetDate.getDate();
|
|
408
|
+
} else if (unit === 'm' || unit === 'y') {
|
|
409
|
+
if (unit === 'm') {
|
|
410
|
+
targetMonthIndex = operator === 'sum' ? targetMonthIndex + amount : targetMonthIndex - amount;
|
|
411
|
+
} else if (unit === 'y') {
|
|
412
|
+
targetYear = operator === 'sum' ? targetYear + amount : targetYear - amount;
|
|
413
|
+
}
|
|
414
|
+
const lastDayOfTargetMonth = new Date(targetYear, targetMonthIndex + 1, 0).getDate();
|
|
415
|
+
targetDay = Math.min(day, lastDayOfTargetMonth);
|
|
416
|
+
}
|
|
417
|
+
return new Date(Date.UTC(targetYear, targetMonthIndex, targetDay)).getTime();
|
|
418
|
+
};
|
|
419
|
+
|
|
283
420
|
// Operator key
|
|
284
421
|
const OPERATOR$j = Symbol('SUBTRACT');
|
|
285
422
|
const subtractWithExpectedDecimals = operateWithExpectedDecimals$1('subtract');
|
|
@@ -301,9 +438,15 @@ class Subtract extends Arithmetic {
|
|
|
301
438
|
if (operands.length < 2) {
|
|
302
439
|
throw new Error('subtract expression requires at least 2 operands');
|
|
303
440
|
}
|
|
441
|
+
dateArithmeticTypeCheck(...operands);
|
|
304
442
|
super('-', OPERATOR$j, operands);
|
|
305
443
|
}
|
|
306
444
|
operate(results) {
|
|
445
|
+
const dateCalculationResults = this.getDateCalculationResults(results);
|
|
446
|
+
if (dateCalculationResults) {
|
|
447
|
+
const [dateNumber, ...durations] = dateCalculationResults;
|
|
448
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, duration, 'subtract'), dateNumber));
|
|
449
|
+
}
|
|
307
450
|
const presentResults = this.getResultValues(results);
|
|
308
451
|
if (presentResults === false) {
|
|
309
452
|
return false;
|
|
@@ -333,9 +476,15 @@ class Sum extends Arithmetic {
|
|
|
333
476
|
if (operands.length < 2) {
|
|
334
477
|
throw new Error('sum expression requires at least 2 operands');
|
|
335
478
|
}
|
|
479
|
+
dateArithmeticTypeCheck(...operands);
|
|
336
480
|
super('+', OPERATOR$i, operands);
|
|
337
481
|
}
|
|
338
482
|
operate(results) {
|
|
483
|
+
const dateCalculationResults = this.getDateCalculationResults(results);
|
|
484
|
+
if (dateCalculationResults) {
|
|
485
|
+
const [dateNumber, ...durations] = dateCalculationResults;
|
|
486
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, duration, 'sum'), dateNumber));
|
|
487
|
+
}
|
|
339
488
|
const presentResults = this.getResultValues(results);
|
|
340
489
|
if (presentResults === false) {
|
|
341
490
|
return false;
|
|
@@ -374,6 +523,17 @@ class Operand {
|
|
|
374
523
|
* @return {string}
|
|
375
524
|
*/
|
|
376
525
|
function printValue(value) {
|
|
526
|
+
const dateDuration = toDateDuration(value);
|
|
527
|
+
if (dateDuration) {
|
|
528
|
+
switch (dateDuration.unit) {
|
|
529
|
+
case 'd':
|
|
530
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'days' : 'day'}"`;
|
|
531
|
+
case 'm':
|
|
532
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'months' : 'month'}"`;
|
|
533
|
+
case 'y':
|
|
534
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'years' : 'year'}"`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
377
537
|
if (isString(value)) {
|
|
378
538
|
return `"${value}"`;
|
|
379
539
|
}
|
|
@@ -428,47 +588,6 @@ class Value extends Operand {
|
|
|
428
588
|
}
|
|
429
589
|
}
|
|
430
590
|
|
|
431
|
-
/**
|
|
432
|
-
* Convert a value to number if possible, otherwise return undefined
|
|
433
|
-
* @param value value to be converted to number
|
|
434
|
-
*/
|
|
435
|
-
const toNumber = value => {
|
|
436
|
-
const isValueNumber = isNumber(value);
|
|
437
|
-
if (isValueNumber) {
|
|
438
|
-
return value;
|
|
439
|
-
} else if (isString(value)) {
|
|
440
|
-
if (value.match(/^\d+\.\d+$/)) {
|
|
441
|
-
return parseFloat(value);
|
|
442
|
-
} else if (value.match(/^0$|^[1-9]\d*$/)) {
|
|
443
|
-
return parseInt(value);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return undefined;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Convert a value to string if possible, otherwise return undefined
|
|
451
|
-
* @param value value to be converted to string
|
|
452
|
-
*/
|
|
453
|
-
const toString = value => {
|
|
454
|
-
if (isNumber(value)) {
|
|
455
|
-
return `${value}`;
|
|
456
|
-
} else if (isString(value)) {
|
|
457
|
-
return value;
|
|
458
|
-
}
|
|
459
|
-
return undefined;
|
|
460
|
-
};
|
|
461
|
-
/**
|
|
462
|
-
* Convert a value to number if it's type is string, otherwise return NaN
|
|
463
|
-
* @param value value to be converted to number
|
|
464
|
-
*/
|
|
465
|
-
const toDateNumber = value => {
|
|
466
|
-
if (isString(value)) {
|
|
467
|
-
return Date.parse(value);
|
|
468
|
-
}
|
|
469
|
-
return NaN;
|
|
470
|
-
};
|
|
471
|
-
|
|
472
591
|
const keyWithArrayIndexRegex$1 = /^(?<currentKey>[^[\]]+?)(?<indexes>(?:\[\d+])+)?$/;
|
|
473
592
|
const arrayIndexRegex$1 = /\[(\d+)]/g;
|
|
474
593
|
function parseBacktickWrappedKey$1(key) {
|
|
@@ -2722,6 +2841,10 @@ function relationalCompare$1(left, right, op) {
|
|
|
2722
2841
|
}
|
|
2723
2842
|
return false;
|
|
2724
2843
|
}
|
|
2844
|
+
function dateArithmeticReduce$1(values, op) {
|
|
2845
|
+
const [date, ...durations] = values;
|
|
2846
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
2847
|
+
}
|
|
2725
2848
|
function arithmeticReduce$1(values, op) {
|
|
2726
2849
|
if (op === OP_SUM) {
|
|
2727
2850
|
return values.reduce(addDecimals$1);
|
|
@@ -3144,22 +3267,37 @@ function interpret(compiled, ctx) {
|
|
|
3144
3267
|
stack$1[++stackTop$1] = false;
|
|
3145
3268
|
break;
|
|
3146
3269
|
}
|
|
3270
|
+
const isDateArithmetic = !isNaN(toDateNumber(a));
|
|
3147
3271
|
if (!isNumber(a) || !isNumber(b)) {
|
|
3148
|
-
|
|
3272
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
3273
|
+
if (isDateArithmetic) {
|
|
3274
|
+
const duration = toDateDuration(b);
|
|
3275
|
+
if (!duration) {
|
|
3276
|
+
throw new Error(`arithmetic operand is not a date duration: ${b}`);
|
|
3277
|
+
}
|
|
3278
|
+
} else {
|
|
3279
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3280
|
+
}
|
|
3281
|
+
} else {
|
|
3282
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3283
|
+
}
|
|
3149
3284
|
}
|
|
3150
3285
|
if (op === OP_SUM) {
|
|
3151
|
-
stack$1[++stackTop$1] = addDecimals$1(a, b);
|
|
3286
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? addDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'sum'));
|
|
3152
3287
|
} else if (op === OP_SUBTRACT) {
|
|
3153
|
-
stack$1[++stackTop$1] = subtractDecimals$1(a, b);
|
|
3154
|
-
} else if (
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3288
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? subtractDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'subtract'));
|
|
3289
|
+
} else if (isNumber(a) && isNumber(b)) {
|
|
3290
|
+
if (op === OP_MULTIPLY) {
|
|
3291
|
+
stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
|
|
3292
|
+
} else {
|
|
3293
|
+
stack$1[++stackTop$1] = divideDecimals$1(a, b);
|
|
3294
|
+
}
|
|
3158
3295
|
}
|
|
3159
3296
|
break;
|
|
3160
3297
|
}
|
|
3161
3298
|
const values = new Array(n);
|
|
3162
3299
|
let hasNull = false;
|
|
3300
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[stackTop$1 - n + 1])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3163
3301
|
for (let j = n - 1; j >= 0; j--) {
|
|
3164
3302
|
const v = stack$1[stackTop$1--];
|
|
3165
3303
|
if (v === null || v === undefined) {
|
|
@@ -3167,11 +3305,20 @@ function interpret(compiled, ctx) {
|
|
|
3167
3305
|
break;
|
|
3168
3306
|
}
|
|
3169
3307
|
if (!isNumber(v)) {
|
|
3170
|
-
|
|
3308
|
+
if (isDateArithmetic) {
|
|
3309
|
+
const duration = toDateDuration(v);
|
|
3310
|
+
if (!duration && j > 0) {
|
|
3311
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
3312
|
+
}
|
|
3313
|
+
} else {
|
|
3314
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
if (isNumber(v) || isString(v)) {
|
|
3318
|
+
values[j] = v;
|
|
3171
3319
|
}
|
|
3172
|
-
values[j] = v;
|
|
3173
3320
|
}
|
|
3174
|
-
stack$1[++stackTop$1] = hasNull ? false : arithmeticReduce$1(values, op);
|
|
3321
|
+
stack$1[++stackTop$1] = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce$1(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce$1(values, op) : false;
|
|
3175
3322
|
break;
|
|
3176
3323
|
}
|
|
3177
3324
|
|
|
@@ -3547,6 +3694,10 @@ function arithmeticReduce(values, op) {
|
|
|
3547
3694
|
}
|
|
3548
3695
|
return values.reduce(divideDecimals);
|
|
3549
3696
|
}
|
|
3697
|
+
function dateArithmeticReduce(values, op) {
|
|
3698
|
+
const [date, ...durations] = values;
|
|
3699
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
3700
|
+
}
|
|
3550
3701
|
function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
3551
3702
|
const {
|
|
3552
3703
|
bytecode,
|
|
@@ -4197,31 +4348,45 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4197
4348
|
stack[++stackTop] = false;
|
|
4198
4349
|
break;
|
|
4199
4350
|
}
|
|
4351
|
+
const isDateArithmetic = !isNaN(toDateNumber(aVal));
|
|
4200
4352
|
if (!isNumber(aVal) || !isNumber(bVal)) {
|
|
4201
|
-
|
|
4353
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
4354
|
+
if (isDateArithmetic) {
|
|
4355
|
+
const duration = toDateDuration(bVal);
|
|
4356
|
+
if (!duration) {
|
|
4357
|
+
throw new Error(`arithmetic operand is not a date duration: ${bVal}`);
|
|
4358
|
+
}
|
|
4359
|
+
} else {
|
|
4360
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4361
|
+
}
|
|
4362
|
+
} else {
|
|
4363
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4364
|
+
}
|
|
4202
4365
|
}
|
|
4203
4366
|
// After the throw above, aVal and bVal are narrowed to number
|
|
4204
4367
|
if (op === OP_SUM) {
|
|
4205
|
-
stack[++stackTop] = addDecimals(aVal, bVal);
|
|
4368
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? addDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'sum'));
|
|
4206
4369
|
} else if (op === OP_SUBTRACT) {
|
|
4207
|
-
stack[++stackTop] = subtractDecimals(aVal, bVal);
|
|
4208
|
-
} else if (
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
// Division — match OOP isInfinite guard: use a marker so
|
|
4212
|
-
// comparisons can decide whether to preserve the expression.
|
|
4213
|
-
const result = divideDecimals(aVal, bVal);
|
|
4214
|
-
if (isUnusableResult(result)) {
|
|
4215
|
-
// Push marker with original operands for reconstruction
|
|
4216
|
-
const marker = {
|
|
4217
|
-
_r: 4,
|
|
4218
|
-
_val: result,
|
|
4219
|
-
left: aVal,
|
|
4220
|
-
right: bVal
|
|
4221
|
-
};
|
|
4222
|
-
stack[++stackTop] = marker;
|
|
4370
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? subtractDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'subtract'));
|
|
4371
|
+
} else if (isNumber(aVal) && isNumber(bVal)) {
|
|
4372
|
+
if (op === OP_MULTIPLY) {
|
|
4373
|
+
stack[++stackTop] = multiplyDecimals(aVal, bVal);
|
|
4223
4374
|
} else {
|
|
4224
|
-
|
|
4375
|
+
// Division — match OOP isInfinite guard: use a marker so
|
|
4376
|
+
// comparisons can decide whether to preserve the expression.
|
|
4377
|
+
const result = divideDecimals(aVal, bVal);
|
|
4378
|
+
if (isUnusableResult(result)) {
|
|
4379
|
+
// Push marker with original operands for reconstruction
|
|
4380
|
+
const marker = {
|
|
4381
|
+
_r: 4,
|
|
4382
|
+
_val: result,
|
|
4383
|
+
left: aVal,
|
|
4384
|
+
right: bVal
|
|
4385
|
+
};
|
|
4386
|
+
stack[++stackTop] = marker;
|
|
4387
|
+
} else {
|
|
4388
|
+
stack[++stackTop] = result;
|
|
4389
|
+
}
|
|
4225
4390
|
}
|
|
4226
4391
|
}
|
|
4227
4392
|
break;
|
|
@@ -4229,6 +4394,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4229
4394
|
// N-operand path
|
|
4230
4395
|
const values = new Array(n);
|
|
4231
4396
|
let hasNull = false;
|
|
4397
|
+
const firstOperand = slotVal(stack[stackTop - n + 1]);
|
|
4398
|
+
const isDateArithmetic = typeof firstOperand !== 'object' && !isNaN(toDateNumber(firstOperand)) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
4232
4399
|
for (let j = n - 1; j >= 0; j--) {
|
|
4233
4400
|
const v = slotVal(stack[stackTop--]);
|
|
4234
4401
|
if (v === null || v === undefined) {
|
|
@@ -4236,15 +4403,24 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4236
4403
|
break;
|
|
4237
4404
|
}
|
|
4238
4405
|
if (!isNumber(v)) {
|
|
4239
|
-
|
|
4406
|
+
if (isDateArithmetic) {
|
|
4407
|
+
const duration = toDateDuration(v);
|
|
4408
|
+
if (!duration && j > 0) {
|
|
4409
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
4410
|
+
}
|
|
4411
|
+
} else {
|
|
4412
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
4413
|
+
}
|
|
4240
4414
|
}
|
|
4241
4415
|
// After the throw above, v is narrowed to number
|
|
4242
|
-
|
|
4416
|
+
if (isNumber(v) || isString(v)) {
|
|
4417
|
+
values[j] = v;
|
|
4418
|
+
}
|
|
4243
4419
|
}
|
|
4244
|
-
const reduced = hasNull ? false : arithmeticReduce(values, op);
|
|
4420
|
+
const reduced = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce(values, op) : false;
|
|
4245
4421
|
if (hasNull) {
|
|
4246
4422
|
stack[++stackTop] = false;
|
|
4247
|
-
} else if (op === OP_DIVIDE && reduced !== false && isUnusableResult(reduced)) {
|
|
4423
|
+
} else if (op === OP_DIVIDE && reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4248
4424
|
// N-operand division producing Infinity/NaN — create marker
|
|
4249
4425
|
const result = reduced;
|
|
4250
4426
|
const marker = {
|
|
@@ -4254,7 +4430,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4254
4430
|
right: values[1]
|
|
4255
4431
|
};
|
|
4256
4432
|
stack[++stackTop] = marker;
|
|
4257
|
-
} else if (reduced !== false && isUnusableResult(reduced)) {
|
|
4433
|
+
} else if (reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4258
4434
|
// Other arithmetic producing Infinity/NaN — preserve expression
|
|
4259
4435
|
stack[++stackTop] = makeResidual([opNames[op], ...values]);
|
|
4260
4436
|
} else {
|
package/lib/illogical.esm.js
CHANGED
|
@@ -117,6 +117,71 @@ function areAllResults(values) {
|
|
|
117
117
|
function areAllNumbers(results) {
|
|
118
118
|
return results.every(isNumber);
|
|
119
119
|
}
|
|
120
|
+
function isReference(operand) {
|
|
121
|
+
return operand.constructor.name === 'Reference';
|
|
122
|
+
}
|
|
123
|
+
function isValue(operand) {
|
|
124
|
+
return operand.constructor.name === 'Value';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert a value to number if possible, otherwise return undefined
|
|
129
|
+
* @param value value to be converted to number
|
|
130
|
+
*/
|
|
131
|
+
const toNumber = value => {
|
|
132
|
+
const isValueNumber = isNumber(value);
|
|
133
|
+
if (isValueNumber) {
|
|
134
|
+
return value;
|
|
135
|
+
} else if (isString(value)) {
|
|
136
|
+
if (value.match(/^\d+\.\d+$/)) {
|
|
137
|
+
return parseFloat(value);
|
|
138
|
+
} else if (value.match(/^0$|^[1-9]\d*$/)) {
|
|
139
|
+
return parseInt(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Convert a value to string if possible, otherwise return undefined
|
|
147
|
+
* @param value value to be converted to string
|
|
148
|
+
*/
|
|
149
|
+
const toString = value => {
|
|
150
|
+
if (isNumber(value)) {
|
|
151
|
+
return `${value}`;
|
|
152
|
+
} else if (isString(value)) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Convert a value to number if it's type is string, otherwise return NaN
|
|
159
|
+
* @param value value to be converted to number
|
|
160
|
+
*/
|
|
161
|
+
const toDateNumber = value => {
|
|
162
|
+
if (isString(value)) {
|
|
163
|
+
return Date.parse(value);
|
|
164
|
+
}
|
|
165
|
+
return NaN;
|
|
166
|
+
};
|
|
167
|
+
const formatDateNumber = dateNumber => new Date(dateNumber).toISOString().split('T')[0];
|
|
168
|
+
const toDateDuration = value => {
|
|
169
|
+
if (!isString(value)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const isDateDuration = value.match(/^([1-9]\d*)([dmy])$/);
|
|
173
|
+
if (!isDateDuration) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const unit = isDateDuration[2];
|
|
177
|
+
if (unit !== 'd' && unit !== 'm' && unit !== 'y') {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
amount: parseInt(isDateDuration[1], 10),
|
|
182
|
+
unit
|
|
183
|
+
};
|
|
184
|
+
};
|
|
120
185
|
|
|
121
186
|
/**
|
|
122
187
|
* Abstract arithmetic expression
|
|
@@ -155,6 +220,21 @@ class Arithmetic {
|
|
|
155
220
|
}
|
|
156
221
|
return presentValues;
|
|
157
222
|
}
|
|
223
|
+
getDateCalculationResults(results) {
|
|
224
|
+
const [date, ...durations] = results;
|
|
225
|
+
const dateNumber = toDateNumber(date);
|
|
226
|
+
if (isNaN(dateNumber)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const parsedDurations = durations.flatMap(d => {
|
|
230
|
+
const parsedDuration = toDateDuration(d);
|
|
231
|
+
return parsedDuration ? [parsedDuration] : [];
|
|
232
|
+
});
|
|
233
|
+
if (parsedDurations.length !== durations.length) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return [dateNumber, ...parsedDurations];
|
|
237
|
+
}
|
|
158
238
|
|
|
159
239
|
/**
|
|
160
240
|
* Performs the arithmetic operation on the operands evaluated values.
|
|
@@ -276,6 +356,63 @@ class Multiply extends Arithmetic {
|
|
|
276
356
|
}
|
|
277
357
|
}
|
|
278
358
|
|
|
359
|
+
const dateArithmeticTypeCheck = (...operands) => {
|
|
360
|
+
const [first, ...rest] = operands;
|
|
361
|
+
const restAreAllReferences = rest.every(op => isReference(op));
|
|
362
|
+
const values = rest.filter(op => isValue(op));
|
|
363
|
+
const valuesAreAllDurations = values.every(op => !!toDateDuration(op.evaluate()));
|
|
364
|
+
const valuesAreAllNumbers = values.every(op => isNumber(op.evaluate()));
|
|
365
|
+
if (isReference(first)) {
|
|
366
|
+
if (!restAreAllReferences && !valuesAreAllDurations && !valuesAreAllNumbers) {
|
|
367
|
+
throw new Error('sum expression value literals should be all numbers or all date durations');
|
|
368
|
+
}
|
|
369
|
+
} else if (isValue(first)) {
|
|
370
|
+
if (isNumber(first.evaluate())) {
|
|
371
|
+
if (!restAreAllReferences && !valuesAreAllNumbers) {
|
|
372
|
+
throw new Error('sum expression value literals should be all numbers');
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
if (!isNaN(toDateNumber(first.evaluate()))) {
|
|
376
|
+
if (!restAreAllReferences && !valuesAreAllDurations) {
|
|
377
|
+
throw new Error('sum expression value literals should be all date durations');
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error(
|
|
381
|
+
// eslint-disable-next-line max-len
|
|
382
|
+
'sum expression value literals should be all numbers or starting with an iso date string followed by date durations');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const mutateDateWithDuration = (dateNumber, duration, operator) => {
|
|
389
|
+
const {
|
|
390
|
+
amount,
|
|
391
|
+
unit
|
|
392
|
+
} = duration;
|
|
393
|
+
const date = new Date(dateNumber);
|
|
394
|
+
let targetYear = date.getUTCFullYear();
|
|
395
|
+
let targetMonthIndex = date.getUTCMonth();
|
|
396
|
+
const day = date.getUTCDate();
|
|
397
|
+
let targetDay = day;
|
|
398
|
+
if (unit === 'd') {
|
|
399
|
+
const targetDate = new Date(targetYear, targetMonthIndex, targetDay);
|
|
400
|
+
targetDate.setDate(operator === 'sum' ? targetDate.getDate() + amount : targetDate.getDate() - amount);
|
|
401
|
+
targetYear = targetDate.getFullYear();
|
|
402
|
+
targetMonthIndex = targetDate.getMonth();
|
|
403
|
+
targetDay = targetDate.getDate();
|
|
404
|
+
} else if (unit === 'm' || unit === 'y') {
|
|
405
|
+
if (unit === 'm') {
|
|
406
|
+
targetMonthIndex = operator === 'sum' ? targetMonthIndex + amount : targetMonthIndex - amount;
|
|
407
|
+
} else if (unit === 'y') {
|
|
408
|
+
targetYear = operator === 'sum' ? targetYear + amount : targetYear - amount;
|
|
409
|
+
}
|
|
410
|
+
const lastDayOfTargetMonth = new Date(targetYear, targetMonthIndex + 1, 0).getDate();
|
|
411
|
+
targetDay = Math.min(day, lastDayOfTargetMonth);
|
|
412
|
+
}
|
|
413
|
+
return new Date(Date.UTC(targetYear, targetMonthIndex, targetDay)).getTime();
|
|
414
|
+
};
|
|
415
|
+
|
|
279
416
|
// Operator key
|
|
280
417
|
const OPERATOR$j = Symbol('SUBTRACT');
|
|
281
418
|
const subtractWithExpectedDecimals = operateWithExpectedDecimals$1('subtract');
|
|
@@ -297,9 +434,15 @@ class Subtract extends Arithmetic {
|
|
|
297
434
|
if (operands.length < 2) {
|
|
298
435
|
throw new Error('subtract expression requires at least 2 operands');
|
|
299
436
|
}
|
|
437
|
+
dateArithmeticTypeCheck(...operands);
|
|
300
438
|
super('-', OPERATOR$j, operands);
|
|
301
439
|
}
|
|
302
440
|
operate(results) {
|
|
441
|
+
const dateCalculationResults = this.getDateCalculationResults(results);
|
|
442
|
+
if (dateCalculationResults) {
|
|
443
|
+
const [dateNumber, ...durations] = dateCalculationResults;
|
|
444
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, duration, 'subtract'), dateNumber));
|
|
445
|
+
}
|
|
303
446
|
const presentResults = this.getResultValues(results);
|
|
304
447
|
if (presentResults === false) {
|
|
305
448
|
return false;
|
|
@@ -329,9 +472,15 @@ class Sum extends Arithmetic {
|
|
|
329
472
|
if (operands.length < 2) {
|
|
330
473
|
throw new Error('sum expression requires at least 2 operands');
|
|
331
474
|
}
|
|
475
|
+
dateArithmeticTypeCheck(...operands);
|
|
332
476
|
super('+', OPERATOR$i, operands);
|
|
333
477
|
}
|
|
334
478
|
operate(results) {
|
|
479
|
+
const dateCalculationResults = this.getDateCalculationResults(results);
|
|
480
|
+
if (dateCalculationResults) {
|
|
481
|
+
const [dateNumber, ...durations] = dateCalculationResults;
|
|
482
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, duration, 'sum'), dateNumber));
|
|
483
|
+
}
|
|
335
484
|
const presentResults = this.getResultValues(results);
|
|
336
485
|
if (presentResults === false) {
|
|
337
486
|
return false;
|
|
@@ -370,6 +519,17 @@ class Operand {
|
|
|
370
519
|
* @return {string}
|
|
371
520
|
*/
|
|
372
521
|
function printValue(value) {
|
|
522
|
+
const dateDuration = toDateDuration(value);
|
|
523
|
+
if (dateDuration) {
|
|
524
|
+
switch (dateDuration.unit) {
|
|
525
|
+
case 'd':
|
|
526
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'days' : 'day'}"`;
|
|
527
|
+
case 'm':
|
|
528
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'months' : 'month'}"`;
|
|
529
|
+
case 'y':
|
|
530
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'years' : 'year'}"`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
373
533
|
if (isString(value)) {
|
|
374
534
|
return `"${value}"`;
|
|
375
535
|
}
|
|
@@ -424,47 +584,6 @@ class Value extends Operand {
|
|
|
424
584
|
}
|
|
425
585
|
}
|
|
426
586
|
|
|
427
|
-
/**
|
|
428
|
-
* Convert a value to number if possible, otherwise return undefined
|
|
429
|
-
* @param value value to be converted to number
|
|
430
|
-
*/
|
|
431
|
-
const toNumber = value => {
|
|
432
|
-
const isValueNumber = isNumber(value);
|
|
433
|
-
if (isValueNumber) {
|
|
434
|
-
return value;
|
|
435
|
-
} else if (isString(value)) {
|
|
436
|
-
if (value.match(/^\d+\.\d+$/)) {
|
|
437
|
-
return parseFloat(value);
|
|
438
|
-
} else if (value.match(/^0$|^[1-9]\d*$/)) {
|
|
439
|
-
return parseInt(value);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return undefined;
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Convert a value to string if possible, otherwise return undefined
|
|
447
|
-
* @param value value to be converted to string
|
|
448
|
-
*/
|
|
449
|
-
const toString = value => {
|
|
450
|
-
if (isNumber(value)) {
|
|
451
|
-
return `${value}`;
|
|
452
|
-
} else if (isString(value)) {
|
|
453
|
-
return value;
|
|
454
|
-
}
|
|
455
|
-
return undefined;
|
|
456
|
-
};
|
|
457
|
-
/**
|
|
458
|
-
* Convert a value to number if it's type is string, otherwise return NaN
|
|
459
|
-
* @param value value to be converted to number
|
|
460
|
-
*/
|
|
461
|
-
const toDateNumber = value => {
|
|
462
|
-
if (isString(value)) {
|
|
463
|
-
return Date.parse(value);
|
|
464
|
-
}
|
|
465
|
-
return NaN;
|
|
466
|
-
};
|
|
467
|
-
|
|
468
587
|
const keyWithArrayIndexRegex$1 = /^(?<currentKey>[^[\]]+?)(?<indexes>(?:\[\d+])+)?$/;
|
|
469
588
|
const arrayIndexRegex$1 = /\[(\d+)]/g;
|
|
470
589
|
function parseBacktickWrappedKey$1(key) {
|
|
@@ -2718,6 +2837,10 @@ function relationalCompare$1(left, right, op) {
|
|
|
2718
2837
|
}
|
|
2719
2838
|
return false;
|
|
2720
2839
|
}
|
|
2840
|
+
function dateArithmeticReduce$1(values, op) {
|
|
2841
|
+
const [date, ...durations] = values;
|
|
2842
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
2843
|
+
}
|
|
2721
2844
|
function arithmeticReduce$1(values, op) {
|
|
2722
2845
|
if (op === OP_SUM) {
|
|
2723
2846
|
return values.reduce(addDecimals$1);
|
|
@@ -3140,22 +3263,37 @@ function interpret(compiled, ctx) {
|
|
|
3140
3263
|
stack$1[++stackTop$1] = false;
|
|
3141
3264
|
break;
|
|
3142
3265
|
}
|
|
3266
|
+
const isDateArithmetic = !isNaN(toDateNumber(a));
|
|
3143
3267
|
if (!isNumber(a) || !isNumber(b)) {
|
|
3144
|
-
|
|
3268
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
3269
|
+
if (isDateArithmetic) {
|
|
3270
|
+
const duration = toDateDuration(b);
|
|
3271
|
+
if (!duration) {
|
|
3272
|
+
throw new Error(`arithmetic operand is not a date duration: ${b}`);
|
|
3273
|
+
}
|
|
3274
|
+
} else {
|
|
3275
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3276
|
+
}
|
|
3277
|
+
} else {
|
|
3278
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3279
|
+
}
|
|
3145
3280
|
}
|
|
3146
3281
|
if (op === OP_SUM) {
|
|
3147
|
-
stack$1[++stackTop$1] = addDecimals$1(a, b);
|
|
3282
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? addDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'sum'));
|
|
3148
3283
|
} else if (op === OP_SUBTRACT) {
|
|
3149
|
-
stack$1[++stackTop$1] = subtractDecimals$1(a, b);
|
|
3150
|
-
} else if (
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3284
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? subtractDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'subtract'));
|
|
3285
|
+
} else if (isNumber(a) && isNumber(b)) {
|
|
3286
|
+
if (op === OP_MULTIPLY) {
|
|
3287
|
+
stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
|
|
3288
|
+
} else {
|
|
3289
|
+
stack$1[++stackTop$1] = divideDecimals$1(a, b);
|
|
3290
|
+
}
|
|
3154
3291
|
}
|
|
3155
3292
|
break;
|
|
3156
3293
|
}
|
|
3157
3294
|
const values = new Array(n);
|
|
3158
3295
|
let hasNull = false;
|
|
3296
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[stackTop$1 - n + 1])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3159
3297
|
for (let j = n - 1; j >= 0; j--) {
|
|
3160
3298
|
const v = stack$1[stackTop$1--];
|
|
3161
3299
|
if (v === null || v === undefined) {
|
|
@@ -3163,11 +3301,20 @@ function interpret(compiled, ctx) {
|
|
|
3163
3301
|
break;
|
|
3164
3302
|
}
|
|
3165
3303
|
if (!isNumber(v)) {
|
|
3166
|
-
|
|
3304
|
+
if (isDateArithmetic) {
|
|
3305
|
+
const duration = toDateDuration(v);
|
|
3306
|
+
if (!duration && j > 0) {
|
|
3307
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
3308
|
+
}
|
|
3309
|
+
} else {
|
|
3310
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (isNumber(v) || isString(v)) {
|
|
3314
|
+
values[j] = v;
|
|
3167
3315
|
}
|
|
3168
|
-
values[j] = v;
|
|
3169
3316
|
}
|
|
3170
|
-
stack$1[++stackTop$1] = hasNull ? false : arithmeticReduce$1(values, op);
|
|
3317
|
+
stack$1[++stackTop$1] = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce$1(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce$1(values, op) : false;
|
|
3171
3318
|
break;
|
|
3172
3319
|
}
|
|
3173
3320
|
|
|
@@ -3543,6 +3690,10 @@ function arithmeticReduce(values, op) {
|
|
|
3543
3690
|
}
|
|
3544
3691
|
return values.reduce(divideDecimals);
|
|
3545
3692
|
}
|
|
3693
|
+
function dateArithmeticReduce(values, op) {
|
|
3694
|
+
const [date, ...durations] = values;
|
|
3695
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
3696
|
+
}
|
|
3546
3697
|
function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
3547
3698
|
const {
|
|
3548
3699
|
bytecode,
|
|
@@ -4193,31 +4344,45 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4193
4344
|
stack[++stackTop] = false;
|
|
4194
4345
|
break;
|
|
4195
4346
|
}
|
|
4347
|
+
const isDateArithmetic = !isNaN(toDateNumber(aVal));
|
|
4196
4348
|
if (!isNumber(aVal) || !isNumber(bVal)) {
|
|
4197
|
-
|
|
4349
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
4350
|
+
if (isDateArithmetic) {
|
|
4351
|
+
const duration = toDateDuration(bVal);
|
|
4352
|
+
if (!duration) {
|
|
4353
|
+
throw new Error(`arithmetic operand is not a date duration: ${bVal}`);
|
|
4354
|
+
}
|
|
4355
|
+
} else {
|
|
4356
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4357
|
+
}
|
|
4358
|
+
} else {
|
|
4359
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4360
|
+
}
|
|
4198
4361
|
}
|
|
4199
4362
|
// After the throw above, aVal and bVal are narrowed to number
|
|
4200
4363
|
if (op === OP_SUM) {
|
|
4201
|
-
stack[++stackTop] = addDecimals(aVal, bVal);
|
|
4364
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? addDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'sum'));
|
|
4202
4365
|
} else if (op === OP_SUBTRACT) {
|
|
4203
|
-
stack[++stackTop] = subtractDecimals(aVal, bVal);
|
|
4204
|
-
} else if (
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
// Division — match OOP isInfinite guard: use a marker so
|
|
4208
|
-
// comparisons can decide whether to preserve the expression.
|
|
4209
|
-
const result = divideDecimals(aVal, bVal);
|
|
4210
|
-
if (isUnusableResult(result)) {
|
|
4211
|
-
// Push marker with original operands for reconstruction
|
|
4212
|
-
const marker = {
|
|
4213
|
-
_r: 4,
|
|
4214
|
-
_val: result,
|
|
4215
|
-
left: aVal,
|
|
4216
|
-
right: bVal
|
|
4217
|
-
};
|
|
4218
|
-
stack[++stackTop] = marker;
|
|
4366
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? subtractDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'subtract'));
|
|
4367
|
+
} else if (isNumber(aVal) && isNumber(bVal)) {
|
|
4368
|
+
if (op === OP_MULTIPLY) {
|
|
4369
|
+
stack[++stackTop] = multiplyDecimals(aVal, bVal);
|
|
4219
4370
|
} else {
|
|
4220
|
-
|
|
4371
|
+
// Division — match OOP isInfinite guard: use a marker so
|
|
4372
|
+
// comparisons can decide whether to preserve the expression.
|
|
4373
|
+
const result = divideDecimals(aVal, bVal);
|
|
4374
|
+
if (isUnusableResult(result)) {
|
|
4375
|
+
// Push marker with original operands for reconstruction
|
|
4376
|
+
const marker = {
|
|
4377
|
+
_r: 4,
|
|
4378
|
+
_val: result,
|
|
4379
|
+
left: aVal,
|
|
4380
|
+
right: bVal
|
|
4381
|
+
};
|
|
4382
|
+
stack[++stackTop] = marker;
|
|
4383
|
+
} else {
|
|
4384
|
+
stack[++stackTop] = result;
|
|
4385
|
+
}
|
|
4221
4386
|
}
|
|
4222
4387
|
}
|
|
4223
4388
|
break;
|
|
@@ -4225,6 +4390,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4225
4390
|
// N-operand path
|
|
4226
4391
|
const values = new Array(n);
|
|
4227
4392
|
let hasNull = false;
|
|
4393
|
+
const firstOperand = slotVal(stack[stackTop - n + 1]);
|
|
4394
|
+
const isDateArithmetic = typeof firstOperand !== 'object' && !isNaN(toDateNumber(firstOperand)) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
4228
4395
|
for (let j = n - 1; j >= 0; j--) {
|
|
4229
4396
|
const v = slotVal(stack[stackTop--]);
|
|
4230
4397
|
if (v === null || v === undefined) {
|
|
@@ -4232,15 +4399,24 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4232
4399
|
break;
|
|
4233
4400
|
}
|
|
4234
4401
|
if (!isNumber(v)) {
|
|
4235
|
-
|
|
4402
|
+
if (isDateArithmetic) {
|
|
4403
|
+
const duration = toDateDuration(v);
|
|
4404
|
+
if (!duration && j > 0) {
|
|
4405
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
4406
|
+
}
|
|
4407
|
+
} else {
|
|
4408
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
4409
|
+
}
|
|
4236
4410
|
}
|
|
4237
4411
|
// After the throw above, v is narrowed to number
|
|
4238
|
-
|
|
4412
|
+
if (isNumber(v) || isString(v)) {
|
|
4413
|
+
values[j] = v;
|
|
4414
|
+
}
|
|
4239
4415
|
}
|
|
4240
|
-
const reduced = hasNull ? false : arithmeticReduce(values, op);
|
|
4416
|
+
const reduced = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce(values, op) : false;
|
|
4241
4417
|
if (hasNull) {
|
|
4242
4418
|
stack[++stackTop] = false;
|
|
4243
|
-
} else if (op === OP_DIVIDE && reduced !== false && isUnusableResult(reduced)) {
|
|
4419
|
+
} else if (op === OP_DIVIDE && reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4244
4420
|
// N-operand division producing Infinity/NaN — create marker
|
|
4245
4421
|
const result = reduced;
|
|
4246
4422
|
const marker = {
|
|
@@ -4250,7 +4426,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4250
4426
|
right: values[1]
|
|
4251
4427
|
};
|
|
4252
4428
|
stack[++stackTop] = marker;
|
|
4253
|
-
} else if (reduced !== false && isUnusableResult(reduced)) {
|
|
4429
|
+
} else if (reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4254
4430
|
// Other arithmetic producing Infinity/NaN — preserve expression
|
|
4255
4431
|
stack[++stackTop] = makeResidual([opNames[op], ...values]);
|
|
4256
4432
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@briza/illogical",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A micro conditional javascript engine used to parse the raw logical and comparison expressions, evaluate the expression in the given data context, and provide access to a text form of the given expressions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/illogical.cjs",
|
|
@@ -33,26 +33,26 @@
|
|
|
33
33
|
"prepublishOnly": "npm run test && npm run build",
|
|
34
34
|
"check-licenses": "license-checker --summary --excludePrivatePackages --onlyAllow \"MIT;MIT OR X11;Apache-2.0;ISC;BSD-3-Clause;BSD-2-Clause;CC-BY-4.0;Public Domain;BSD;CC-BY-3.0;CC0-1.0;Python-2.0;BlueOak-1.0.0;Unlicense\"",
|
|
35
35
|
"bench": "npm run build && node --import tsx src/benchmark/evaluate.ts --cases conditions/sample-conditions --out benchmark/results-sample-evaluate-oop.json && node --import tsx src/benchmark/evaluate.ts --cases conditions/sample-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-sample-evaluate-bytecode.json && node --import tsx src/benchmark/evaluate.ts --cases conditions/synthetic-conditions --out benchmark/results-synthetic-evaluate-oop.json && node --import tsx src/benchmark/evaluate.ts --cases conditions/synthetic-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-synthetic-evaluate-bytecode.json && node --import tsx src/benchmark/simplify.ts --cases conditions/sample-conditions --out benchmark/results-sample-simplify-oop.json && node --import tsx src/benchmark/simplify.ts --cases conditions/sample-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-sample-simplify-bytecode.json && node --import tsx src/benchmark/simplify.ts --cases conditions/synthetic-conditions --out benchmark/results-synthetic-simplify-oop.json && node --import tsx src/benchmark/simplify.ts --cases conditions/synthetic-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-synthetic-simplify-bytecode.json && node --import tsx src/benchmark/report.ts benchmark/results-sample-evaluate-oop.json benchmark/results-sample-evaluate-bytecode.json --op evaluate --out benchmark/report-sample-evaluate.md && node --import tsx src/benchmark/report.ts benchmark/results-synthetic-evaluate-oop.json benchmark/results-synthetic-evaluate-bytecode.json --op evaluate --out benchmark/report-synthetic-evaluate.md && node --import tsx src/benchmark/report.ts benchmark/results-sample-simplify-oop.json benchmark/results-sample-simplify-bytecode.json --op simplify --out benchmark/report-sample-simplify.md && node --import tsx src/benchmark/report.ts benchmark/results-synthetic-simplify-oop.json benchmark/results-synthetic-simplify-bytecode.json --op simplify --out benchmark/report-synthetic-simplify.md",
|
|
36
|
-
"bench:sample:oop:evaluate":
|
|
37
|
-
"bench:sample:oop:simplify":
|
|
38
|
-
"bench:synthetic:oop:evaluate":
|
|
39
|
-
"bench:synthetic:oop:simplify":
|
|
40
|
-
"bench:sample:bytecode:evaluate":
|
|
41
|
-
"bench:
|
|
42
|
-
"bench:
|
|
43
|
-
"bench:synthetic:bytecode:simplify":
|
|
44
|
-
"bench:sample:compare:evaluate":
|
|
45
|
-
"bench:synthetic:compare:evaluate":
|
|
46
|
-
"bench:sample:compare:simplify":
|
|
47
|
-
"bench:synthetic:compare:simplify":
|
|
48
|
-
"bench:sample:report:evaluate":
|
|
49
|
-
"bench:synthetic:report:evaluate":
|
|
50
|
-
"bench:sample:report:full:evaluate":
|
|
51
|
-
"bench:synthetic:report:full:evaluate":
|
|
52
|
-
"bench:sample:report:simplify":
|
|
53
|
-
"bench:synthetic:report:simplify":
|
|
54
|
-
"bench:sample:report:full:simplify":
|
|
55
|
-
"bench:synthetic:report:full:simplify":
|
|
36
|
+
"bench:sample:oop:evaluate": "npm run build && node --import tsx src/benchmark/evaluate.ts --cases conditions/sample-conditions --out benchmark/results-sample-evaluate-oop.json",
|
|
37
|
+
"bench:sample:oop:simplify": "npm run build && node --import tsx src/benchmark/simplify.ts --cases conditions/sample-conditions --out benchmark/results-sample-simplify-oop.json",
|
|
38
|
+
"bench:synthetic:oop:evaluate": "npm run build && node --import tsx src/benchmark/evaluate.ts --cases conditions/synthetic-conditions --out benchmark/results-synthetic-evaluate-oop.json",
|
|
39
|
+
"bench:synthetic:oop:simplify": "npm run build && node --import tsx src/benchmark/simplify.ts --cases conditions/synthetic-conditions --out benchmark/results-synthetic-simplify-oop.json",
|
|
40
|
+
"bench:sample:bytecode:evaluate": "npm run build && node --import tsx src/benchmark/evaluate.ts --cases conditions/sample-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-sample-evaluate-bytecode.json",
|
|
41
|
+
"bench:sample:bytecode:simplify": "npm run build && node --import tsx src/benchmark/simplify.ts --cases conditions/sample-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-sample-simplify-bytecode.json",
|
|
42
|
+
"bench:synthetic:bytecode:evaluate": "npm run build && node --import tsx src/benchmark/evaluate.ts --cases conditions/synthetic-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-synthetic-evaluate-bytecode.json",
|
|
43
|
+
"bench:synthetic:bytecode:simplify": "npm run build && node --import tsx src/benchmark/simplify.ts --cases conditions/synthetic-conditions --options '{\"evaluator\":\"bytecode\"}' --out benchmark/results-synthetic-simplify-bytecode.json",
|
|
44
|
+
"bench:sample:compare:evaluate": "node --import tsx src/benchmark/compare.ts benchmark/results-sample-evaluate-oop.json benchmark/results-sample-evaluate-bytecode.json",
|
|
45
|
+
"bench:synthetic:compare:evaluate": "node --import tsx src/benchmark/compare.ts benchmark/results-synthetic-evaluate-oop.json benchmark/results-synthetic-evaluate-bytecode.json",
|
|
46
|
+
"bench:sample:compare:simplify": "node --import tsx src/benchmark/compare.ts benchmark/results-sample-simplify-oop.json benchmark/results-sample-simplify-bytecode.json",
|
|
47
|
+
"bench:synthetic:compare:simplify": "node --import tsx src/benchmark/compare.ts benchmark/results-synthetic-simplify-oop.json benchmark/results-synthetic-simplify-bytecode.json",
|
|
48
|
+
"bench:sample:report:evaluate": "node --import tsx src/benchmark/report.ts benchmark/results-sample-evaluate-oop.json benchmark/results-sample-evaluate-bytecode.json --op evaluate --out benchmark/report-sample-evaluate.md",
|
|
49
|
+
"bench:synthetic:report:evaluate": "node --import tsx src/benchmark/report.ts benchmark/results-synthetic-evaluate-oop.json benchmark/results-synthetic-evaluate-bytecode.json --op evaluate --out benchmark/report-synthetic-evaluate.md",
|
|
50
|
+
"bench:sample:report:full:evaluate": "node --import tsx src/benchmark/report.ts benchmark/results-sample-evaluate-oop.json benchmark/results-sample-evaluate-bytecode.json --op evaluate --full --out benchmark/report-sample-evaluate-full.md",
|
|
51
|
+
"bench:synthetic:report:full:evaluate": "node --import tsx src/benchmark/report.ts benchmark/results-synthetic-evaluate-oop.json benchmark/results-synthetic-evaluate-bytecode.json --op evaluate --full --out benchmark/report-synthetic-evaluate-full.md",
|
|
52
|
+
"bench:sample:report:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-sample-simplify-oop.json benchmark/results-sample-simplify-bytecode.json --op simplify --out benchmark/report-sample-simplify.md",
|
|
53
|
+
"bench:synthetic:report:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-synthetic-simplify-oop.json benchmark/results-synthetic-simplify-bytecode.json --op simplify --out benchmark/report-synthetic-simplify.md",
|
|
54
|
+
"bench:sample:report:full:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-sample-simplify-oop.json benchmark/results-sample-simplify-bytecode.json --op simplify --full --out benchmark/report-sample-simplify-full.md",
|
|
55
|
+
"bench:synthetic:report:full:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-synthetic-simplify-oop.json benchmark/results-synthetic-simplify-bytecode.json --op simplify --full --out benchmark/report-synthetic-simplify-full.md",
|
|
56
56
|
"get-bytecode": "node --import tsx src/bytecode/get-bytecode.ts",
|
|
57
57
|
"debug-bytecode": "node --import tsx src/tools/debugger.ts"
|
|
58
58
|
},
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Operand } from '../operand/index.js';
|
|
2
|
+
import { Reference } from '../operand/reference.js';
|
|
3
|
+
import { Value } from '../operand/value.js';
|
|
1
4
|
import { Evaluable, Result } from './evaluable.js';
|
|
2
5
|
/**
|
|
3
6
|
* Is number predicate.
|
|
@@ -45,3 +48,5 @@ export declare function areAllResults(values: (Result | Evaluable)[]): values is
|
|
|
45
48
|
export declare function areAllNumbers(results: Result[]): results is number[];
|
|
46
49
|
export declare function isUndefined(value: unknown): value is undefined;
|
|
47
50
|
export declare function isNull(value: unknown): value is null;
|
|
51
|
+
export declare function isReference(operand: Operand): operand is Reference;
|
|
52
|
+
export declare function isValue(operand: Operand): operand is Value;
|
package/types/common/util.d.ts
CHANGED
|
@@ -14,3 +14,9 @@ export declare const toString: (value: Result) => string | undefined;
|
|
|
14
14
|
* @param value value to be converted to number
|
|
15
15
|
*/
|
|
16
16
|
export declare const toDateNumber: (value: Result) => number;
|
|
17
|
+
export declare const formatDateNumber: (dateNumber: number) => string;
|
|
18
|
+
export type DateDuration = {
|
|
19
|
+
amount: number;
|
|
20
|
+
unit: 'd' | 'm' | 'y';
|
|
21
|
+
};
|
|
22
|
+
export declare const toDateDuration: (value: Result) => DateDuration | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context, Evaluable, EvaluableType, Result, SimplifyArgs } from '../../common/evaluable.js';
|
|
2
|
+
import { DateDuration } from '../../common/util.js';
|
|
2
3
|
import { Operand } from '../../operand/index.js';
|
|
3
4
|
import { ExpressionInput } from '../../parser/index.js';
|
|
4
5
|
import { Options } from '../../parser/options.js';
|
|
@@ -27,6 +28,7 @@ export declare abstract class Arithmetic implements Evaluable {
|
|
|
27
28
|
* array of numbers
|
|
28
29
|
*/
|
|
29
30
|
protected getResultValues(results: Result[]): number[] | false;
|
|
31
|
+
protected getDateCalculationResults(results: Result[]): [number, ...DateDuration[]] | false;
|
|
30
32
|
/**
|
|
31
33
|
* Performs the arithmetic operation on the operands evaluated values.
|
|
32
34
|
* @param {Result[]} results Operand result values.
|