@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.
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/modules/excel/cell.d.ts +18 -0
- package/dist/browser/modules/excel/cell.js +21 -0
- package/dist/browser/modules/excel/utils/cell-format.js +85 -13
- package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/browser/modules/excel/workbook.browser.js +49 -0
- package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/browser/modules/formula/compile/binder.js +48 -6
- package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
- package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/browser/modules/formula/functions/_shared.js +47 -0
- package/dist/browser/modules/formula/functions/conditional.js +103 -22
- package/dist/browser/modules/formula/functions/date.js +105 -23
- package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/browser/modules/formula/functions/engineering.js +103 -151
- package/dist/browser/modules/formula/functions/financial.js +210 -184
- package/dist/browser/modules/formula/functions/lookup.js +224 -157
- package/dist/browser/modules/formula/functions/math.d.ts +26 -0
- package/dist/browser/modules/formula/functions/math.js +249 -69
- package/dist/browser/modules/formula/functions/statistical.js +221 -171
- package/dist/browser/modules/formula/functions/text.js +112 -52
- package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
- package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
- package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
- package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
- package/dist/browser/modules/formula/runtime/values.js +20 -2
- package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/browser/modules/formula/syntax/ast.js +1 -0
- package/dist/browser/modules/formula/syntax/parser.js +29 -7
- package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
- package/dist/browser/modules/formula/syntax/token-types.js +9 -0
- package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/modules/excel/cell.js +21 -0
- package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
- package/dist/cjs/modules/excel/workbook.browser.js +49 -0
- package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/cjs/modules/formula/compile/binder.js +48 -6
- package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/cjs/modules/formula/functions/_shared.js +48 -0
- package/dist/cjs/modules/formula/functions/conditional.js +103 -22
- package/dist/cjs/modules/formula/functions/date.js +104 -22
- package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/cjs/modules/formula/functions/engineering.js +109 -157
- package/dist/cjs/modules/formula/functions/financial.js +209 -183
- package/dist/cjs/modules/formula/functions/lookup.js +224 -157
- package/dist/cjs/modules/formula/functions/math.js +254 -70
- package/dist/cjs/modules/formula/functions/statistical.js +222 -172
- package/dist/cjs/modules/formula/functions/text.js +112 -52
- package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
- package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
- package/dist/cjs/modules/formula/runtime/values.js +21 -2
- package/dist/cjs/modules/formula/syntax/parser.js +29 -7
- package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
- package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/esm/index.js +2 -0
- package/dist/esm/modules/excel/cell.js +21 -0
- package/dist/esm/modules/excel/utils/cell-format.js +85 -13
- package/dist/esm/modules/excel/workbook.browser.js +49 -0
- package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/esm/modules/formula/compile/binder.js +48 -6
- package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
- package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/esm/modules/formula/functions/_shared.js +47 -0
- package/dist/esm/modules/formula/functions/conditional.js +103 -22
- package/dist/esm/modules/formula/functions/date.js +105 -23
- package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/esm/modules/formula/functions/engineering.js +103 -151
- package/dist/esm/modules/formula/functions/financial.js +210 -184
- package/dist/esm/modules/formula/functions/lookup.js +224 -157
- package/dist/esm/modules/formula/functions/math.js +249 -69
- package/dist/esm/modules/formula/functions/statistical.js +221 -171
- package/dist/esm/modules/formula/functions/text.js +112 -52
- package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
- package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
- package/dist/esm/modules/formula/runtime/values.js +20 -2
- package/dist/esm/modules/formula/syntax/ast.js +1 -0
- package/dist/esm/modules/formula/syntax/parser.js +29 -7
- package/dist/esm/modules/formula/syntax/token-types.js +9 -0
- package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/iife/excelts.iife.js +1502 -1379
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +26 -26
- package/dist/types/index.d.ts +1 -0
- package/dist/types/modules/excel/cell.d.ts +18 -0
- package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/types/modules/formula/functions/math.d.ts +26 -0
- package/dist/types/modules/formula/materialize/types.d.ts +15 -0
- package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/types/modules/formula/runtime/values.d.ts +13 -0
- package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
- 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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return err;
|
|
77
|
+
const values = toNumberArray(args);
|
|
78
|
+
if (!Array.isArray(values)) {
|
|
79
|
+
return values;
|
|
81
80
|
}
|
|
82
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
245
|
-
|
|
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
|
|
252
|
-
const err =
|
|
253
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
-
|
|
744
|
-
if (nums.length === 0) {
|
|
815
|
+
if (outOfRange || count === 0) {
|
|
745
816
|
return ERRORS.NUM;
|
|
746
817
|
}
|
|
747
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
763
|
-
if (nums.length === 0) {
|
|
835
|
+
if (outOfRange || count === 0) {
|
|
764
836
|
return ERRORS.NUM;
|
|
765
837
|
}
|
|
766
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
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 =
|
|
910
|
-
const y =
|
|
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
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
2755
|
-
|
|
2756
|
-
|
|
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 (
|
|
2766
|
-
sum +=
|
|
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
|
-
|
|
2771
|
-
|
|
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
|
-
|
|
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
|
|
2789
|
-
|
|
2790
|
-
|
|
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 (
|
|
2800
|
-
sum +=
|
|
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
|
-
|
|
2805
|
-
|
|
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
|
-
|
|
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
|
|
2823
|
-
|
|
2824
|
-
|
|
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 (
|
|
2834
|
-
sum +=
|
|
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
|
-
|
|
2839
|
-
|
|
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
|
-
|
|
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
|