@cj-tech-master/excelts 9.3.1 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/browser/index.d.ts +1 -0
  2. package/dist/browser/index.js +2 -0
  3. package/dist/browser/modules/excel/cell.d.ts +18 -0
  4. package/dist/browser/modules/excel/cell.js +21 -0
  5. package/dist/browser/modules/excel/utils/cell-format.js +85 -13
  6. package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
  7. package/dist/browser/modules/excel/workbook.browser.js +49 -0
  8. package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
  9. package/dist/browser/modules/formula/compile/binder.js +48 -6
  10. package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
  11. package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
  12. package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
  13. package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
  14. package/dist/browser/modules/formula/functions/_shared.js +47 -0
  15. package/dist/browser/modules/formula/functions/conditional.js +103 -22
  16. package/dist/browser/modules/formula/functions/date.js +105 -23
  17. package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
  18. package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
  19. package/dist/browser/modules/formula/functions/engineering.js +103 -151
  20. package/dist/browser/modules/formula/functions/financial.js +210 -184
  21. package/dist/browser/modules/formula/functions/lookup.js +224 -157
  22. package/dist/browser/modules/formula/functions/math.d.ts +26 -0
  23. package/dist/browser/modules/formula/functions/math.js +249 -69
  24. package/dist/browser/modules/formula/functions/statistical.js +221 -171
  25. package/dist/browser/modules/formula/functions/text.js +112 -52
  26. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
  27. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
  28. package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
  29. package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
  30. package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
  31. package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
  32. package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
  33. package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
  34. package/dist/browser/modules/formula/runtime/values.js +20 -2
  35. package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
  36. package/dist/browser/modules/formula/syntax/ast.js +1 -0
  37. package/dist/browser/modules/formula/syntax/parser.js +29 -7
  38. package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
  39. package/dist/browser/modules/formula/syntax/token-types.js +9 -0
  40. package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
  41. package/dist/cjs/index.js +7 -2
  42. package/dist/cjs/modules/excel/cell.js +21 -0
  43. package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
  44. package/dist/cjs/modules/excel/workbook.browser.js +49 -0
  45. package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
  46. package/dist/cjs/modules/formula/compile/binder.js +48 -6
  47. package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
  48. package/dist/cjs/modules/formula/functions/_shared.js +48 -0
  49. package/dist/cjs/modules/formula/functions/conditional.js +103 -22
  50. package/dist/cjs/modules/formula/functions/date.js +104 -22
  51. package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
  52. package/dist/cjs/modules/formula/functions/engineering.js +109 -157
  53. package/dist/cjs/modules/formula/functions/financial.js +209 -183
  54. package/dist/cjs/modules/formula/functions/lookup.js +224 -157
  55. package/dist/cjs/modules/formula/functions/math.js +254 -70
  56. package/dist/cjs/modules/formula/functions/statistical.js +222 -172
  57. package/dist/cjs/modules/formula/functions/text.js +112 -52
  58. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
  59. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
  60. package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
  61. package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
  62. package/dist/cjs/modules/formula/runtime/values.js +21 -2
  63. package/dist/cjs/modules/formula/syntax/parser.js +29 -7
  64. package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
  65. package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
  66. package/dist/esm/index.js +2 -0
  67. package/dist/esm/modules/excel/cell.js +21 -0
  68. package/dist/esm/modules/excel/utils/cell-format.js +85 -13
  69. package/dist/esm/modules/excel/workbook.browser.js +49 -0
  70. package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
  71. package/dist/esm/modules/formula/compile/binder.js +48 -6
  72. package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
  73. package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
  74. package/dist/esm/modules/formula/functions/_shared.js +47 -0
  75. package/dist/esm/modules/formula/functions/conditional.js +103 -22
  76. package/dist/esm/modules/formula/functions/date.js +105 -23
  77. package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
  78. package/dist/esm/modules/formula/functions/engineering.js +103 -151
  79. package/dist/esm/modules/formula/functions/financial.js +210 -184
  80. package/dist/esm/modules/formula/functions/lookup.js +224 -157
  81. package/dist/esm/modules/formula/functions/math.js +249 -69
  82. package/dist/esm/modules/formula/functions/statistical.js +221 -171
  83. package/dist/esm/modules/formula/functions/text.js +112 -52
  84. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
  85. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
  86. package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
  87. package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
  88. package/dist/esm/modules/formula/runtime/values.js +20 -2
  89. package/dist/esm/modules/formula/syntax/ast.js +1 -0
  90. package/dist/esm/modules/formula/syntax/parser.js +29 -7
  91. package/dist/esm/modules/formula/syntax/token-types.js +9 -0
  92. package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
  93. package/dist/iife/excelts.iife.js +1502 -1379
  94. package/dist/iife/excelts.iife.js.map +1 -1
  95. package/dist/iife/excelts.iife.min.js +26 -26
  96. package/dist/types/index.d.ts +1 -0
  97. package/dist/types/modules/excel/cell.d.ts +18 -0
  98. package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
  99. package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
  100. package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
  101. package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
  102. package/dist/types/modules/formula/functions/math.d.ts +26 -0
  103. package/dist/types/modules/formula/materialize/types.d.ts +15 -0
  104. package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
  105. package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
  106. package/dist/types/modules/formula/runtime/values.d.ts +13 -0
  107. package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
  108. package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
  109. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  * Math / Aggregate Functions — Native RuntimeValue implementation.
