@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 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
- throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
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 (op === OP_MULTIPLY) {
3155
- stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
3156
- } else {
3157
- stack$1[++stackTop$1] = divideDecimals$1(a, b);
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
- throw new Error(`arithmetic operand is not a number: ${v}`);
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(rawKey)) {
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(rawKey)) {
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
- throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
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 (op === OP_MULTIPLY) {
4209
- stack[++stackTop] = multiplyDecimals(aVal, bVal);
4210
- } else {
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
- stack[++stackTop] = result;
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
- throw new Error(`arithmetic operand is not a number: ${v}`);
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
- values[j] = v;
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 {
@@ -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
- throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
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 (op === OP_MULTIPLY) {
3151
- stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
3152
- } else {
3153
- stack$1[++stackTop$1] = divideDecimals$1(a, b);
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
- throw new Error(`arithmetic operand is not a number: ${v}`);
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(rawKey)) {
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(rawKey)) {
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
- throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
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 (op === OP_MULTIPLY) {
4205
- stack[++stackTop] = multiplyDecimals(aVal, bVal);
4206
- } else {
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
- stack[++stackTop] = result;
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
- throw new Error(`arithmetic operand is not a number: ${v}`);
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
- values[j] = v;
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.0",
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": "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: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",
42
- "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",
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",
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
- [![build status](https://github.com/briza-insurance/illogical/actions/workflows/test.yml/badge.svg)](https://github.com/briza-insurance/illogical/actions?branch=master)
13
- [![npm version](https://badge.fury.io/js/@briza%2Fillogical.svg?icon=si%3Anpm)](https://badge.fury.io/js/@briza%2Fillogical)
14
- [![install size](https://packagephobia.com/badge?p=@briza/illogical)](https://packagephobia.com/result?p=@briza/illogical)
15
- ![zero dependencies](https://img.shields.io/badge/0-dependencies-green)
16
- ![npm downloads](https://img.shields.io/npm/dm/%40briza%2Fillogical)
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
- <p>
22
- <br />
23
- JSON DSL for expressing and evaluating business rules. Underwriters use illogical to model business rules for their question sets, enabling distributors to render great user experiences.
24
- </p>
20
+ [![build status](https://github.com/briza-insurance/illogical/actions/workflows/test.yml/badge.svg)](https://github.com/briza-insurance/illogical/actions?branch=master)
21
+ [![npm version](https://badge.fury.io/js/@briza%2Fillogical.svg?icon=si%3Anpm)](https://badge.fury.io/js/@briza%2Fillogical)
22
+ [![install size](https://packagephobia.com/badge?p=@briza/illogical)](https://packagephobia.com/result?p=@briza/illogical)
23
+ ![zero dependencies](https://img.shields.io/badge/0-dependencies-green)
24
+ ![npm downloads](https://img.shields.io/npm/dm/%40briza%2Fillogical)
25
25
  </div>
26
26
 
27
27
  ---
28
28
 
29
- This project is designed such that business rules can be shared between the front-end and back-end, serialized in JSON.
29
+ ## 🚀 Getting Started
30
30
 
31
- ## 🚀 Get Started
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
- ## 🖼️ Resources
72
+ ## 📚 Documentation
69
73
 
70
- Understand supported expressions:
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
- Learn about usages:
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
- Bytecode evaluation and debugging tools:
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)
@@ -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;
@@ -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;
@@ -0,0 +1,2 @@
1
+ import { Operand } from '../../operand/index.js';
2
+ export declare const dateArithmeticTypeCheck: (...operands: Operand[]) => void;
@@ -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.
@@ -0,0 +1,2 @@
1
+ import { DateDuration } from '../../common/util.js';
2
+ export declare const mutateDateWithDuration: (dateNumber: number, duration: DateDuration, operator: "sum" | "subtract") => number;