@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
@@ -4,7 +4,7 @@
4
4
  * Native RuntimeValue implementations.
5
5
  */
6
6
  import { RVKind, ERRORS, rvNumber, rvArray, toNumberRV, toBooleanRV, topLeft, isError } from "../runtime/values.js";
7
- import { argToNumber, flattenAll, flattenNumbers, firstError } from "./_shared.js";
7
+ import { argToNumber, flattenAll, flattenNumbers, firstError, forEachNumber } from "./_shared.js";
8
8
  /**
9
9
  * Extract a boolean from a single arg. Returns the boolean or ErrorValue.
10
10
  */
@@ -74,16 +74,14 @@ function quickselect(arr, k) {
74
74
  return arr[k];
75
75
  }
76
76
  export const fnMEDIAN = args => {
77
- const nums = flattenNumbers(args);
78
- const err = firstError(nums);
79
- if (err) {
80
- return err;
77
+ const values = toNumberArray(args);
78
+ if (!Array.isArray(values)) {
79
+ return values;
81
80
  }
82
- if (nums.length === 0) {
81
+ const n = values.length;
82
+ if (n === 0) {
83
83
  return ERRORS.NUM;
84
84
  }
85
- const values = nums.map(n => n.value);
86
- const n = values.length;
87
85
  const mid = n >> 1;
88
86
  if (n % 2 !== 0) {
89
87
  return rvNumber(quickselect(values, mid));
@@ -114,6 +112,12 @@ export const fnLARGE = args => {
114
112
  // with the same shape where each cell holds LARGE at that k.
115
113
  if (args[1].kind === RVKind.Array) {
116
114
  const kArr = args[1];
115
+ // Sort once when the array contains more than a single cell — k is
116
+ // O(1) afterwards. quickselect per-cell would allocate a fresh
117
+ // `values.slice()` for every k, which quickly becomes quadratic
118
+ // when LARGE(A1:A100, {1;2;3;…}) evaluates over long ranges.
119
+ const totalCells = kArr.height * kArr.width;
120
+ const sortedDesc = totalCells > 1 ? values.slice().sort((a, b) => b - a) : null;
117
121
  const outRows = [];
118
122
  for (const row of kArr.rows) {
119
123
  const outRow = [];
@@ -132,10 +136,13 @@ export const fnLARGE = args => {
132
136
  outRow.push(ERRORS.NUM);
133
137
  continue;
134
138
  }
135
- // k-th largest == (n − k)-th smallest. Use a copy — quickselect
136
- // mutates the array it works on, and we need a fresh view for
137
- // every cell in the output.
138
- outRow.push(rvNumber(quickselect(values.slice(), values.length - kInt)));
139
+ if (sortedDesc) {
140
+ outRow.push(rvNumber(sortedDesc[kInt - 1]));
141
+ }
142
+ else {
143
+ // Single-cell k — quickselect is cheaper than a full sort.
144
+ outRow.push(rvNumber(quickselect(values.slice(), values.length - kInt)));
145
+ }
139
146
  }
140
147
  outRows.push(outRow);
141
148
  }
@@ -160,6 +167,10 @@ export const fnSMALL = args => {
160
167
  const values = nums.map(n => n.value);
161
168
  if (args[1].kind === RVKind.Array) {
162
169
  const kArr = args[1];
170
+ // Multi-cell k → sort once ascending, index in O(1). See LARGE for
171
+ // the same optimisation and rationale.
172
+ const totalCells = kArr.height * kArr.width;
173
+ const sortedAsc = totalCells > 1 ? values.slice().sort((a, b) => a - b) : null;
163
174
  const outRows = [];
164
175
  for (const row of kArr.rows) {
165
176
  const outRow = [];
@@ -178,7 +189,12 @@ export const fnSMALL = args => {
178
189
  outRow.push(ERRORS.NUM);
179
190
  continue;
180
191
  }
181
- outRow.push(rvNumber(quickselect(values.slice(), kInt - 1)));
192
+ if (sortedAsc) {
193
+ outRow.push(rvNumber(sortedAsc[kInt - 1]));
194
+ }
195
+ else {
196
+ outRow.push(rvNumber(quickselect(values.slice(), kInt - 1)));
197
+ }
182
198
  }
183
199
  outRows.push(outRow);
184
200
  }
@@ -230,30 +246,39 @@ export const fnRANK = args => {
230
246
  * Returns `null` when there is no data at all (callers decide whether that
231
247
  * should be `#DIV/0!` or zero given the sample/population convention).
232
248
  */
249
+ /**
250
+ * Single-pass mean + sum-of-squared-deviations using Welford's online
251
+ * algorithm. Returns `null` when the array is empty.
252
+ *
253
+ * Welford's recurrence keeps the sum of squared deviations numerically
254
+ * stable (avoids the catastrophic cancellation that bites
255
+ * `Σx² - (Σx)²/n` for datasets with a large mean and small variance).
256
+ * Costs one pass rather than two — meaningful on multi-thousand-cell
257
+ * ranges feeding STDEV / VAR / their variants.
258
+ */
233
259
  function computeMeanAndSumSq(nums) {
234
260
  const n = nums.length;
235
261
  if (n === 0) {
236
262
  return null;
237
263
  }
238
- let sum = 0;
239
- for (const v of nums) {
240
- sum += v;
241
- }
242
- const mean = sum / n;
264
+ let mean = 0;
243
265
  let sumSq = 0;
244
- for (const v of nums) {
245
- sumSq += (v - mean) ** 2;
266
+ for (let i = 0; i < n; i++) {
267
+ const x = nums[i];
268
+ const delta = x - mean;
269
+ mean += delta / (i + 1);
270
+ // `x - mean` uses the *updated* mean — this product form is the
271
+ // identity that makes Welford's recurrence equal the second-pass
272
+ // `(x - final_mean)²` sum exactly.
273
+ sumSq += delta * (x - mean);
246
274
  }
247
275
  return { n, mean, sumSq };
248
276
  }
249
277
  /** Resolve {args} to `number[]` or an error. Shared by STDEV/VAR family. */
250
278
  function toNumberArray(args) {
251
- const rawNums = flattenNumbers(args);
252
- const err = firstError(rawNums);
253
- if (err) {
254
- return err;
255
- }
256
- return rawNums.map(n => n.value);
279
+ const out = [];
280
+ const err = forEachNumber(args, n => out.push(n));
281
+ return err ?? out;
257
282
  }
258
283
  export const fnSTDEV = args => {
259
284
  const nums = toNumberArray(args);
@@ -454,9 +479,16 @@ export const fnNORMINV = args => {
454
479
  // PERCENTILE, QUARTILE, MODE
455
480
  // ============================================================================
456
481
  export const fnPERCENTILE = args => {
457
- const nums = flattenNumbers([args[0]])
458
- .filter((v) => v.kind === RVKind.Number)
459
- .map(n => n.value);
482
+ // Propagate errors from within the range — `flattenNumbers` returns
483
+ // both numbers and errors, and previously we silently filtered errors
484
+ // out by `.kind === Number`, producing a bogus percentile on ranges
485
+ // that contained `#N/A`. Surface the error like AVERAGE / SUM do.
486
+ const raw = flattenNumbers([args[0]]);
487
+ const err = firstError(raw);
488
+ if (err) {
489
+ return err;
490
+ }
491
+ const nums = raw.map(n => n.value);
460
492
  const k = argToNumber(args[1]);
461
493
  if (k.kind === RVKind.Error) {
462
494
  return k;
@@ -476,9 +508,12 @@ export const fnPERCENTILE = args => {
476
508
  return rvNumber(nums[lower] + frac * (nums[upper] - nums[lower]));
477
509
  };
478
510
  export const fnPERCENTILEEXC = args => {
479
- const nums = flattenNumbers([args[0]])
480
- .filter((v) => v.kind === RVKind.Number)
481
- .map(n => n.value);
511
+ const raw = flattenNumbers([args[0]]);
512
+ const err = firstError(raw);
513
+ if (err) {
514
+ return err;
515
+ }
516
+ const nums = raw.map(n => n.value);
482
517
  const k = argToNumber(args[1]);
483
518
  if (k.kind === RVKind.Error) {
484
519
  return k;
@@ -588,20 +623,48 @@ export const fnMODE_MULT = args => {
588
623
  * Returns the shorter prefix-length pair aligned by position.
589
624
  */
590
625
  function pairedNumbers(args, aIdx, bIdx) {
591
- const flatA = flattenNumbers([args[aIdx]]);
592
- const errA = firstError(flatA);
593
- if (errA) {
594
- return errA;
595
- }
596
- const flatB = flattenNumbers([args[bIdx]]);
597
- const errB = firstError(flatB);
598
- if (errB) {
599
- return errB;
600
- }
601
- const xs = flatA.filter((v) => v.kind === RVKind.Number).map(n => n.value);
602
- const ys = flatB.filter((v) => v.kind === RVKind.Number).map(n => n.value);
603
- const n = Math.min(xs.length, ys.length);
604
- return { xs: xs.slice(0, n), ys: ys.slice(0, n) };
626
+ // Walk both ranges in lockstep so the x/y pairing preserves the
627
+ // source position. Excel's CORREL / SLOPE / INTERCEPT skip any pair
628
+ // where either side is non-numeric (text, blank, boolean) — they do
629
+ // NOT drop the non-numeric cells from one side and then pair by
630
+ // surviving position, which would realign unrelated values.
631
+ const aArg = args[aIdx];
632
+ const bArg = args[bIdx];
633
+ const aCells = [];
634
+ const bCells = [];
635
+ collectCells(aArg, aCells);
636
+ collectCells(bArg, bCells);
637
+ const xs = [];
638
+ const ys = [];
639
+ const n = Math.min(aCells.length, bCells.length);
640
+ for (let i = 0; i < n; i++) {
641
+ const a = aCells[i];
642
+ const b = bCells[i];
643
+ if (a.kind === RVKind.Error) {
644
+ return a;
645
+ }
646
+ if (b.kind === RVKind.Error) {
647
+ return b;
648
+ }
649
+ if (a.kind === RVKind.Number && b.kind === RVKind.Number) {
650
+ xs.push(a.value);
651
+ ys.push(b.value);
652
+ }
653
+ }
654
+ return { xs, ys };
655
+ }
656
+ /** Walk a runtime value and push every scalar cell (in row-major order). */
657
+ function collectCells(arg, out) {
658
+ if (arg.kind === RVKind.Array) {
659
+ for (const row of arg.rows) {
660
+ for (const cell of row) {
661
+ out.push(cell);
662
+ }
663
+ }
664
+ }
665
+ else if (arg.kind !== RVKind.Reference && arg.kind !== RVKind.Lambda) {
666
+ out.push(arg);
667
+ }
605
668
  }
606
669
  function pairedSums(xs, ys) {
607
670
  const n = xs.length;
@@ -735,42 +798,44 @@ export { fnFACT, fnFACTDOUBLE, fnCOMBIN, fnCOMBINA, fnPERMUT } from "./math.js";
735
798
  // GEOMEAN, HARMEAN, TRIMMEAN, DEVSQ, AVEDEV
736
799
  // ============================================================================
737
800
  export const fnGEOMEAN = args => {
738
- const rawNums = flattenNumbers(args);
739
- const err = firstError(rawNums);
801
+ let logSum = 0;
802
+ let count = 0;
803
+ let outOfRange = false;
804
+ const err = forEachNumber(args, n => {
805
+ if (n <= 0) {
806
+ outOfRange = true;
807
+ return;
808
+ }
809
+ logSum += Math.log(n);
810
+ count++;
811
+ });
740
812
  if (err) {
741
813
  return err;
742
814
  }
743
- const nums = rawNums;
744
- if (nums.length === 0) {
815
+ if (outOfRange || count === 0) {
745
816
  return ERRORS.NUM;
746
817
  }
747
- let logSum = 0;
748
- for (const n of nums) {
749
- if (n.value <= 0) {
750
- return ERRORS.NUM;
751
- }
752
- logSum += Math.log(n.value);
753
- }
754
- return rvNumber(Math.exp(logSum / nums.length));
818
+ return rvNumber(Math.exp(logSum / count));
755
819
  };
756
820
  export const fnHARMEAN = args => {
757
- const rawNums = flattenNumbers(args);
758
- const err = firstError(rawNums);
821
+ let recipSum = 0;
822
+ let count = 0;
823
+ let outOfRange = false;
824
+ const err = forEachNumber(args, n => {
825
+ if (n <= 0) {
826
+ outOfRange = true;
827
+ return;
828
+ }
829
+ recipSum += 1 / n;
830
+ count++;
831
+ });
759
832
  if (err) {
760
833
  return err;
761
834
  }
762
- const nums = rawNums;
763
- if (nums.length === 0) {
835
+ if (outOfRange || count === 0) {
764
836
  return ERRORS.NUM;
765
837
  }
766
- let recipSum = 0;
767
- for (const n of nums) {
768
- if (n.value <= 0) {
769
- return ERRORS.NUM;
770
- }
771
- recipSum += 1 / n.value;
772
- }
773
- return rvNumber(nums.length / recipSum);
838
+ return rvNumber(count / recipSum);
774
839
  };
775
840
  export const fnTRIMMEAN = args => {
776
841
  const all = flattenNumbers([args[0]]);
@@ -892,22 +957,27 @@ export const fnCONFIDENCE_T = args => {
892
957
  * side is non-numeric).
893
958
  */
894
959
  function pairedNumericValues(a, b) {
895
- const xsAll = flattenNumbers([a]);
896
- const xsErr = firstError(xsAll);
897
- if (xsErr) {
898
- return xsErr;
899
- }
900
- const ysAll = flattenNumbers([b]);
901
- const ysErr = firstError(ysAll);
902
- if (ysErr) {
903
- return ysErr;
904
- }
905
- const n = Math.min(xsAll.length, ysAll.length);
960
+ // Walk both ranges in lockstep so pair alignment survives non-numeric
961
+ // cells. Previously `flattenNumbers` dropped text / blanks before the
962
+ // zip, which silently shifted the rest of the pairs and produced a
963
+ // spurious covariance. Excel's COVARIANCE.P / .S pair cells by
964
+ // position and skip only the pairs where either side is non-numeric.
965
+ const aCells = [];
966
+ const bCells = [];
967
+ collectCells(a, aCells);
968
+ collectCells(b, bCells);
906
969
  const xs = [];
907
970
  const ys = [];
971
+ const n = Math.min(aCells.length, bCells.length);
908
972
  for (let i = 0; i < n; i++) {
909
- const x = xsAll[i];
910
- const y = ysAll[i];
973
+ const x = aCells[i];
974
+ const y = bCells[i];
975
+ if (x.kind === RVKind.Error) {
976
+ return x;
977
+ }
978
+ if (y.kind === RVKind.Error) {
979
+ return y;
980
+ }
911
981
  if (x.kind === RVKind.Number && y.kind === RVKind.Number) {
912
982
  xs.push(x.value);
913
983
  ys.push(y.value);
@@ -1048,38 +1118,24 @@ export const fnAVERAGEA = args => {
1048
1118
  }
1049
1119
  return count === 0 ? ERRORS.DIV0 : rvNumber(sum / count);
1050
1120
  };
1051
- export const fnMAXA = args => {
1052
- const all = flattenAll(args);
1053
- let max = -Infinity;
1121
+ export const fnMAXA = args => reduceAValue(args, -Infinity, (best, n) => (n > best ? n : best));
1122
+ export const fnMINA = args => reduceAValue(args, Infinity, (best, n) => (n < best ? n : best));
1123
+ /**
1124
+ * Shared MAXA / MINA reducer. Excel's `*A` variants differ from MAX / MIN
1125
+ * only in how they treat text and booleans inside ranges:
1126
+ * - Number → its value
1127
+ * - Boolean → 1 / 0
1128
+ * - String → 0 (NOT skipped like MAX / MIN)
1129
+ * - Blank → skipped
1130
+ * - Error → propagated
1131
+ *
1132
+ * When no non-blank cells are seen, both return 0 (the untouched
1133
+ * identity fallback matches Excel's historical behaviour).
1134
+ */
1135
+ function reduceAValue(args, identity, fold) {
1136
+ let best = identity;
1054
1137
  let found = false;
1055
- for (const v of all) {
1056
- if (v.kind === RVKind.Blank) {
1057
- continue;
1058
- }
1059
- if (v.kind === RVKind.Error) {
1060
- return v;
1061
- }
1062
- let n;
1063
- if (v.kind === RVKind.Number) {
1064
- n = v.value;
1065
- }
1066
- else if (v.kind === RVKind.Boolean) {
1067
- n = v.value ? 1 : 0;
1068
- }
1069
- else {
1070
- n = 0;
1071
- }
1072
- if (n > max) {
1073
- max = n;
1074
- }
1075
- found = true;
1076
- }
1077
- return rvNumber(found ? max : 0);
1078
- };
1079
- export const fnMINA = args => {
1080
1138
  const all = flattenAll(args);
1081
- let min = Infinity;
1082
- let found = false;
1083
1139
  for (const v of all) {
1084
1140
  if (v.kind === RVKind.Blank) {
1085
1141
  continue;
@@ -1087,23 +1143,12 @@ export const fnMINA = args => {
1087
1143
  if (v.kind === RVKind.Error) {
1088
1144
  return v;
1089
1145
  }
1090
- let n;
1091
- if (v.kind === RVKind.Number) {
1092
- n = v.value;
1093
- }
1094
- else if (v.kind === RVKind.Boolean) {
1095
- n = v.value ? 1 : 0;
1096
- }
1097
- else {
1098
- n = 0;
1099
- }
1100
- if (n < min) {
1101
- min = n;
1102
- }
1146
+ const n = v.kind === RVKind.Number ? v.value : v.kind === RVKind.Boolean ? (v.value ? 1 : 0) : 0; // text counts as 0 for MAXA / MINA.
1147
+ best = fold(best, n);
1103
1148
  found = true;
1104
1149
  }
1105
- return rvNumber(found ? min : 0);
1106
- };
1150
+ return rvNumber(found ? best : 0);
1151
+ }
1107
1152
  // ============================================================================
1108
1153
  // Private helpers for distributions (pure number → number, unchanged)
1109
1154
  // ============================================================================
@@ -2052,7 +2097,10 @@ export const fnERF = args => {
2052
2097
  if (lower.kind === RVKind.Error) {
2053
2098
  return lower;
2054
2099
  }
2055
- if (args.length > 1) {
2100
+ // Blank 2nd arg → behave like an omitted upper bound, i.e. return
2101
+ // `erf(lower)`. Previously a blank coerced to 0 and flipped the sign
2102
+ // of the result via `erf(0) − erf(lower)`.
2103
+ if (args.length > 1 && args[1].kind !== RVKind.Blank) {
2056
2104
  const upper = argToNumber(args[1]);
2057
2105
  if (upper.kind === RVKind.Error) {
2058
2106
  return upper;
@@ -2751,104 +2799,106 @@ export const fnF_INV_RT = args => {
2751
2799
  * Formula: n / ((n-1)(n-2)) * Σ((xi-mean)/s)^3, where s is the sample stdev.
2752
2800
  */
2753
2801
  export const fnSKEW = args => {
2754
- const nums = flattenNumbers(args);
2755
- const err = firstError(nums);
2756
- if (err) {
2757
- return err;
2802
+ const xs = toNumberArray(args);
2803
+ if (!Array.isArray(xs)) {
2804
+ return xs;
2758
2805
  }
2759
- const xs = nums.map(n => n.value);
2760
2806
  const n = xs.length;
2761
2807
  if (n < 3) {
2762
2808
  return ERRORS.DIV0;
2763
2809
  }
2810
+ // Single pass 1: mean.
2764
2811
  let sum = 0;
2765
- for (const v of xs) {
2766
- sum += v;
2812
+ for (let i = 0; i < n; i++) {
2813
+ sum += xs[i];
2767
2814
  }
2768
2815
  const mean = sum / n;
2816
+ // Single pass 2: accumulate Σ(x−μ)² and Σ(x−μ)³ together. Computing
2817
+ // the cubed normalisation after the loop (dividing by stdev³) is
2818
+ // algebraically equivalent to Σ((x−μ)/s)³ and avoids the third pass.
2769
2819
  let sumSq = 0;
2770
- for (const v of xs) {
2771
- sumSq += (v - mean) ** 2;
2820
+ let sumCube = 0;
2821
+ for (let i = 0; i < n; i++) {
2822
+ const d = xs[i] - mean;
2823
+ const d2 = d * d;
2824
+ sumSq += d2;
2825
+ sumCube += d2 * d;
2772
2826
  }
2773
2827
  const sampleStd = Math.sqrt(sumSq / (n - 1));
2774
2828
  if (sampleStd === 0) {
2775
2829
  return ERRORS.DIV0;
2776
2830
  }
2777
- let sumCubed = 0;
2778
- for (const v of xs) {
2779
- sumCubed += ((v - mean) / sampleStd) ** 3;
2780
- }
2781
- return rvNumber((n / ((n - 1) * (n - 2))) * sumCubed);
2831
+ return rvNumber((n / ((n - 1) * (n - 2))) * (sumCube / (sampleStd * sampleStd * sampleStd)));
2782
2832
  };
2783
2833
  /**
2784
2834
  * SKEW.P — population skewness.
2785
2835
  * Formula: (1/n) * Σ((xi-mean)/σ)^3, where σ is the population stdev.
2786
2836
  */
2787
2837
  export const fnSKEW_P = args => {
2788
- const nums = flattenNumbers(args);
2789
- const err = firstError(nums);
2790
- if (err) {
2791
- return err;
2838
+ const xs = toNumberArray(args);
2839
+ if (!Array.isArray(xs)) {
2840
+ return xs;
2792
2841
  }
2793
- const xs = nums.map(n => n.value);
2794
2842
  const n = xs.length;
2795
2843
  if (n < 1) {
2796
2844
  return ERRORS.DIV0;
2797
2845
  }
2798
2846
  let sum = 0;
2799
- for (const v of xs) {
2800
- sum += v;
2847
+ for (let i = 0; i < n; i++) {
2848
+ sum += xs[i];
2801
2849
  }
2802
2850
  const mean = sum / n;
2803
2851
  let sumSq = 0;
2804
- for (const v of xs) {
2805
- sumSq += (v - mean) ** 2;
2852
+ let sumCube = 0;
2853
+ for (let i = 0; i < n; i++) {
2854
+ const d = xs[i] - mean;
2855
+ const d2 = d * d;
2856
+ sumSq += d2;
2857
+ sumCube += d2 * d;
2806
2858
  }
2807
2859
  const popStd = Math.sqrt(sumSq / n);
2808
2860
  if (popStd === 0) {
2809
2861
  return ERRORS.DIV0;
2810
2862
  }
2811
- let sumCubed = 0;
2812
- for (const v of xs) {
2813
- sumCubed += ((v - mean) / popStd) ** 3;
2814
- }
2815
- return rvNumber(sumCubed / n);
2863
+ return rvNumber(sumCube / n / (popStd * popStd * popStd));
2816
2864
  };
2817
2865
  /**
2818
2866
  * KURT — sample excess kurtosis.
2819
2867
  * Formula: n(n+1) / ((n-1)(n-2)(n-3)) * Σ((xi-mean)/s)^4 - 3(n-1)^2 / ((n-2)(n-3)).
2820
2868
  */
2821
2869
  export const fnKURT = args => {
2822
- const nums = flattenNumbers(args);
2823
- const err = firstError(nums);
2824
- if (err) {
2825
- return err;
2870
+ const xs = toNumberArray(args);
2871
+ if (!Array.isArray(xs)) {
2872
+ return xs;
2826
2873
  }
2827
- const xs = nums.map(n => n.value);
2828
2874
  const n = xs.length;
2829
2875
  if (n < 4) {
2830
2876
  return ERRORS.DIV0;
2831
2877
  }
2832
2878
  let sum = 0;
2833
- for (const v of xs) {
2834
- sum += v;
2879
+ for (let i = 0; i < n; i++) {
2880
+ sum += xs[i];
2835
2881
  }
2836
2882
  const mean = sum / n;
2883
+ // Single pass for Σ(x−μ)² and Σ(x−μ)⁴ — the `(x−μ)/s` normalisation
2884
+ // is factored out after the loop (divide by stdev⁴) so we don't need
2885
+ // to know `s` ahead of time.
2837
2886
  let sumSq = 0;
2838
- for (const v of xs) {
2839
- sumSq += (v - mean) ** 2;
2887
+ let sumQuad = 0;
2888
+ for (let i = 0; i < n; i++) {
2889
+ const d = xs[i] - mean;
2890
+ const d2 = d * d;
2891
+ sumSq += d2;
2892
+ sumQuad += d2 * d2;
2840
2893
  }
2841
2894
  const sampleStd = Math.sqrt(sumSq / (n - 1));
2842
2895
  if (sampleStd === 0) {
2843
2896
  return ERRORS.DIV0;
2844
2897
  }
2845
- let sumQuad = 0;
2846
- for (const v of xs) {
2847
- sumQuad += ((v - mean) / sampleStd) ** 4;
2848
- }
2898
+ const s4 = sampleStd * sampleStd;
2849
2899
  const term1 = (n * (n + 1)) / ((n - 1) * (n - 2) * (n - 3));
2850
2900
  const term2 = (3 * (n - 1) ** 2) / ((n - 2) * (n - 3));
2851
- return rvNumber(term1 * sumQuad - term2);
2901
+ return rvNumber(term1 * (sumQuad / (s4 * s4)) - term2);
2852
2902
  };
2853
2903
  // ============================================================================
2854
2904
  // PERCENTRANK family