3
3
  */
4
4
  import { RVKind, ERRORS, rvNumber, rvString, rvArray, toNumberRV, toStringRV, topLeft, isError, isArray } from "../runtime/values.js";
5
- import { argToNumber, flattenAll, flattenNumbers, firstError } from "./_shared.js";
5
+ import { argToNumber, flattenAll, flattenNumbers, firstError, forEachNumber } from "./_shared.js";
6
6
  // ============================================================================
7
7
  // Internal Helpers
8
8
  // ============================================================================
@@ -250,76 +250,98 @@ export const fnACOTH = args => {
250
250
  // Math / Aggregate Functions
251
251
  // ============================================================================
252
252
  export const fnSUM = args => {
253
- const nums = flattenNumbers(args);
254
- const err = firstError(nums);
253
+ let sum = 0;
254
+ const err = forEachNumber(args, n => {
255
+ sum += n;
256
+ });
255
257
  if (err) {
256
258
  return err;
257
259
  }
258
- let sum = 0;
259
- for (const n of nums) {
260
- sum += n.value;
261
- }
262
260
  // Fail fast on overflow to Infinity; otherwise the result leaks into
263
261
  // any formula that aggregates it (AVERAGE, STDEV, etc.) and those
264
262
  // downstream callers would then fan #NUM! out across the graph.
265
263
  return Number.isFinite(sum) ? rvNumber(sum) : ERRORS.NUM;
266
264
  };
267
265
  export const fnAVERAGE = args => {
268
- const nums = flattenNumbers(args);
269
- const err = firstError(nums);
266
+ let sum = 0;
267
+ let count = 0;
268
+ const err = forEachNumber(args, n => {
269
+ sum += n;
270
+ count++;
271
+ });
270
272
  if (err) {
271
273
  return err;
272
274
  }
273
- if (nums.length === 0) {
275
+ if (count === 0) {
274
276
  return ERRORS.DIV0;
275
277
  }
276
- let sum = 0;
277
- for (const n of nums) {
278
- sum += n.value;
279
- }
280
- const avg = sum / nums.length;
278
+ const avg = sum / count;
281
279
  return Number.isFinite(avg) ? rvNumber(avg) : ERRORS.NUM;
282
280
  };
283
281
  export const fnMIN = args => {
284
- const nums = flattenNumbers(args);
285
- const err = firstError(nums);
286
- if (err) {
287
- return err;
288
- }
289
- if (nums.length === 0) {
290
- return rvNumber(0);
291
- }
292
282
  let min = Infinity;
293
- for (const n of nums) {
294
- if (n.value < min) {
295
- min = n.value;
283
+ let seen = false;
284
+ const err = forEachNumber(args, n => {
285
+ seen = true;
286
+ if (n < min) {
287
+ min = n;
296
288
  }
297
- }
298
- return rvNumber(min);
299
- };
300
- export const fnMAX = args => {
301
- const nums = flattenNumbers(args);
302
- const err = firstError(nums);
289
+ });
303
290
  if (err) {
304
291
  return err;
305
292
  }
306
- if (nums.length === 0) {
307
- return rvNumber(0);
308
- }
293
+ return seen ? rvNumber(min) : rvNumber(0);
294
+ };
295
+ export const fnMAX = args => {
309
296
  let max = -Infinity;
310
- for (const n of nums) {
311
- if (n.value > max) {
312
- max = n.value;
297
+ let seen = false;
298
+ const err = forEachNumber(args, n => {
299
+ seen = true;
300
+ if (n > max) {
301
+ max = n;
313
302
  }
303
+ });
304
+ if (err) {
305
+ return err;
314
306
  }
315
- return rvNumber(max);
307
+ return seen ? rvNumber(max) : rvNumber(0);
316
308
  };
317
309
  export const fnCOUNT = args => {
310
+ // Excel's rules — intentionally asymmetric:
311
+ // - Number cell / scalar → counted (direct + inside array).
312
+ // - Numeric-string DIRECT scalar (`COUNT("5")`) → counted. Inside
313
+ // an array, numeric strings are NOT counted.
314
+ // - Boolean / non-numeric string / blank / error → NOT counted in
315
+ // any context (diverges from COUNTA which counts booleans).
316
+ // Previously the engine flattened everything through `topLeft` and
317
+ // only accepted `Number` — which meant `COUNT("5")` returned 0.
318
318
  let count = 0;
319
- const all = flattenAll(args);
320
- for (const v of all) {
321
- if (v.kind === RVKind.Number) {
322
- count++;
319
+ for (const arg of args) {
320
+ if (arg.kind === RVKind.Array) {
321
+ for (const row of arg.rows) {
322
+ for (const cell of row) {
323
+ if (cell.kind === RVKind.Number) {
324
+ count++;
325
+ }
326
+ }
327
+ }
328
+ }
329
+ else {
330
+ const s = topLeft(arg);
331
+ if (s.kind === RVKind.Number) {
332
+ count++;
333
+ }
334
+ else if (s.kind === RVKind.String) {
335
+ // Numeric strings (including `"5"`, `"3.14"`, `"1e3"`) are
336
+ // counted when supplied as direct scalars. Non-numeric text
337
+ // is silently skipped. Route through `toNumberRV` so the same
338
+ // parser that `VALUE()` uses decides.
339
+ const nr = toNumberRV(s);
340
+ if (nr.kind === RVKind.Number) {
341
+ count++;
342
+ }
343
+ }
344
+ // Blank, Boolean, Error, Reference, Lambda → skipped.
323
345
  }
324
346
  }
325
347
  return rvNumber(count);
@@ -328,8 +350,12 @@ export const fnCOUNTA = args => {
328
350
  let count = 0;
329
351
  const all = flattenAll(args);
330
352
  for (const v of all) {
331
- // Count everything that is not blank and not empty string
332
- if (v.kind !== RVKind.Blank && !(v.kind === RVKind.String && v.value === "")) {
353
+ // Excel: COUNTA counts every non-blank cell, INCLUDING cells that
354
+ // hold an empty string (e.g. a formula that returned `=""`). Only
355
+ // the fully-blank kind is excluded. Previously we also excluded
356
+ // empty strings — that matched Google Sheets but diverged from
357
+ // Excel's documented behaviour.
358
+ if (v.kind !== RVKind.Blank) {
333
359
  count++;
334
360
  }
335
361
  }
@@ -339,6 +365,11 @@ export const fnCOUNTBLANK = args => {
339
365
  let count = 0;
340
366
  const all = flattenAll(args);
341
367
  for (const v of all) {
368
+ // Excel: COUNTBLANK counts both truly-blank cells and cells
369
+ // containing an empty string result (e.g. `=""`). This is
370
+ // intentionally asymmetric with COUNTA — COUNTA+COUNTBLANK can
371
+ // legitimately exceed the total cell count when `=""` formulas
372
+ // are present, matching Excel's documented behaviour.
342
373
  if (v.kind === RVKind.Blank || (v.kind === RVKind.String && v.value === "")) {
343
374
  count++;
344
375
  }
@@ -346,18 +377,18 @@ export const fnCOUNTBLANK = args => {
346
377
  return rvNumber(count);
347
378
  };
348
379
  export const fnPRODUCT = args => {
349
- const nums = flattenNumbers(args);
350
- const err = firstError(nums);
380
+ let product = 1;
381
+ let seen = false;
382
+ const err = forEachNumber(args, n => {
383
+ product *= n;
384
+ seen = true;
385
+ });
351
386
  if (err) {
352
387
  return err;
353
388
  }
354
- if (nums.length === 0) {
389
+ if (!seen) {
355
390
  return rvNumber(0);
356
391
  }
357
- let product = 1;
358
- for (const n of nums) {
359
- product *= n.value;
360
- }
361
392
  // Excel surfaces an overflow as #NUM! rather than letting Infinity
362
393
  // propagate into subsequent arithmetic.
363
394
  return isFinite(product) ? rvNumber(product) : ERRORS.NUM;
@@ -395,13 +426,23 @@ export const fnSUMPRODUCT = args => {
395
426
  }
396
427
  }
397
428
  }
429
+ // Hoist per-array broadcast classification and row-shape out of the
430
+ // hot (r, c) loop — the shape is constant across all cells, but the
431
+ // previous code re-inspected `arr.height === 1 && arr.width === 1`
432
+ // per cell (arrays.length × rows × cols times).
433
+ const arrayCount = arrays.length;
434
+ const broadcasts = new Array(arrayCount);
435
+ for (let i = 0; i < arrayCount; i++) {
436
+ const arr = arrays[i];
437
+ broadcasts[i] = arr.height === 1 && arr.width === 1;
438
+ }
398
439
  let sum = 0;
399
440
  for (let r = 0; r < rows; r++) {
400
441
  for (let c = 0; c < cols; c++) {
401
442
  let product = 1;
402
- for (const arr of arrays) {
403
- // Broadcast 1x1 arrays to the target cell position.
404
- const val = arr.height === 1 && arr.width === 1 ? arr.rows[0][0] : arr.rows[r][c];
443
+ for (let i = 0; i < arrayCount; i++) {
444
+ const arr = arrays[i];
445
+ const val = broadcasts[i] ? arr.rows[0][0] : arr.rows[r][c];
405
446
  if (val.kind === RVKind.Error) {
406
447
  return val;
407
448
  }
@@ -450,6 +491,66 @@ export const fnCEILING = args => {
450
491
  }
451
492
  return rvNumber(Math.ceil(num.value / sig) * sig);
452
493
  };
494
+ /**
495
+ * CEILING.MATH(number, [significance], [mode]) — rounds away from zero
496
+ * by default, or toward zero when `mode` is non-zero AND `number` is
497
+ * negative. Significance is always interpreted by absolute value.
498
+ *
499
+ * Different from CEILING: negative numbers with positive significance
500
+ * are valid (Excel does NOT require same sign), and there is an extra
501
+ * `mode` switch that flips the rounding direction for negatives.
502
+ */
503
+ export const fnCEILING_MATH = args => {
504
+ const num = argToNumber(args[0]);
505
+ if (isError(num)) {
506
+ return num;
507
+ }
508
+ // Blank `significance` → Excel default 1 (for positive num) or -1
509
+ // (for negative num); either way |sig| = 1. Use 1 explicitly since
510
+ // the algorithm below works on `Math.abs(sig)`.
511
+ const sigRV = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(1);
512
+ if (isError(sigRV)) {
513
+ return sigRV;
514
+ }
515
+ const sigAbs = Math.abs(sigRV.value);
516
+ if (sigAbs === 0) {
517
+ return rvNumber(0);
518
+ }
519
+ const modeRV = args.length > 2 && args[2].kind !== RVKind.Blank ? argToNumber(args[2]) : rvNumber(0);
520
+ if (isError(modeRV)) {
521
+ return modeRV;
522
+ }
523
+ // Round away from zero by default; toward zero when `mode` is non-zero
524
+ // AND the number is negative. For positive numbers `mode` has no effect.
525
+ if (num.value >= 0) {
526
+ return rvNumber(Math.ceil(num.value / sigAbs) * sigAbs);
527
+ }
528
+ if (modeRV.value !== 0) {
529
+ // Round away from zero (toward −∞) for negatives: use `Math.floor`.
530
+ return rvNumber(Math.floor(num.value / sigAbs) * sigAbs);
531
+ }
532
+ // Default: round toward zero for negatives.
533
+ return rvNumber(Math.ceil(num.value / sigAbs) * sigAbs);
534
+ };
535
+ /**
536
+ * CEILING.PRECISE / ISO.CEILING — always rounds toward +∞ (irrespective
537
+ * of sign), using the absolute value of significance.
538
+ */
539
+ export const fnCEILING_PRECISE = args => {
540
+ const num = argToNumber(args[0]);
541
+ if (isError(num)) {
542
+ return num;
543
+ }
544
+ const sigRV = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(1);
545
+ if (isError(sigRV)) {
546
+ return sigRV;
547
+ }
548
+ const sigAbs = Math.abs(sigRV.value);
549
+ if (sigAbs === 0) {
550
+ return rvNumber(0);
551
+ }
552
+ return rvNumber(Math.ceil(num.value / sigAbs) * sigAbs);
553
+ };
453
554
  export const fnFLOOR = args => {
454
555
  const num = argToNumber(args[0]);
455
556
  if (isError(num)) {
@@ -468,6 +569,58 @@ export const fnFLOOR = args => {
468
569
  }
469
570
  return rvNumber(Math.floor(num.value / sig) * sig);
470
571
  };
572
+ /**
573
+ * FLOOR.MATH(number, [significance], [mode]) — rounds toward zero by
574
+ * default, or away from zero when `mode` is non-zero AND `number` is
575
+ * negative. Uses `|significance|` so negative significance never
576
+ * produces #NUM!.
577
+ */
578
+ export const fnFLOOR_MATH = args => {
579
+ const num = argToNumber(args[0]);
580
+ if (isError(num)) {
581
+ return num;
582
+ }
583
+ const sigRV = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(1);
584
+ if (isError(sigRV)) {
585
+ return sigRV;
586
+ }
587
+ const sigAbs = Math.abs(sigRV.value);
588
+ if (sigAbs === 0) {
589
+ return rvNumber(0);
590
+ }
591
+ const modeRV = args.length > 2 && args[2].kind !== RVKind.Blank ? argToNumber(args[2]) : rvNumber(0);
592
+ if (isError(modeRV)) {
593
+ return modeRV;
594
+ }
595
+ // Positive numbers always round toward zero (down). Negative numbers
596
+ // default to rounding away from zero (down = further negative); `mode`
597
+ // non-zero flips to rounding toward zero.
598
+ if (num.value >= 0) {
599
+ return rvNumber(Math.floor(num.value / sigAbs) * sigAbs);
600
+ }
601
+ if (modeRV.value !== 0) {
602
+ return rvNumber(Math.ceil(num.value / sigAbs) * sigAbs);
603
+ }
604
+ return rvNumber(Math.floor(num.value / sigAbs) * sigAbs);
605
+ };
606
+ /**
607
+ * FLOOR.PRECISE — always rounds toward −∞ using `|significance|`.
608
+ */
609
+ export const fnFLOOR_PRECISE = args => {
610
+ const num = argToNumber(args[0]);
611
+ if (isError(num)) {
612
+ return num;
613
+ }
614
+ const sigRV = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(1);
615
+ if (isError(sigRV)) {
616
+ return sigRV;
617
+ }
618
+ const sigAbs = Math.abs(sigRV.value);
619
+ if (sigAbs === 0) {
620
+ return rvNumber(0);
621
+ }
622
+ return rvNumber(Math.floor(num.value / sigAbs) * sigAbs);
623
+ };
471
624
  export const fnINT = args => {
472
625
  const n = argToNumber(args[0]);
473
626
  return isError(n) ? n : rvNumber(Math.floor(n.value));
@@ -593,7 +746,11 @@ export const fnLOG = args => {
593
746
  if (n.value <= 0) {
594
747
  return ERRORS.NUM;
595
748
  }
596
- const baseRV = args.length > 1 ? argToNumber(args[1]) : rvNumber(10);
749
+ // Blank 2nd arg → default base 10. Without this guard, `LOG(100, )`
750
+ // coerces blank → 0 via argToNumber and falls into the `<= 0` branch,
751
+ // incorrectly returning #NUM!. Excel treats the omitted / blank slot
752
+ // as "use the default".
753
+ const baseRV = args.length > 1 && args[1].kind !== RVKind.Blank ? argToNumber(args[1]) : rvNumber(10);
597
754
  if (isError(baseRV)) {
598
755
  return baseRV;
599
756
  }
@@ -668,15 +825,13 @@ export const fnTRUNC = args => {
668
825
  return rvNumber(applyRounding(num.value, digitsRV.value, Math.trunc));
669
826
  };
670
827
  export const fnSUMSQ = args => {
671
- const nums = flattenNumbers(args);
672
- const err = firstError(nums);
828
+ let sum = 0;
829
+ const err = forEachNumber(args, n => {
830
+ sum += n * n;
831
+ });
673
832
  if (err) {
674
833
  return err;
675
834
  }
676
- let sum = 0;
677
- for (const n of nums) {
678
- sum += n.value ** 2;
679
- }
680
835
  return isFinite(sum) ? rvNumber(sum) : ERRORS.NUM;
681
836
  };
682
837
  export const fnGCD = args => {
@@ -827,12 +982,24 @@ export const fnBASE = args => {
827
982
  if (radix.value < 2 || radix.value > 36) {
828
983
  return ERRORS.NUM;
829
984
  }
985
+ // Excel's BASE requires `num` in `[0, 2^53)`. Negative inputs produce
986
+ // a `-` prefix via JS `.toString(radix)` — Excel rejects them. Very
987
+ // large inputs exceed the precise integer range and would corrupt the
988
+ // lower digits; Excel reports #NUM! there too.
989
+ if (num.value < 0 || num.value >= 2 ** 53) {
990
+ return ERRORS.NUM;
991
+ }
830
992
  const minLenRV = args.length > 2 ? argToNumber(args[2]) : rvNumber(0);
831
993
  if (isError(minLenRV)) {
832
994
  return minLenRV;
833
995
  }
996
+ // `minLen` must be in `[0, 255]`; Excel rejects larger widths.
997
+ const minLen = Math.trunc(minLenRV.value);
998
+ if (minLen < 0 || minLen > 255) {
999
+ return ERRORS.NUM;
1000
+ }
834
1001
  const result = Math.floor(num.value).toString(Math.floor(radix.value)).toUpperCase();
835
- return rvString(minLenRV.value > 0 ? result.padStart(minLenRV.value, "0") : result);
1002
+ return rvString(minLen > 0 ? result.padStart(minLen, "0") : result);
836
1003
  };
837
1004
  export const fnDECIMAL = args => {
838
1005
  const e = topLeft(args[0]);
@@ -898,10 +1065,10 @@ export const fnROMAN = args => {
898
1065
  if (isError(f)) {
899
1066
  return f;
900
1067
  }
901
- if (f.value === 1 && f.value === 1) {
902
- // Boolean inputs flow through argToNumber as 0/1 already.
903
- }
904
- form = Math.floor(f.value);
1068
+ // Excel accepts TRUE/FALSE and 0..4 for `form`. argToNumber already
1069
+ // coerces booleans to 0/1, so nothing extra is needed here — we
1070
+ // just truncate toward zero and bounds-check.
1071
+ form = Math.trunc(f.value);
905
1072
  if (form < 0 || form > 4) {
906
1073
  return ERRORS.VALUE;
907
1074
  }
@@ -1003,8 +1170,15 @@ function sumPairedArrays(args, combine) {
1003
1170
  if (a0.kind !== RVKind.Array || a1.kind !== RVKind.Array) {
1004
1171
  return ERRORS.VALUE;
1005
1172
  }
1006
- const h = Math.min(a0.height, a1.height);
1007
- const w = Math.min(a0.width, a1.width);
1173
+ // Excel requires matching shapes — `SUMX2MY2`/`SUMX2PY2`/`SUMXMY2`
1174
+ // return `#N/A` when the two arrays have different cell counts.
1175
+ // Previously we silently clamped to the min, producing a numeric
1176
+ // result that quietly dropped the tail of the longer array.
1177
+ if (a0.height !== a1.height || a0.width !== a1.width) {
1178
+ return ERRORS.NA;
1179
+ }
1180
+ const h = a0.height;
1181
+ const w = a0.width;
1008
1182
  let sum = 0;
1009
1183
  for (let r = 0; r < h; r++) {
1010
1184
  for (let c = 0; c < w; c++) {
@@ -1166,6 +1340,12 @@ export const fnPERMUT = args => {
1166
1340
  let result = 1;
1167
1341
  for (let i = 0; i < ki; i++) {
1168
1342
  result *= ni - i;
1343
+ // Excel surfaces an overflow as #NUM! rather than silently persisting
1344
+ // Infinity. Without this guard `PERMUT(200, 50)` returns Infinity
1345
+ // which then poisons every formula that references the cell.
1346
+ if (!Number.isFinite(result)) {
1347
+ return ERRORS.NUM;
1348
+ }
1169
1349
  }
1170
1350
  return rvNumber(result);
1171
1351
  };