@briza/illogical 2.1.0 → 2.2.1
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 +283 -78
- package/lib/illogical.esm.js +283 -78
- package/package.json +21 -21
- package/readme.md +28 -16
- package/types/bytecode/refs.d.ts +1 -0
- 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) {
|
|
@@ -1955,6 +2074,36 @@ function resolveCompactRef(ref, ctx) {
|
|
|
1955
2074
|
}
|
|
1956
2075
|
return resolveTokens(ref.tokens ?? [], ref.t, ctx);
|
|
1957
2076
|
}
|
|
2077
|
+
function getKeyFromCompactRef(ref) {
|
|
2078
|
+
if (typeof ref === 'string') {
|
|
2079
|
+
return ref;
|
|
2080
|
+
}
|
|
2081
|
+
if (Array.isArray(ref)) {
|
|
2082
|
+
return ref.map(ref => ref.includes('.') ? `\`${ref}\`` : ref).join('.');
|
|
2083
|
+
}
|
|
2084
|
+
let tokens = ref.tokens;
|
|
2085
|
+
if (ref.d) {
|
|
2086
|
+
let current = ref.k;
|
|
2087
|
+
let match = dynamicKeyRegex.exec(current);
|
|
2088
|
+
while (match) {
|
|
2089
|
+
current = current.replace(dynamicKeyRegex, '').replace('[]', '');
|
|
2090
|
+
match = dynamicKeyRegex.exec(current);
|
|
2091
|
+
}
|
|
2092
|
+
tokens = parseStaticKey(current);
|
|
2093
|
+
}
|
|
2094
|
+
let key = '';
|
|
2095
|
+
for (const token of tokens ?? []) {
|
|
2096
|
+
if (isNumber(token.value)) {
|
|
2097
|
+
return key;
|
|
2098
|
+
}
|
|
2099
|
+
if (token.value.includes('.')) {
|
|
2100
|
+
key += `${key ? '.' : ''}\`${token.value}\``;
|
|
2101
|
+
} else {
|
|
2102
|
+
key += `${key ? '.' : ''}${token.value}`;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return key;
|
|
2106
|
+
}
|
|
1958
2107
|
|
|
1959
2108
|
/**
|
|
1960
2109
|
* Bytecode compiler.
|
|
@@ -2722,6 +2871,10 @@ function relationalCompare$1(left, right, op) {
|
|
|
2722
2871
|
}
|
|
2723
2872
|
return false;
|
|
2724
2873
|
}
|
|
2874
|
+
function dateArithmeticReduce$1(values, op) {
|
|
2875
|
+
const [date, ...durations] = values;
|
|
2876
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
2877
|
+
}
|
|
2725
2878
|
function arithmeticReduce$1(values, op) {
|
|
2726
2879
|
if (op === OP_SUM) {
|
|
2727
2880
|
return values.reduce(addDecimals$1);
|
|
@@ -3144,22 +3297,37 @@ function interpret(compiled, ctx) {
|
|
|
3144
3297
|
stack$1[++stackTop$1] = false;
|
|
3145
3298
|
break;
|
|
3146
3299
|
}
|
|
3300
|
+
const isDateArithmetic = !isNaN(toDateNumber(a));
|
|
3147
3301
|
if (!isNumber(a) || !isNumber(b)) {
|
|
3148
|
-
|
|
3302
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
3303
|
+
if (isDateArithmetic) {
|
|
3304
|
+
const duration = toDateDuration(b);
|
|
3305
|
+
if (!duration) {
|
|
3306
|
+
throw new Error(`arithmetic operand is not a date duration: ${b}`);
|
|
3307
|
+
}
|
|
3308
|
+
} else {
|
|
3309
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3310
|
+
}
|
|
3311
|
+
} else {
|
|
3312
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3313
|
+
}
|
|
3149
3314
|
}
|
|
3150
3315
|
if (op === OP_SUM) {
|
|
3151
|
-
stack$1[++stackTop$1] = addDecimals$1(a, b);
|
|
3316
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? addDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'sum'));
|
|
3152
3317
|
} else if (op === OP_SUBTRACT) {
|
|
3153
|
-
stack$1[++stackTop$1] = subtractDecimals$1(a, b);
|
|
3154
|
-
} else if (
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3318
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? subtractDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'subtract'));
|
|
3319
|
+
} else if (isNumber(a) && isNumber(b)) {
|
|
3320
|
+
if (op === OP_MULTIPLY) {
|
|
3321
|
+
stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
|
|
3322
|
+
} else {
|
|
3323
|
+
stack$1[++stackTop$1] = divideDecimals$1(a, b);
|
|
3324
|
+
}
|
|
3158
3325
|
}
|
|
3159
3326
|
break;
|
|
3160
3327
|
}
|
|
3161
3328
|
const values = new Array(n);
|
|
3162
3329
|
let hasNull = false;
|
|
3330
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[stackTop$1 - n + 1])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3163
3331
|
for (let j = n - 1; j >= 0; j--) {
|
|
3164
3332
|
const v = stack$1[stackTop$1--];
|
|
3165
3333
|
if (v === null || v === undefined) {
|
|
@@ -3167,11 +3335,20 @@ function interpret(compiled, ctx) {
|
|
|
3167
3335
|
break;
|
|
3168
3336
|
}
|
|
3169
3337
|
if (!isNumber(v)) {
|
|
3170
|
-
|
|
3338
|
+
if (isDateArithmetic) {
|
|
3339
|
+
const duration = toDateDuration(v);
|
|
3340
|
+
if (!duration && j > 0) {
|
|
3341
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
3342
|
+
}
|
|
3343
|
+
} else {
|
|
3344
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
if (isNumber(v) || isString(v)) {
|
|
3348
|
+
values[j] = v;
|
|
3171
3349
|
}
|
|
3172
|
-
values[j] = v;
|
|
3173
3350
|
}
|
|
3174
|
-
stack$1[++stackTop$1] = hasNull ? false : arithmeticReduce$1(values, op);
|
|
3351
|
+
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
3352
|
break;
|
|
3176
3353
|
}
|
|
3177
3354
|
|
|
@@ -3547,6 +3724,10 @@ function arithmeticReduce(values, op) {
|
|
|
3547
3724
|
}
|
|
3548
3725
|
return values.reduce(divideDecimals);
|
|
3549
3726
|
}
|
|
3727
|
+
function dateArithmeticReduce(values, op) {
|
|
3728
|
+
const [date, ...durations] = values;
|
|
3729
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
3730
|
+
}
|
|
3550
3731
|
function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
3551
3732
|
const {
|
|
3552
3733
|
bytecode,
|
|
@@ -3606,7 +3787,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
3606
3787
|
case OP_PUSH_REF_DYNAMIC:
|
|
3607
3788
|
{
|
|
3608
3789
|
const idx = numAt(bytecode[i++]);
|
|
3609
|
-
const rawKey = refRawKeys[idx];
|
|
3610
3790
|
let val;
|
|
3611
3791
|
if (resolvedRefDirty[idx]) {
|
|
3612
3792
|
val = resolvedRefCache[idx];
|
|
@@ -3625,12 +3805,12 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
3625
3805
|
// Ref is absent from context.
|
|
3626
3806
|
// Check if it should be treated as a concrete undefined (evaluated as undefined)
|
|
3627
3807
|
// or as a residual expression (preserved for later simplification).
|
|
3628
|
-
if (strictSet?.has(
|
|
3808
|
+
if (strictSet?.has(getKeyFromCompactRef(refs[idx]))) {
|
|
3629
3809
|
// strictKeys: force-evaluate as undefined (not a residual)
|
|
3630
3810
|
stack[++stackTop] = undefined;
|
|
3631
3811
|
break;
|
|
3632
3812
|
}
|
|
3633
|
-
if (optionalSet && !optionalSet.has(
|
|
3813
|
+
if (optionalSet && !optionalSet.has(getKeyFromCompactRef(refs[idx]))) {
|
|
3634
3814
|
// Key not in optionalKeys: treat as definitely-present but absent → undefined
|
|
3635
3815
|
stack[++stackTop] = undefined;
|
|
3636
3816
|
break;
|
|
@@ -4197,31 +4377,45 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4197
4377
|
stack[++stackTop] = false;
|
|
4198
4378
|
break;
|
|
4199
4379
|
}
|
|
4380
|
+
const isDateArithmetic = !isNaN(toDateNumber(aVal));
|
|
4200
4381
|
if (!isNumber(aVal) || !isNumber(bVal)) {
|
|
4201
|
-
|
|
4382
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
4383
|
+
if (isDateArithmetic) {
|
|
4384
|
+
const duration = toDateDuration(bVal);
|
|
4385
|
+
if (!duration) {
|
|
4386
|
+
throw new Error(`arithmetic operand is not a date duration: ${bVal}`);
|
|
4387
|
+
}
|
|
4388
|
+
} else {
|
|
4389
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4390
|
+
}
|
|
4391
|
+
} else {
|
|
4392
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4393
|
+
}
|
|
4202
4394
|
}
|
|
4203
4395
|
// After the throw above, aVal and bVal are narrowed to number
|
|
4204
4396
|
if (op === OP_SUM) {
|
|
4205
|
-
stack[++stackTop] = addDecimals(aVal, bVal);
|
|
4397
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? addDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'sum'));
|
|
4206
4398
|
} 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;
|
|
4399
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? subtractDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'subtract'));
|
|
4400
|
+
} else if (isNumber(aVal) && isNumber(bVal)) {
|
|
4401
|
+
if (op === OP_MULTIPLY) {
|
|
4402
|
+
stack[++stackTop] = multiplyDecimals(aVal, bVal);
|
|
4223
4403
|
} else {
|
|
4224
|
-
|
|
4404
|
+
// Division — match OOP isInfinite guard: use a marker so
|
|
4405
|
+
// comparisons can decide whether to preserve the expression.
|
|
4406
|
+
const result = divideDecimals(aVal, bVal);
|
|
4407
|
+
if (isUnusableResult(result)) {
|
|
4408
|
+
// Push marker with original operands for reconstruction
|
|
4409
|
+
const marker = {
|
|
4410
|
+
_r: 4,
|
|
4411
|
+
_val: result,
|
|
4412
|
+
left: aVal,
|
|
4413
|
+
right: bVal
|
|
4414
|
+
};
|
|
4415
|
+
stack[++stackTop] = marker;
|
|
4416
|
+
} else {
|
|
4417
|
+
stack[++stackTop] = result;
|
|
4418
|
+
}
|
|
4225
4419
|
}
|
|
4226
4420
|
}
|
|
4227
4421
|
break;
|
|
@@ -4229,6 +4423,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4229
4423
|
// N-operand path
|
|
4230
4424
|
const values = new Array(n);
|
|
4231
4425
|
let hasNull = false;
|
|
4426
|
+
const firstOperand = slotVal(stack[stackTop - n + 1]);
|
|
4427
|
+
const isDateArithmetic = typeof firstOperand !== 'object' && !isNaN(toDateNumber(firstOperand)) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
4232
4428
|
for (let j = n - 1; j >= 0; j--) {
|
|
4233
4429
|
const v = slotVal(stack[stackTop--]);
|
|
4234
4430
|
if (v === null || v === undefined) {
|
|
@@ -4236,15 +4432,24 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4236
4432
|
break;
|
|
4237
4433
|
}
|
|
4238
4434
|
if (!isNumber(v)) {
|
|
4239
|
-
|
|
4435
|
+
if (isDateArithmetic) {
|
|
4436
|
+
const duration = toDateDuration(v);
|
|
4437
|
+
if (!duration && j > 0) {
|
|
4438
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
4439
|
+
}
|
|
4440
|
+
} else {
|
|
4441
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
4442
|
+
}
|
|
4240
4443
|
}
|
|
4241
4444
|
// After the throw above, v is narrowed to number
|
|
4242
|
-
|
|
4445
|
+
if (isNumber(v) || isString(v)) {
|
|
4446
|
+
values[j] = v;
|
|
4447
|
+
}
|
|
4243
4448
|
}
|
|
4244
|
-
const reduced = hasNull ? false : arithmeticReduce(values, op);
|
|
4449
|
+
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
4450
|
if (hasNull) {
|
|
4246
4451
|
stack[++stackTop] = false;
|
|
4247
|
-
} else if (op === OP_DIVIDE && reduced !== false && isUnusableResult(reduced)) {
|
|
4452
|
+
} else if (op === OP_DIVIDE && reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4248
4453
|
// N-operand division producing Infinity/NaN — create marker
|
|
4249
4454
|
const result = reduced;
|
|
4250
4455
|
const marker = {
|
|
@@ -4254,7 +4459,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4254
4459
|
right: values[1]
|
|
4255
4460
|
};
|
|
4256
4461
|
stack[++stackTop] = marker;
|
|
4257
|
-
} else if (reduced !== false && isUnusableResult(reduced)) {
|
|
4462
|
+
} else if (reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4258
4463
|
// Other arithmetic producing Infinity/NaN — preserve expression
|
|
4259
4464
|
stack[++stackTop] = makeResidual([opNames[op], ...values]);
|
|
4260
4465
|
} 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) {
|
|
@@ -1951,6 +2070,36 @@ function resolveCompactRef(ref, ctx) {
|
|
|
1951
2070
|
}
|
|
1952
2071
|
return resolveTokens(ref.tokens ?? [], ref.t, ctx);
|
|
1953
2072
|
}
|
|
2073
|
+
function getKeyFromCompactRef(ref) {
|
|
2074
|
+
if (typeof ref === 'string') {
|
|
2075
|
+
return ref;
|
|
2076
|
+
}
|
|
2077
|
+
if (Array.isArray(ref)) {
|
|
2078
|
+
return ref.map(ref => ref.includes('.') ? `\`${ref}\`` : ref).join('.');
|
|
2079
|
+
}
|
|
2080
|
+
let tokens = ref.tokens;
|
|
2081
|
+
if (ref.d) {
|
|
2082
|
+
let current = ref.k;
|
|
2083
|
+
let match = dynamicKeyRegex.exec(current);
|
|
2084
|
+
while (match) {
|
|
2085
|
+
current = current.replace(dynamicKeyRegex, '').replace('[]', '');
|
|
2086
|
+
match = dynamicKeyRegex.exec(current);
|
|
2087
|
+
}
|
|
2088
|
+
tokens = parseStaticKey(current);
|
|
2089
|
+
}
|
|
2090
|
+
let key = '';
|
|
2091
|
+
for (const token of tokens ?? []) {
|
|
2092
|
+
if (isNumber(token.value)) {
|
|
2093
|
+
return key;
|
|
2094
|
+
}
|
|
2095
|
+
if (token.value.includes('.')) {
|
|
2096
|
+
key += `${key ? '.' : ''}\`${token.value}\``;
|
|
2097
|
+
} else {
|
|
2098
|
+
key += `${key ? '.' : ''}${token.value}`;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return key;
|
|
2102
|
+
}
|
|
1954
2103
|
|
|
1955
2104
|
/**
|
|
1956
2105
|
* Bytecode compiler.
|
|
@@ -2718,6 +2867,10 @@ function relationalCompare$1(left, right, op) {
|
|
|
2718
2867
|
}
|
|
2719
2868
|
return false;
|
|
2720
2869
|
}
|
|
2870
|
+
function dateArithmeticReduce$1(values, op) {
|
|
2871
|
+
const [date, ...durations] = values;
|
|
2872
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
2873
|
+
}
|
|
2721
2874
|
function arithmeticReduce$1(values, op) {
|
|
2722
2875
|
if (op === OP_SUM) {
|
|
2723
2876
|
return values.reduce(addDecimals$1);
|
|
@@ -3140,22 +3293,37 @@ function interpret(compiled, ctx) {
|
|
|
3140
3293
|
stack$1[++stackTop$1] = false;
|
|
3141
3294
|
break;
|
|
3142
3295
|
}
|
|
3296
|
+
const isDateArithmetic = !isNaN(toDateNumber(a));
|
|
3143
3297
|
if (!isNumber(a) || !isNumber(b)) {
|
|
3144
|
-
|
|
3298
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
3299
|
+
if (isDateArithmetic) {
|
|
3300
|
+
const duration = toDateDuration(b);
|
|
3301
|
+
if (!duration) {
|
|
3302
|
+
throw new Error(`arithmetic operand is not a date duration: ${b}`);
|
|
3303
|
+
}
|
|
3304
|
+
} else {
|
|
3305
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3306
|
+
}
|
|
3307
|
+
} else {
|
|
3308
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3309
|
+
}
|
|
3145
3310
|
}
|
|
3146
3311
|
if (op === OP_SUM) {
|
|
3147
|
-
stack$1[++stackTop$1] = addDecimals$1(a, b);
|
|
3312
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? addDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'sum'));
|
|
3148
3313
|
} else if (op === OP_SUBTRACT) {
|
|
3149
|
-
stack$1[++stackTop$1] = subtractDecimals$1(a, b);
|
|
3150
|
-
} else if (
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3314
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? subtractDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'subtract'));
|
|
3315
|
+
} else if (isNumber(a) && isNumber(b)) {
|
|
3316
|
+
if (op === OP_MULTIPLY) {
|
|
3317
|
+
stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
|
|
3318
|
+
} else {
|
|
3319
|
+
stack$1[++stackTop$1] = divideDecimals$1(a, b);
|
|
3320
|
+
}
|
|
3154
3321
|
}
|
|
3155
3322
|
break;
|
|
3156
3323
|
}
|
|
3157
3324
|
const values = new Array(n);
|
|
3158
3325
|
let hasNull = false;
|
|
3326
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[stackTop$1 - n + 1])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3159
3327
|
for (let j = n - 1; j >= 0; j--) {
|
|
3160
3328
|
const v = stack$1[stackTop$1--];
|
|
3161
3329
|
if (v === null || v === undefined) {
|
|
@@ -3163,11 +3331,20 @@ function interpret(compiled, ctx) {
|
|
|
3163
3331
|
break;
|
|
3164
3332
|
}
|
|
3165
3333
|
if (!isNumber(v)) {
|
|
3166
|
-
|
|
3334
|
+
if (isDateArithmetic) {
|
|
3335
|
+
const duration = toDateDuration(v);
|
|
3336
|
+
if (!duration && j > 0) {
|
|
3337
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
3338
|
+
}
|
|
3339
|
+
} else {
|
|
3340
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
if (isNumber(v) || isString(v)) {
|
|
3344
|
+
values[j] = v;
|
|
3167
3345
|
}
|
|
3168
|
-
values[j] = v;
|
|
3169
3346
|
}
|
|
3170
|
-
stack$1[++stackTop$1] = hasNull ? false : arithmeticReduce$1(values, op);
|
|
3347
|
+
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
3348
|
break;
|
|
3172
3349
|
}
|
|
3173
3350
|
|
|
@@ -3543,6 +3720,10 @@ function arithmeticReduce(values, op) {
|
|
|
3543
3720
|
}
|
|
3544
3721
|
return values.reduce(divideDecimals);
|
|
3545
3722
|
}
|
|
3723
|
+
function dateArithmeticReduce(values, op) {
|
|
3724
|
+
const [date, ...durations] = values;
|
|
3725
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
3726
|
+
}
|
|
3546
3727
|
function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
3547
3728
|
const {
|
|
3548
3729
|
bytecode,
|
|
@@ -3602,7 +3783,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
3602
3783
|
case OP_PUSH_REF_DYNAMIC:
|
|
3603
3784
|
{
|
|
3604
3785
|
const idx = numAt(bytecode[i++]);
|
|
3605
|
-
const rawKey = refRawKeys[idx];
|
|
3606
3786
|
let val;
|
|
3607
3787
|
if (resolvedRefDirty[idx]) {
|
|
3608
3788
|
val = resolvedRefCache[idx];
|
|
@@ -3621,12 +3801,12 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
3621
3801
|
// Ref is absent from context.
|
|
3622
3802
|
// Check if it should be treated as a concrete undefined (evaluated as undefined)
|
|
3623
3803
|
// or as a residual expression (preserved for later simplification).
|
|
3624
|
-
if (strictSet?.has(
|
|
3804
|
+
if (strictSet?.has(getKeyFromCompactRef(refs[idx]))) {
|
|
3625
3805
|
// strictKeys: force-evaluate as undefined (not a residual)
|
|
3626
3806
|
stack[++stackTop] = undefined;
|
|
3627
3807
|
break;
|
|
3628
3808
|
}
|
|
3629
|
-
if (optionalSet && !optionalSet.has(
|
|
3809
|
+
if (optionalSet && !optionalSet.has(getKeyFromCompactRef(refs[idx]))) {
|
|
3630
3810
|
// Key not in optionalKeys: treat as definitely-present but absent → undefined
|
|
3631
3811
|
stack[++stackTop] = undefined;
|
|
3632
3812
|
break;
|
|
@@ -4193,31 +4373,45 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4193
4373
|
stack[++stackTop] = false;
|
|
4194
4374
|
break;
|
|
4195
4375
|
}
|
|
4376
|
+
const isDateArithmetic = !isNaN(toDateNumber(aVal));
|
|
4196
4377
|
if (!isNumber(aVal) || !isNumber(bVal)) {
|
|
4197
|
-
|
|
4378
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
4379
|
+
if (isDateArithmetic) {
|
|
4380
|
+
const duration = toDateDuration(bVal);
|
|
4381
|
+
if (!duration) {
|
|
4382
|
+
throw new Error(`arithmetic operand is not a date duration: ${bVal}`);
|
|
4383
|
+
}
|
|
4384
|
+
} else {
|
|
4385
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4386
|
+
}
|
|
4387
|
+
} else {
|
|
4388
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4389
|
+
}
|
|
4198
4390
|
}
|
|
4199
4391
|
// After the throw above, aVal and bVal are narrowed to number
|
|
4200
4392
|
if (op === OP_SUM) {
|
|
4201
|
-
stack[++stackTop] = addDecimals(aVal, bVal);
|
|
4393
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? addDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'sum'));
|
|
4202
4394
|
} 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;
|
|
4395
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? subtractDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'subtract'));
|
|
4396
|
+
} else if (isNumber(aVal) && isNumber(bVal)) {
|
|
4397
|
+
if (op === OP_MULTIPLY) {
|
|
4398
|
+
stack[++stackTop] = multiplyDecimals(aVal, bVal);
|
|
4219
4399
|
} else {
|
|
4220
|
-
|
|
4400
|
+
// Division — match OOP isInfinite guard: use a marker so
|
|
4401
|
+
// comparisons can decide whether to preserve the expression.
|
|
4402
|
+
const result = divideDecimals(aVal, bVal);
|
|
4403
|
+
if (isUnusableResult(result)) {
|
|
4404
|
+
// Push marker with original operands for reconstruction
|
|
4405
|
+
const marker = {
|
|
4406
|
+
_r: 4,
|
|
4407
|
+
_val: result,
|
|
4408
|
+
left: aVal,
|
|
4409
|
+
right: bVal
|
|
4410
|
+
};
|
|
4411
|
+
stack[++stackTop] = marker;
|
|
4412
|
+
} else {
|
|
4413
|
+
stack[++stackTop] = result;
|
|
4414
|
+
}
|
|
4221
4415
|
}
|
|
4222
4416
|
}
|
|
4223
4417
|
break;
|
|
@@ -4225,6 +4419,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4225
4419
|
// N-operand path
|
|
4226
4420
|
const values = new Array(n);
|
|
4227
4421
|
let hasNull = false;
|
|
4422
|
+
const firstOperand = slotVal(stack[stackTop - n + 1]);
|
|
4423
|
+
const isDateArithmetic = typeof firstOperand !== 'object' && !isNaN(toDateNumber(firstOperand)) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
4228
4424
|
for (let j = n - 1; j >= 0; j--) {
|
|
4229
4425
|
const v = slotVal(stack[stackTop--]);
|
|
4230
4426
|
if (v === null || v === undefined) {
|
|
@@ -4232,15 +4428,24 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4232
4428
|
break;
|
|
4233
4429
|
}
|
|
4234
4430
|
if (!isNumber(v)) {
|
|
4235
|
-
|
|
4431
|
+
if (isDateArithmetic) {
|
|
4432
|
+
const duration = toDateDuration(v);
|
|
4433
|
+
if (!duration && j > 0) {
|
|
4434
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
4435
|
+
}
|
|
4436
|
+
} else {
|
|
4437
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
4438
|
+
}
|
|
4236
4439
|
}
|
|
4237
4440
|
// After the throw above, v is narrowed to number
|
|
4238
|
-
|
|
4441
|
+
if (isNumber(v) || isString(v)) {
|
|
4442
|
+
values[j] = v;
|
|
4443
|
+
}
|
|
4239
4444
|
}
|
|
4240
|
-
const reduced = hasNull ? false : arithmeticReduce(values, op);
|
|
4445
|
+
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
4446
|
if (hasNull) {
|
|
4242
4447
|
stack[++stackTop] = false;
|
|
4243
|
-
} else if (op === OP_DIVIDE && reduced !== false && isUnusableResult(reduced)) {
|
|
4448
|
+
} else if (op === OP_DIVIDE && reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4244
4449
|
// N-operand division producing Infinity/NaN — create marker
|
|
4245
4450
|
const result = reduced;
|
|
4246
4451
|
const marker = {
|
|
@@ -4250,7 +4455,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4250
4455
|
right: values[1]
|
|
4251
4456
|
};
|
|
4252
4457
|
stack[++stackTop] = marker;
|
|
4253
|
-
} else if (reduced !== false && isUnusableResult(reduced)) {
|
|
4458
|
+
} else if (reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4254
4459
|
// Other arithmetic producing Infinity/NaN — preserve expression
|
|
4255
4460
|
stack[++stackTop] = makeResidual([opNames[op], ...values]);
|
|
4256
4461
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@briza/illogical",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
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
|
},
|
package/readme.md
CHANGED
|
@@ -8,33 +8,37 @@
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
<h3 align="center">illogical</h3>
|
|
10
10
|
</div>
|
|
11
|
+
</div>
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-

|
|
17
|
-
|
|
13
|
+
<div align="center">
|
|
14
|
+
<p>
|
|
15
|
+
**illogical** is a JSON DSL (domain-specific language) for expressing and evaluating business rules in the insurance industry. Underwriters use illogical to model business rules for their question sets, enabling distributors to render great user experiences.
|
|
16
|
+
</p>
|
|
18
17
|
</div>
|
|
19
18
|
|
|
20
19
|
<div align="center">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
[](https://github.com/briza-insurance/illogical/actions?branch=master)
|
|
21
|
+
[](https://badge.fury.io/js/@briza%2Fillogical)
|
|
22
|
+
[](https://packagephobia.com/result?p=@briza/illogical)
|
|
23
|
+

|
|
24
|
+

|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
## 🚀 Getting Started
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Get up and running with illogical in just a few steps.
|
|
32
|
+
|
|
33
|
+
### Installation
|
|
32
34
|
|
|
33
35
|
```sh
|
|
34
36
|
# install illogical
|
|
35
37
|
npm install @briza/illogical
|
|
36
38
|
```
|
|
37
39
|
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
38
42
|
```js
|
|
39
43
|
// Import the illogical engine
|
|
40
44
|
import Engine from '@briza/illogical'
|
|
@@ -65,9 +69,11 @@ engine.evaluate(['==', '$age.(String)', '21'], ctx) // true
|
|
|
65
69
|
engine.evaluate(['AND', ['>', '$age', 20], ['==', '$name', 'peter']]) // true
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
##
|
|
72
|
+
## 📚 Documentation
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
### Core Concepts
|
|
75
|
+
|
|
76
|
+
Explore the supported expressions and their usage:
|
|
71
77
|
|
|
72
78
|
- [Comparison Expressions](./specs/comparison-expressions.md)
|
|
73
79
|
- [Logical Expressions](./specs/logical-expressions.md)
|
|
@@ -75,19 +81,25 @@ Understand supported expressions:
|
|
|
75
81
|
- [Evaluation Data Context](./specs/evaluation-data-context.md)
|
|
76
82
|
- [Operand Types](./specs/operand-types.md)
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
### API Reference
|
|
85
|
+
|
|
86
|
+
Learn how to use the engine and its methods:
|
|
79
87
|
|
|
80
88
|
- [Evaluate](./specs/evaluate.md)
|
|
81
89
|
- [Statement](./specs/statement.md)
|
|
82
90
|
- [Parse](./specs/parse.md)
|
|
83
91
|
- [Simplify](./specs/simplify.md)
|
|
84
92
|
|
|
93
|
+
### Customization
|
|
94
|
+
|
|
85
95
|
Customize the engine and the documentation:
|
|
86
96
|
|
|
87
97
|
- [Engine Options](./specs/engine.md)
|
|
88
98
|
- [Code Documentation](https://briza-insurance.github.io/illogical/index.html)
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
### Development Tools
|
|
101
|
+
|
|
102
|
+
For advanced usage like bytecode evaluation and debugging:
|
|
91
103
|
|
|
92
104
|
- [Bytecode Evaluator](./specs/bytecode-evaluator.md)
|
|
93
105
|
- [Debugger Tools](./specs/debugger-tools.md)
|
package/types/bytecode/refs.d.ts
CHANGED
|
@@ -71,3 +71,4 @@ export declare function resolveDynamic(key: string, dataType: DataType | undefin
|
|
|
71
71
|
* (OP_OVERLAP_SCAN_REFS_CONST, OP_OR_AND_IN_CONST_2).
|
|
72
72
|
*/
|
|
73
73
|
export declare function resolveCompactRef(ref: CompactRef, ctx: Context): Result;
|
|
74
|
+
export declare function getKeyFromCompactRef(ref: CompactRef): string;
|
|
@@ -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.
|