@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
|
@@ -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
|
-
|
|
254
|
-
const err =
|
|
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
|
-
|
|
269
|
-
|
|
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 (
|
|
275
|
+
if (count === 0) {
|
|
274
276
|
return ERRORS.DIV0;
|
|
275
277
|
}
|
|
276
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
293
|
+
return seen ? rvNumber(min) : rvNumber(0);
|
|
294
|
+
};
|
|
295
|
+
export const fnMAX = args => {
|
|
309
296
|
let max = -Infinity;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
//
|
|
332
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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 (
|
|
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 (
|
|
403
|
-
|
|
404
|
-
const val =
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
const err =
|
|
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(
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
form = Math.
|
|
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
|
-
|
|
1007
|
-
|
|
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
|
};
|