@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
|
@@ -55,8 +55,8 @@ const fnTEXTJOIN = args => {
|
|
|
55
55
|
if (e0) {
|
|
56
56
|
return e0;
|
|
57
57
|
}
|
|
58
|
-
const delimiter = (0, values_1.toStringRV)(args[0]);
|
|
59
|
-
const ignoreEmptyRV = (0, values_1.toBooleanRV)(args[1]);
|
|
58
|
+
const delimiter = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
59
|
+
const ignoreEmptyRV = (0, values_1.toBooleanRV)((0, values_1.topLeft)(args[1]));
|
|
60
60
|
if ((0, values_1.isError)(ignoreEmptyRV)) {
|
|
61
61
|
return ignoreEmptyRV;
|
|
62
62
|
}
|
|
@@ -102,7 +102,8 @@ const fnLEFT = args => {
|
|
|
102
102
|
if (err) {
|
|
103
103
|
return err;
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
// Implicit intersection on the text arg (see MID for rationale).
|
|
106
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
106
107
|
let n;
|
|
107
108
|
if (args.length > 1) {
|
|
108
109
|
// Use `argToNumber` so array arguments get implicit-intersection to
|
|
@@ -131,7 +132,7 @@ const fnRIGHT = args => {
|
|
|
131
132
|
if (err) {
|
|
132
133
|
return err;
|
|
133
134
|
}
|
|
134
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
135
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
135
136
|
let n;
|
|
136
137
|
if (args.length > 1) {
|
|
137
138
|
// Implicit intersection via `argToNumber` — see LEFT for rationale.
|
|
@@ -159,9 +160,11 @@ const fnMID = args => {
|
|
|
159
160
|
if (err) {
|
|
160
161
|
return err;
|
|
161
162
|
}
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
//
|
|
163
|
+
// Implicit intersection on the text arg — without topLeft, passing
|
|
164
|
+
// an array would route through `toStringRV`'s default branch and
|
|
165
|
+
// silently return the empty string, making `MID(A1:A2, 1, 3)` look
|
|
166
|
+
// like an empty cell instead of a 3-char prefix of the first cell.
|
|
167
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
165
168
|
const startNumRV = (0, _shared_1.argToNumber)(args[1]);
|
|
166
169
|
if ((0, values_1.isError)(startNumRV)) {
|
|
167
170
|
return startNumRV;
|
|
@@ -258,7 +261,7 @@ const fnSUBSTITUTE = args => {
|
|
|
258
261
|
return (0, values_1.rvString)(text);
|
|
259
262
|
}
|
|
260
263
|
if (args.length > 3) {
|
|
261
|
-
const instanceNumRV = (0, values_1.toNumberRV)(args[3]);
|
|
264
|
+
const instanceNumRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[3]));
|
|
262
265
|
if ((0, values_1.isError)(instanceNumRV)) {
|
|
263
266
|
return instanceNumRV;
|
|
264
267
|
}
|
|
@@ -283,7 +286,7 @@ const fnREPLACE = args => {
|
|
|
283
286
|
if (err) {
|
|
284
287
|
return err;
|
|
285
288
|
}
|
|
286
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
289
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
287
290
|
// Implicit intersection on the numeric arguments — see LEFT.
|
|
288
291
|
const startNumRV = (0, _shared_1.argToNumber)(args[1]);
|
|
289
292
|
if ((0, values_1.isError)(startNumRV)) {
|
|
@@ -305,7 +308,7 @@ const fnREPLACE = args => {
|
|
|
305
308
|
if (e3) {
|
|
306
309
|
return e3;
|
|
307
310
|
}
|
|
308
|
-
const newText = (0, values_1.toStringRV)(args[3]);
|
|
311
|
+
const newText = (0, values_1.toStringRV)((0, values_1.topLeft)(args[3]));
|
|
309
312
|
return (0, values_1.rvString)(text.slice(0, startNum - 1) + newText + text.slice(startNum - 1 + numChars));
|
|
310
313
|
};
|
|
311
314
|
exports.fnREPLACE = fnREPLACE;
|
|
@@ -321,8 +324,9 @@ const fnFIND = args => {
|
|
|
321
324
|
if (err1) {
|
|
322
325
|
return err1;
|
|
323
326
|
}
|
|
324
|
-
|
|
325
|
-
const
|
|
327
|
+
// Implicit intersection on text args so arrays collapse to top-left.
|
|
328
|
+
const findText = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
329
|
+
const withinText = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
326
330
|
let startNum;
|
|
327
331
|
if (args.length > 2) {
|
|
328
332
|
// Implicit intersection so an array supplied as start_num collapses
|
|
@@ -353,8 +357,9 @@ const fnSEARCH = args => {
|
|
|
353
357
|
if (err1) {
|
|
354
358
|
return err1;
|
|
355
359
|
}
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
// Implicit intersection on text args (see FIND for rationale).
|
|
361
|
+
let findText = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
362
|
+
const withinText = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
358
363
|
let startNum;
|
|
359
364
|
if (args.length > 2) {
|
|
360
365
|
const startNumRV = (0, _shared_1.argToNumber)(args[2]);
|
|
@@ -395,7 +400,7 @@ const fnREPT = args => {
|
|
|
395
400
|
return err;
|
|
396
401
|
}
|
|
397
402
|
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
398
|
-
const timesRV = (0, values_1.toNumberRV)(args[1]);
|
|
403
|
+
const timesRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[1]));
|
|
399
404
|
if ((0, values_1.isError)(timesRV)) {
|
|
400
405
|
return timesRV;
|
|
401
406
|
}
|
|
@@ -425,7 +430,7 @@ const fnTEXT = args => {
|
|
|
425
430
|
if (e1) {
|
|
426
431
|
return e1;
|
|
427
432
|
}
|
|
428
|
-
const fmt = (0, values_1.toStringRV)(args[1]);
|
|
433
|
+
const fmt = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
429
434
|
// "@" format = return text as-is
|
|
430
435
|
if (fmt === "@") {
|
|
431
436
|
return (0, values_1.rvString)((0, values_1.toStringRV)(rawVal));
|
|
@@ -603,10 +608,52 @@ function formatNumber(val, fmt) {
|
|
|
603
608
|
tokens.push({ kind: "literal", text: ch });
|
|
604
609
|
}
|
|
605
610
|
// Count integer/fraction digit slots and decide whether to group.
|
|
611
|
+
// Trailing commas after the last integer digit token — before the
|
|
612
|
+
// decimal point or end of pattern — act as a "scale by 1/1000^k"
|
|
613
|
+
// multiplier (Excel's "thousands scaling"), not thousand separators.
|
|
614
|
+
// Example: `#,##0,` → 1,234,567 → "1,235" (divide by 1000).
|
|
615
|
+
// Example: `#,##0,,` → 1,234,567,890 → "1,234" (divide by 1,000,000).
|
|
606
616
|
let sawDot = false;
|
|
607
617
|
const intDigitSlots = [];
|
|
608
618
|
const fracDigitSlots = [];
|
|
609
619
|
let hasGrouping = false;
|
|
620
|
+
// Scan for trailing commas that sit strictly after the last integer
|
|
621
|
+
// digit slot but before any dot or fractional slot. Each such comma
|
|
622
|
+
// divides the value by 1000. Scan from end-of-pattern backward to
|
|
623
|
+
// locate them.
|
|
624
|
+
let scalingFactor = 1;
|
|
625
|
+
{
|
|
626
|
+
// Find the last integer digit-slot index.
|
|
627
|
+
let lastIntDigitIdx = -1;
|
|
628
|
+
let dotIdx = -1;
|
|
629
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
630
|
+
const t = tokens[i];
|
|
631
|
+
if (t.kind === "dot") {
|
|
632
|
+
dotIdx = i;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
if (t.kind === "digit") {
|
|
636
|
+
lastIntDigitIdx = i;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Trailing commas live after `lastIntDigitIdx` and strictly before
|
|
640
|
+
// `dotIdx` (if present). They must be adjacent — only run-of-commas
|
|
641
|
+
// directly abutting the last integer digit count as scaling.
|
|
642
|
+
if (lastIntDigitIdx !== -1) {
|
|
643
|
+
const stopBefore = dotIdx === -1 ? tokens.length : dotIdx;
|
|
644
|
+
let k = lastIntDigitIdx + 1;
|
|
645
|
+
while (k < stopBefore && tokens[k].kind === "comma") {
|
|
646
|
+
scalingFactor *= 1000;
|
|
647
|
+
// Remove this comma so later logic doesn't treat it as a
|
|
648
|
+
// thousand-separator or literal artefact. We mutate the token
|
|
649
|
+
// to a no-op literal with empty text.
|
|
650
|
+
tokens[k] = { kind: "literal", text: "" };
|
|
651
|
+
k++;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Apply the scaling before any other formatting work.
|
|
656
|
+
const scaledVal = val / scalingFactor;
|
|
610
657
|
for (const t of tokens) {
|
|
611
658
|
if (t.kind === "dot") {
|
|
612
659
|
sawDot = true;
|
|
@@ -630,7 +677,7 @@ function formatNumber(val, fmt) {
|
|
|
630
677
|
return fmt; // Nothing to format; return the (raw) pattern.
|
|
631
678
|
}
|
|
632
679
|
// Round to the requested fractional precision.
|
|
633
|
-
const rounded = roundHalfAwayFromZeroFmt(
|
|
680
|
+
const rounded = roundHalfAwayFromZeroFmt(scaledVal, fracDigitSlots.length);
|
|
634
681
|
const sign = rounded < 0 ? "-" : "";
|
|
635
682
|
const absStr = Math.abs(rounded).toFixed(fracDigitSlots.length);
|
|
636
683
|
const [intPartRaw, fracPart = ""] = absStr.split(".");
|
|
@@ -710,11 +757,19 @@ function formatNumber(val, fmt) {
|
|
|
710
757
|
emittedOverflow = true;
|
|
711
758
|
}
|
|
712
759
|
const slotIdx = intCursor;
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
// the
|
|
716
|
-
|
|
717
|
-
|
|
760
|
+
// Right-align the integer into the digit slots: leading `#` slots
|
|
761
|
+
// emit nothing when the value is shorter than the pattern, and
|
|
762
|
+
// leading `0` slots pad with a zero. Previously the logic treated
|
|
763
|
+
// every slot as if the integer started at slot 0 (left-align),
|
|
764
|
+
// which made `TEXT(0, "#,##0")` produce "00" instead of "0".
|
|
765
|
+
//
|
|
766
|
+
// The number of "padding" slots that must appear before the first
|
|
767
|
+
// real digit equals `totalIntSlots − groupedInt.length` when that
|
|
768
|
+
// value is positive (overflow path zeroes it out since we already
|
|
769
|
+
// prepended the overflow digits before the first slot).
|
|
770
|
+
const paddingSlots = overflowDigits > 0 ? 0 : Math.max(0, totalIntSlots - groupedInt.length);
|
|
771
|
+
const srcIdx = overflowDigits + slotIdx - paddingSlots;
|
|
772
|
+
if (srcIdx >= 0 && srcIdx < groupedInt.length) {
|
|
718
773
|
out += groupedInt[srcIdx];
|
|
719
774
|
}
|
|
720
775
|
else if (t.char === "0") {
|
|
@@ -1038,7 +1093,7 @@ const fnEXACT = args => {
|
|
|
1038
1093
|
if (err1) {
|
|
1039
1094
|
return err1;
|
|
1040
1095
|
}
|
|
1041
|
-
return (0, values_1.rvBoolean)((0, values_1.toStringRV)(args[0]) === (0, values_1.toStringRV)(args[1]));
|
|
1096
|
+
return (0, values_1.rvBoolean)((0, values_1.toStringRV)((0, values_1.topLeft)(args[0])) === (0, values_1.toStringRV)((0, values_1.topLeft)(args[1])));
|
|
1042
1097
|
};
|
|
1043
1098
|
exports.fnEXACT = fnEXACT;
|
|
1044
1099
|
// ============================================================================
|
|
@@ -1104,7 +1159,7 @@ const fnUNICHAR = args => {
|
|
|
1104
1159
|
if (err) {
|
|
1105
1160
|
return err;
|
|
1106
1161
|
}
|
|
1107
|
-
const nRV = (0, values_1.toNumberRV)(args[0]);
|
|
1162
|
+
const nRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[0]));
|
|
1108
1163
|
if ((0, values_1.isError)(nRV)) {
|
|
1109
1164
|
return nRV;
|
|
1110
1165
|
}
|
|
@@ -1125,7 +1180,7 @@ const fnUNICODE = args => {
|
|
|
1125
1180
|
if (err) {
|
|
1126
1181
|
return err;
|
|
1127
1182
|
}
|
|
1128
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
1183
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1129
1184
|
if (text.length === 0) {
|
|
1130
1185
|
return values_1.ERRORS.VALUE;
|
|
1131
1186
|
}
|
|
@@ -1138,18 +1193,18 @@ const fnBAHTTEXT = args => {
|
|
|
1138
1193
|
if (err) {
|
|
1139
1194
|
return err;
|
|
1140
1195
|
}
|
|
1141
|
-
return (0, values_1.rvString)((0, values_1.toStringRV)(args[0]));
|
|
1196
|
+
return (0, values_1.rvString)((0, values_1.toStringRV)((0, values_1.topLeft)(args[0])));
|
|
1142
1197
|
};
|
|
1143
1198
|
exports.fnBAHTTEXT = fnBAHTTEXT;
|
|
1144
1199
|
const fnDOLLAR = args => {
|
|
1145
|
-
const numRV = (0, values_1.toNumberRV)(args[0]);
|
|
1200
|
+
const numRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[0]));
|
|
1146
1201
|
if ((0, values_1.isError)(numRV)) {
|
|
1147
1202
|
return numRV;
|
|
1148
1203
|
}
|
|
1149
1204
|
const num = numRV.value;
|
|
1150
1205
|
let decimals;
|
|
1151
1206
|
if (args.length > 1) {
|
|
1152
|
-
const decRV = (0, values_1.toNumberRV)(args[1]);
|
|
1207
|
+
const decRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[1]));
|
|
1153
1208
|
if ((0, values_1.isError)(decRV)) {
|
|
1154
1209
|
return decRV;
|
|
1155
1210
|
}
|
|
@@ -1175,14 +1230,14 @@ const fnDOLLAR = args => {
|
|
|
1175
1230
|
};
|
|
1176
1231
|
exports.fnDOLLAR = fnDOLLAR;
|
|
1177
1232
|
const fnFIXED = args => {
|
|
1178
|
-
const numRV = (0, values_1.toNumberRV)(args[0]);
|
|
1233
|
+
const numRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[0]));
|
|
1179
1234
|
if ((0, values_1.isError)(numRV)) {
|
|
1180
1235
|
return numRV;
|
|
1181
1236
|
}
|
|
1182
1237
|
const num = numRV.value;
|
|
1183
1238
|
let decimals;
|
|
1184
1239
|
if (args.length > 1) {
|
|
1185
|
-
const decRV = (0, values_1.toNumberRV)(args[1]);
|
|
1240
|
+
const decRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[1]));
|
|
1186
1241
|
if ((0, values_1.isError)(decRV)) {
|
|
1187
1242
|
return decRV;
|
|
1188
1243
|
}
|
|
@@ -1193,7 +1248,7 @@ const fnFIXED = args => {
|
|
|
1193
1248
|
}
|
|
1194
1249
|
let noCommas;
|
|
1195
1250
|
if (args.length > 2) {
|
|
1196
|
-
const ncRV = (0, values_1.toBooleanRV)(args[2]);
|
|
1251
|
+
const ncRV = (0, values_1.toBooleanRV)((0, values_1.topLeft)(args[2]));
|
|
1197
1252
|
if ((0, values_1.isError)(ncRV)) {
|
|
1198
1253
|
return ncRV;
|
|
1199
1254
|
}
|
|
@@ -1225,7 +1280,7 @@ const fnASC = args => {
|
|
|
1225
1280
|
if (err) {
|
|
1226
1281
|
return err;
|
|
1227
1282
|
}
|
|
1228
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
1283
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1229
1284
|
return (0, values_1.rvString)(text.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xfee0)));
|
|
1230
1285
|
};
|
|
1231
1286
|
exports.fnASC = fnASC;
|
|
@@ -1234,7 +1289,7 @@ const fnDBCS = args => {
|
|
|
1234
1289
|
if (err) {
|
|
1235
1290
|
return err;
|
|
1236
1291
|
}
|
|
1237
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
1292
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1238
1293
|
return (0, values_1.rvString)(text.replace(/[!-~]/g, ch => String.fromCharCode(ch.charCodeAt(0) + 0xfee0)));
|
|
1239
1294
|
};
|
|
1240
1295
|
exports.fnDBCS = fnDBCS;
|
|
@@ -1255,14 +1310,15 @@ const fnNUMBERVALUE = args => {
|
|
|
1255
1310
|
if (e0) {
|
|
1256
1311
|
return e0;
|
|
1257
1312
|
}
|
|
1258
|
-
|
|
1313
|
+
// Implicit intersection on every text arg.
|
|
1314
|
+
let text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1259
1315
|
let decSep = ".";
|
|
1260
1316
|
if (args.length > 1) {
|
|
1261
1317
|
const e1 = (0, _shared_1.checkError)(args[1]);
|
|
1262
1318
|
if (e1) {
|
|
1263
1319
|
return e1;
|
|
1264
1320
|
}
|
|
1265
|
-
decSep = (0, values_1.toStringRV)(args[1]);
|
|
1321
|
+
decSep = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
1266
1322
|
}
|
|
1267
1323
|
let grpSep = ",";
|
|
1268
1324
|
if (args.length > 2) {
|
|
@@ -1270,15 +1326,19 @@ const fnNUMBERVALUE = args => {
|
|
|
1270
1326
|
if (e2) {
|
|
1271
1327
|
return e2;
|
|
1272
1328
|
}
|
|
1273
|
-
grpSep = (0, values_1.toStringRV)(args[2]);
|
|
1329
|
+
grpSep = (0, values_1.toStringRV)((0, values_1.topLeft)(args[2]));
|
|
1274
1330
|
}
|
|
1275
1331
|
text = text.split(grpSep).join("");
|
|
1276
1332
|
if (decSep !== ".") {
|
|
1277
1333
|
text = text.replace(decSep, ".");
|
|
1278
1334
|
}
|
|
1279
|
-
// Handle percentage
|
|
1280
|
-
|
|
1281
|
-
|
|
1335
|
+
// Handle percentage. Excel divides by 100 for EACH trailing `%`, so
|
|
1336
|
+
// `NUMBERVALUE("50%%")` = 50 / 10000 = 0.005. Previously we only
|
|
1337
|
+
// recognised the first `%` and treated `50%%` as the literal string
|
|
1338
|
+
// "50%" → NaN → #VALUE!.
|
|
1339
|
+
let pctCount = 0;
|
|
1340
|
+
while (text.endsWith("%")) {
|
|
1341
|
+
pctCount++;
|
|
1282
1342
|
text = text.slice(0, -1);
|
|
1283
1343
|
}
|
|
1284
1344
|
// `Number("")` is 0, not NaN — reject empty / whitespace-only inputs
|
|
@@ -1290,7 +1350,7 @@ const fnNUMBERVALUE = args => {
|
|
|
1290
1350
|
if (isNaN(n)) {
|
|
1291
1351
|
return values_1.ERRORS.VALUE;
|
|
1292
1352
|
}
|
|
1293
|
-
return (0, values_1.rvNumber)(
|
|
1353
|
+
return (0, values_1.rvNumber)(pctCount > 0 ? n / Math.pow(100, pctCount) : n);
|
|
1294
1354
|
};
|
|
1295
1355
|
exports.fnNUMBERVALUE = fnNUMBERVALUE;
|
|
1296
1356
|
// ============================================================================
|
|
@@ -1309,7 +1369,7 @@ exports.fnNUMBERVALUE = fnNUMBERVALUE;
|
|
|
1309
1369
|
function parseTextBeforeAfterTail(args) {
|
|
1310
1370
|
let inst = 1;
|
|
1311
1371
|
if (args.length > 2) {
|
|
1312
|
-
const instRV = (0, values_1.toNumberRV)(args[2]);
|
|
1372
|
+
const instRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2]));
|
|
1313
1373
|
if ((0, values_1.isError)(instRV)) {
|
|
1314
1374
|
return instRV;
|
|
1315
1375
|
}
|
|
@@ -1317,7 +1377,7 @@ function parseTextBeforeAfterTail(args) {
|
|
|
1317
1377
|
}
|
|
1318
1378
|
let matchMode = 0;
|
|
1319
1379
|
if (args.length > 3) {
|
|
1320
|
-
const mmRV = (0, values_1.toNumberRV)(args[3]);
|
|
1380
|
+
const mmRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[3]));
|
|
1321
1381
|
if ((0, values_1.isError)(mmRV)) {
|
|
1322
1382
|
return mmRV;
|
|
1323
1383
|
}
|
|
@@ -1329,7 +1389,7 @@ function parseTextBeforeAfterTail(args) {
|
|
|
1329
1389
|
}
|
|
1330
1390
|
let matchEnd = 0;
|
|
1331
1391
|
if (args.length > 4) {
|
|
1332
|
-
const meRV = (0, values_1.toNumberRV)(args[4]);
|
|
1392
|
+
const meRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[4]));
|
|
1333
1393
|
if ((0, values_1.isError)(meRV)) {
|
|
1334
1394
|
return meRV;
|
|
1335
1395
|
}
|
|
@@ -1351,8 +1411,8 @@ const fnTEXTBEFORE = args => {
|
|
|
1351
1411
|
if (e1) {
|
|
1352
1412
|
return e1;
|
|
1353
1413
|
}
|
|
1354
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
1355
|
-
const delimiter = (0, values_1.toStringRV)(args[1]);
|
|
1414
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1415
|
+
const delimiter = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
1356
1416
|
const tail = parseTextBeforeAfterTail(args);
|
|
1357
1417
|
if ("kind" in tail && tail.kind === 4 /* RVKind.Error */) {
|
|
1358
1418
|
return tail;
|
|
@@ -1406,8 +1466,8 @@ const fnTEXTAFTER = args => {
|
|
|
1406
1466
|
if (e1) {
|
|
1407
1467
|
return e1;
|
|
1408
1468
|
}
|
|
1409
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
1410
|
-
const delimiter = (0, values_1.toStringRV)(args[1]);
|
|
1469
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1470
|
+
const delimiter = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
1411
1471
|
const tail = parseTextBeforeAfterTail(args);
|
|
1412
1472
|
if ("kind" in tail && tail.kind === 4 /* RVKind.Error */) {
|
|
1413
1473
|
return tail;
|
|
@@ -1453,21 +1513,21 @@ const fnTEXTSPLIT = args => {
|
|
|
1453
1513
|
if (e0) {
|
|
1454
1514
|
return e0;
|
|
1455
1515
|
}
|
|
1456
|
-
const text = (0, values_1.toStringRV)(args[0]);
|
|
1516
|
+
const text = (0, values_1.toStringRV)((0, values_1.topLeft)(args[0]));
|
|
1457
1517
|
let colDelimiter = "";
|
|
1458
1518
|
if (args.length > 1) {
|
|
1459
1519
|
const e1 = (0, _shared_1.checkError)(args[1]);
|
|
1460
1520
|
if (e1) {
|
|
1461
1521
|
return e1;
|
|
1462
1522
|
}
|
|
1463
|
-
colDelimiter = (0, values_1.toStringRV)(args[1]);
|
|
1523
|
+
colDelimiter = (0, values_1.toStringRV)((0, values_1.topLeft)(args[1]));
|
|
1464
1524
|
}
|
|
1465
|
-
const rowDelimiter = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toStringRV)(args[2]) : "";
|
|
1525
|
+
const rowDelimiter = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toStringRV)((0, values_1.topLeft)(args[2])) : "";
|
|
1466
1526
|
// `ignore_empty` (4th arg, default FALSE) — when TRUE, suppress empty
|
|
1467
1527
|
// fragments produced by consecutive delimiters.
|
|
1468
1528
|
let ignoreEmpty = false;
|
|
1469
1529
|
if (args.length > 3 && args[3].kind !== 0 /* RVKind.Blank */) {
|
|
1470
|
-
const ieRV = (0, values_1.toBooleanRV)(args[3]);
|
|
1530
|
+
const ieRV = (0, values_1.toBooleanRV)((0, values_1.topLeft)(args[3]));
|
|
1471
1531
|
if ((0, values_1.isError)(ieRV)) {
|
|
1472
1532
|
return ieRV;
|
|
1473
1533
|
}
|
|
@@ -1479,7 +1539,7 @@ const fnTEXTSPLIT = args => {
|
|
|
1479
1539
|
// consistent with Excel's specification for TEXTSPLIT.
|
|
1480
1540
|
let matchMode = 0;
|
|
1481
1541
|
if (args.length > 4 && args[4].kind !== 0 /* RVKind.Blank */) {
|
|
1482
|
-
const mmRV = (0, values_1.toNumberRV)(args[4]);
|
|
1542
|
+
const mmRV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[4]));
|
|
1483
1543
|
if ((0, values_1.isError)(mmRV)) {
|
|
1484
1544
|
return mmRV;
|
|
1485
1545
|
}
|
|
@@ -171,10 +171,29 @@ function calculateFormulasImpl(workbook) {
|
|
|
171
171
|
let evalOrder = (0, dependency_analysis_1.topologicalSort)(graph);
|
|
172
172
|
// ── Step 6: Evaluate ──
|
|
173
173
|
const session = new evaluator_1.EvalSession();
|
|
174
|
+
// Convert user-registered functions (keyed opaquely on WorkbookLike)
|
|
175
|
+
// into the evaluator's typed `FunctionDescriptor` shape. We take a
|
|
176
|
+
// snapshot up front so later mutations to the workbook's map during
|
|
177
|
+
// evaluation can't observe a half-built state.
|
|
178
|
+
let userFunctions;
|
|
179
|
+
if (workbook.userFunctions && workbook.userFunctions.size > 0) {
|
|
180
|
+
const adapted = new Map();
|
|
181
|
+
for (const [name, desc] of workbook.userFunctions) {
|
|
182
|
+
const upperName = name.toUpperCase();
|
|
183
|
+
adapted.set(upperName, {
|
|
184
|
+
name: upperName,
|
|
185
|
+
minArity: desc.minArity,
|
|
186
|
+
maxArity: desc.maxArity,
|
|
187
|
+
invoke: desc.invoke
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
userFunctions = adapted;
|
|
191
|
+
}
|
|
174
192
|
const ctx = {
|
|
175
193
|
snapshot,
|
|
176
194
|
compiledFormulas: compiledMap,
|
|
177
|
-
currentSheet: snapshot.worksheets[0]?.name ?? ""
|
|
195
|
+
currentSheet: snapshot.worksheets[0]?.name ?? "",
|
|
196
|
+
userFunctions
|
|
178
197
|
};
|
|
179
198
|
// Evaluate in topological order
|
|
180
199
|
evaluateInOrder(evalOrder, compiledMap, results, ctx, session);
|
|
@@ -53,6 +53,13 @@ function buildWritebackPlan(snapshot, compiled, results, previousSpills, previou
|
|
|
53
53
|
// Build ghost map from previous spills (validated against snapshot)
|
|
54
54
|
const ghostMap = new Map();
|
|
55
55
|
for (const [srcKey, region] of previousSpills) {
|
|
56
|
+
// Hoist the worksheet lookup once per region — the previous code did
|
|
57
|
+
// `snapshot.worksheetsById.get(…)` inside the inner cell loop,
|
|
58
|
+
// paying the Map lookup cost `region.rows × region.cols` times.
|
|
59
|
+
const ws = snapshot.worksheetsById.get(region.worksheetId);
|
|
60
|
+
if (!ws) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
56
63
|
for (let r = 0; r < region.rows; r++) {
|
|
57
64
|
for (let c = 0; c < region.cols; c++) {
|
|
58
65
|
if (r === 0 && c === 0) {
|
|
@@ -62,12 +69,9 @@ function buildWritebackPlan(snapshot, compiled, results, previousSpills, previou
|
|
|
62
69
|
const targetCol = region.sourceCol + c;
|
|
63
70
|
const targetKey = (0, workbook_snapshot_1.spillCellKeyFromId)(region.worksheetId, targetRow, targetCol);
|
|
64
71
|
// Validate ghost cell is still unmodified
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
if (isGhostUnmodified(cell, targetKey, previousGhosts)) {
|
|
69
|
-
ghostMap.set(targetKey, srcKey);
|
|
70
|
-
}
|
|
72
|
+
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(targetRow, targetCol));
|
|
73
|
+
if (isGhostUnmodified(cell, targetKey, previousGhosts)) {
|
|
74
|
+
ghostMap.set(targetKey, srcKey);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
}
|