@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
|
@@ -51,8 +51,8 @@ export const fnTEXTJOIN = args => {
|
|
|
51
51
|
if (e0) {
|
|
52
52
|
return e0;
|
|
53
53
|
}
|
|
54
|
-
const delimiter = toStringRV(args[0]);
|
|
55
|
-
const ignoreEmptyRV = toBooleanRV(args[1]);
|
|
54
|
+
const delimiter = toStringRV(topLeft(args[0]));
|
|
55
|
+
const ignoreEmptyRV = toBooleanRV(topLeft(args[1]));
|
|
56
56
|
if (isError(ignoreEmptyRV)) {
|
|
57
57
|
return ignoreEmptyRV;
|
|
58
58
|
}
|
|
@@ -97,7 +97,8 @@ export const fnLEFT = args => {
|
|
|
97
97
|
if (err) {
|
|
98
98
|
return err;
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// Implicit intersection on the text arg (see MID for rationale).
|
|
101
|
+
const text = toStringRV(topLeft(args[0]));
|
|
101
102
|
let n;
|
|
102
103
|
if (args.length > 1) {
|
|
103
104
|
// Use `argToNumber` so array arguments get implicit-intersection to
|
|
@@ -125,7 +126,7 @@ export const fnRIGHT = args => {
|
|
|
125
126
|
if (err) {
|
|
126
127
|
return err;
|
|
127
128
|
}
|
|
128
|
-
const text = toStringRV(args[0]);
|
|
129
|
+
const text = toStringRV(topLeft(args[0]));
|
|
129
130
|
let n;
|
|
130
131
|
if (args.length > 1) {
|
|
131
132
|
// Implicit intersection via `argToNumber` — see LEFT for rationale.
|
|
@@ -152,9 +153,11 @@ export const fnMID = args => {
|
|
|
152
153
|
if (err) {
|
|
153
154
|
return err;
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
156
|
+
// Implicit intersection on the text arg — without topLeft, passing
|
|
157
|
+
// an array would route through `toStringRV`'s default branch and
|
|
158
|
+
// silently return the empty string, making `MID(A1:A2, 1, 3)` look
|
|
159
|
+
// like an empty cell instead of a 3-char prefix of the first cell.
|
|
160
|
+
const text = toStringRV(topLeft(args[0]));
|
|
158
161
|
const startNumRV = argToNumber(args[1]);
|
|
159
162
|
if (isError(startNumRV)) {
|
|
160
163
|
return startNumRV;
|
|
@@ -245,7 +248,7 @@ export const fnSUBSTITUTE = args => {
|
|
|
245
248
|
return rvString(text);
|
|
246
249
|
}
|
|
247
250
|
if (args.length > 3) {
|
|
248
|
-
const instanceNumRV = toNumberRV(args[3]);
|
|
251
|
+
const instanceNumRV = toNumberRV(topLeft(args[3]));
|
|
249
252
|
if (isError(instanceNumRV)) {
|
|
250
253
|
return instanceNumRV;
|
|
251
254
|
}
|
|
@@ -269,7 +272,7 @@ export const fnREPLACE = args => {
|
|
|
269
272
|
if (err) {
|
|
270
273
|
return err;
|
|
271
274
|
}
|
|
272
|
-
const text = toStringRV(args[0]);
|
|
275
|
+
const text = toStringRV(topLeft(args[0]));
|
|
273
276
|
// Implicit intersection on the numeric arguments — see LEFT.
|
|
274
277
|
const startNumRV = argToNumber(args[1]);
|
|
275
278
|
if (isError(startNumRV)) {
|
|
@@ -291,7 +294,7 @@ export const fnREPLACE = args => {
|
|
|
291
294
|
if (e3) {
|
|
292
295
|
return e3;
|
|
293
296
|
}
|
|
294
|
-
const newText = toStringRV(args[3]);
|
|
297
|
+
const newText = toStringRV(topLeft(args[3]));
|
|
295
298
|
return rvString(text.slice(0, startNum - 1) + newText + text.slice(startNum - 1 + numChars));
|
|
296
299
|
};
|
|
297
300
|
// ============================================================================
|
|
@@ -306,8 +309,9 @@ export const fnFIND = args => {
|
|
|
306
309
|
if (err1) {
|
|
307
310
|
return err1;
|
|
308
311
|
}
|
|
309
|
-
|
|
310
|
-
const
|
|
312
|
+
// Implicit intersection on text args so arrays collapse to top-left.
|
|
313
|
+
const findText = toStringRV(topLeft(args[0]));
|
|
314
|
+
const withinText = toStringRV(topLeft(args[1]));
|
|
311
315
|
let startNum;
|
|
312
316
|
if (args.length > 2) {
|
|
313
317
|
// Implicit intersection so an array supplied as start_num collapses
|
|
@@ -337,8 +341,9 @@ export const fnSEARCH = args => {
|
|
|
337
341
|
if (err1) {
|
|
338
342
|
return err1;
|
|
339
343
|
}
|
|
340
|
-
|
|
341
|
-
|
|
344
|
+
// Implicit intersection on text args (see FIND for rationale).
|
|
345
|
+
let findText = toStringRV(topLeft(args[0]));
|
|
346
|
+
const withinText = toStringRV(topLeft(args[1]));
|
|
342
347
|
let startNum;
|
|
343
348
|
if (args.length > 2) {
|
|
344
349
|
const startNumRV = argToNumber(args[2]);
|
|
@@ -378,7 +383,7 @@ export const fnREPT = args => {
|
|
|
378
383
|
return err;
|
|
379
384
|
}
|
|
380
385
|
const text = toStringRV(topLeft(args[0]));
|
|
381
|
-
const timesRV = toNumberRV(args[1]);
|
|
386
|
+
const timesRV = toNumberRV(topLeft(args[1]));
|
|
382
387
|
if (isError(timesRV)) {
|
|
383
388
|
return timesRV;
|
|
384
389
|
}
|
|
@@ -407,7 +412,7 @@ export const fnTEXT = args => {
|
|
|
407
412
|
if (e1) {
|
|
408
413
|
return e1;
|
|
409
414
|
}
|
|
410
|
-
const fmt = toStringRV(args[1]);
|
|
415
|
+
const fmt = toStringRV(topLeft(args[1]));
|
|
411
416
|
// "@" format = return text as-is
|
|
412
417
|
if (fmt === "@") {
|
|
413
418
|
return rvString(toStringRV(rawVal));
|
|
@@ -584,10 +589,52 @@ function formatNumber(val, fmt) {
|
|
|
584
589
|
tokens.push({ kind: "literal", text: ch });
|
|
585
590
|
}
|
|
586
591
|
// Count integer/fraction digit slots and decide whether to group.
|
|
592
|
+
// Trailing commas after the last integer digit token — before the
|
|
593
|
+
// decimal point or end of pattern — act as a "scale by 1/1000^k"
|
|
594
|
+
// multiplier (Excel's "thousands scaling"), not thousand separators.
|
|
595
|
+
// Example: `#,##0,` → 1,234,567 → "1,235" (divide by 1000).
|
|
596
|
+
// Example: `#,##0,,` → 1,234,567,890 → "1,234" (divide by 1,000,000).
|
|
587
597
|
let sawDot = false;
|
|
588
598
|
const intDigitSlots = [];
|
|
589
599
|
const fracDigitSlots = [];
|
|
590
600
|
let hasGrouping = false;
|
|
601
|
+
// Scan for trailing commas that sit strictly after the last integer
|
|
602
|
+
// digit slot but before any dot or fractional slot. Each such comma
|
|
603
|
+
// divides the value by 1000. Scan from end-of-pattern backward to
|
|
604
|
+
// locate them.
|
|
605
|
+
let scalingFactor = 1;
|
|
606
|
+
{
|
|
607
|
+
// Find the last integer digit-slot index.
|
|
608
|
+
let lastIntDigitIdx = -1;
|
|
609
|
+
let dotIdx = -1;
|
|
610
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
611
|
+
const t = tokens[i];
|
|
612
|
+
if (t.kind === "dot") {
|
|
613
|
+
dotIdx = i;
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
if (t.kind === "digit") {
|
|
617
|
+
lastIntDigitIdx = i;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Trailing commas live after `lastIntDigitIdx` and strictly before
|
|
621
|
+
// `dotIdx` (if present). They must be adjacent — only run-of-commas
|
|
622
|
+
// directly abutting the last integer digit count as scaling.
|
|
623
|
+
if (lastIntDigitIdx !== -1) {
|
|
624
|
+
const stopBefore = dotIdx === -1 ? tokens.length : dotIdx;
|
|
625
|
+
let k = lastIntDigitIdx + 1;
|
|
626
|
+
while (k < stopBefore && tokens[k].kind === "comma") {
|
|
627
|
+
scalingFactor *= 1000;
|
|
628
|
+
// Remove this comma so later logic doesn't treat it as a
|
|
629
|
+
// thousand-separator or literal artefact. We mutate the token
|
|
630
|
+
// to a no-op literal with empty text.
|
|
631
|
+
tokens[k] = { kind: "literal", text: "" };
|
|
632
|
+
k++;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Apply the scaling before any other formatting work.
|
|
637
|
+
const scaledVal = val / scalingFactor;
|
|
591
638
|
for (const t of tokens) {
|
|
592
639
|
if (t.kind === "dot") {
|
|
593
640
|
sawDot = true;
|
|
@@ -611,7 +658,7 @@ function formatNumber(val, fmt) {
|
|
|
611
658
|
return fmt; // Nothing to format; return the (raw) pattern.
|
|
612
659
|
}
|
|
613
660
|
// Round to the requested fractional precision.
|
|
614
|
-
const rounded = roundHalfAwayFromZeroFmt(
|
|
661
|
+
const rounded = roundHalfAwayFromZeroFmt(scaledVal, fracDigitSlots.length);
|
|
615
662
|
const sign = rounded < 0 ? "-" : "";
|
|
616
663
|
const absStr = Math.abs(rounded).toFixed(fracDigitSlots.length);
|
|
617
664
|
const [intPartRaw, fracPart = ""] = absStr.split(".");
|
|
@@ -691,11 +738,19 @@ function formatNumber(val, fmt) {
|
|
|
691
738
|
emittedOverflow = true;
|
|
692
739
|
}
|
|
693
740
|
const slotIdx = intCursor;
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
// the
|
|
697
|
-
|
|
698
|
-
|
|
741
|
+
// Right-align the integer into the digit slots: leading `#` slots
|
|
742
|
+
// emit nothing when the value is shorter than the pattern, and
|
|
743
|
+
// leading `0` slots pad with a zero. Previously the logic treated
|
|
744
|
+
// every slot as if the integer started at slot 0 (left-align),
|
|
745
|
+
// which made `TEXT(0, "#,##0")` produce "00" instead of "0".
|
|
746
|
+
//
|
|
747
|
+
// The number of "padding" slots that must appear before the first
|
|
748
|
+
// real digit equals `totalIntSlots − groupedInt.length` when that
|
|
749
|
+
// value is positive (overflow path zeroes it out since we already
|
|
750
|
+
// prepended the overflow digits before the first slot).
|
|
751
|
+
const paddingSlots = overflowDigits > 0 ? 0 : Math.max(0, totalIntSlots - groupedInt.length);
|
|
752
|
+
const srcIdx = overflowDigits + slotIdx - paddingSlots;
|
|
753
|
+
if (srcIdx >= 0 && srcIdx < groupedInt.length) {
|
|
699
754
|
out += groupedInt[srcIdx];
|
|
700
755
|
}
|
|
701
756
|
else if (t.char === "0") {
|
|
@@ -1018,7 +1073,7 @@ export const fnEXACT = args => {
|
|
|
1018
1073
|
if (err1) {
|
|
1019
1074
|
return err1;
|
|
1020
1075
|
}
|
|
1021
|
-
return rvBoolean(toStringRV(args[0]) === toStringRV(args[1]));
|
|
1076
|
+
return rvBoolean(toStringRV(topLeft(args[0])) === toStringRV(topLeft(args[1])));
|
|
1022
1077
|
};
|
|
1023
1078
|
// ============================================================================
|
|
1024
1079
|
// Additional Text Functions
|
|
@@ -1079,7 +1134,7 @@ export const fnUNICHAR = args => {
|
|
|
1079
1134
|
if (err) {
|
|
1080
1135
|
return err;
|
|
1081
1136
|
}
|
|
1082
|
-
const nRV = toNumberRV(args[0]);
|
|
1137
|
+
const nRV = toNumberRV(topLeft(args[0]));
|
|
1083
1138
|
if (isError(nRV)) {
|
|
1084
1139
|
return nRV;
|
|
1085
1140
|
}
|
|
@@ -1099,7 +1154,7 @@ export const fnUNICODE = args => {
|
|
|
1099
1154
|
if (err) {
|
|
1100
1155
|
return err;
|
|
1101
1156
|
}
|
|
1102
|
-
const text = toStringRV(args[0]);
|
|
1157
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1103
1158
|
if (text.length === 0) {
|
|
1104
1159
|
return ERRORS.VALUE;
|
|
1105
1160
|
}
|
|
@@ -1111,17 +1166,17 @@ export const fnBAHTTEXT = args => {
|
|
|
1111
1166
|
if (err) {
|
|
1112
1167
|
return err;
|
|
1113
1168
|
}
|
|
1114
|
-
return rvString(toStringRV(args[0]));
|
|
1169
|
+
return rvString(toStringRV(topLeft(args[0])));
|
|
1115
1170
|
};
|
|
1116
1171
|
export const fnDOLLAR = args => {
|
|
1117
|
-
const numRV = toNumberRV(args[0]);
|
|
1172
|
+
const numRV = toNumberRV(topLeft(args[0]));
|
|
1118
1173
|
if (isError(numRV)) {
|
|
1119
1174
|
return numRV;
|
|
1120
1175
|
}
|
|
1121
1176
|
const num = numRV.value;
|
|
1122
1177
|
let decimals;
|
|
1123
1178
|
if (args.length > 1) {
|
|
1124
|
-
const decRV = toNumberRV(args[1]);
|
|
1179
|
+
const decRV = toNumberRV(topLeft(args[1]));
|
|
1125
1180
|
if (isError(decRV)) {
|
|
1126
1181
|
return decRV;
|
|
1127
1182
|
}
|
|
@@ -1146,14 +1201,14 @@ export const fnDOLLAR = args => {
|
|
|
1146
1201
|
return rvString(num < 0 ? `($${result})` : `$${result}`);
|
|
1147
1202
|
};
|
|
1148
1203
|
export const fnFIXED = args => {
|
|
1149
|
-
const numRV = toNumberRV(args[0]);
|
|
1204
|
+
const numRV = toNumberRV(topLeft(args[0]));
|
|
1150
1205
|
if (isError(numRV)) {
|
|
1151
1206
|
return numRV;
|
|
1152
1207
|
}
|
|
1153
1208
|
const num = numRV.value;
|
|
1154
1209
|
let decimals;
|
|
1155
1210
|
if (args.length > 1) {
|
|
1156
|
-
const decRV = toNumberRV(args[1]);
|
|
1211
|
+
const decRV = toNumberRV(topLeft(args[1]));
|
|
1157
1212
|
if (isError(decRV)) {
|
|
1158
1213
|
return decRV;
|
|
1159
1214
|
}
|
|
@@ -1164,7 +1219,7 @@ export const fnFIXED = args => {
|
|
|
1164
1219
|
}
|
|
1165
1220
|
let noCommas;
|
|
1166
1221
|
if (args.length > 2) {
|
|
1167
|
-
const ncRV = toBooleanRV(args[2]);
|
|
1222
|
+
const ncRV = toBooleanRV(topLeft(args[2]));
|
|
1168
1223
|
if (isError(ncRV)) {
|
|
1169
1224
|
return ncRV;
|
|
1170
1225
|
}
|
|
@@ -1195,7 +1250,7 @@ export const fnASC = args => {
|
|
|
1195
1250
|
if (err) {
|
|
1196
1251
|
return err;
|
|
1197
1252
|
}
|
|
1198
|
-
const text = toStringRV(args[0]);
|
|
1253
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1199
1254
|
return rvString(text.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xfee0)));
|
|
1200
1255
|
};
|
|
1201
1256
|
export const fnDBCS = args => {
|
|
@@ -1203,7 +1258,7 @@ export const fnDBCS = args => {
|
|
|
1203
1258
|
if (err) {
|
|
1204
1259
|
return err;
|
|
1205
1260
|
}
|
|
1206
|
-
const text = toStringRV(args[0]);
|
|
1261
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1207
1262
|
return rvString(text.replace(/[!-~]/g, ch => String.fromCharCode(ch.charCodeAt(0) + 0xfee0)));
|
|
1208
1263
|
};
|
|
1209
1264
|
export const fnJIS = args => fnDBCS(args);
|
|
@@ -1221,14 +1276,15 @@ export const fnNUMBERVALUE = args => {
|
|
|
1221
1276
|
if (e0) {
|
|
1222
1277
|
return e0;
|
|
1223
1278
|
}
|
|
1224
|
-
|
|
1279
|
+
// Implicit intersection on every text arg.
|
|
1280
|
+
let text = toStringRV(topLeft(args[0]));
|
|
1225
1281
|
let decSep = ".";
|
|
1226
1282
|
if (args.length > 1) {
|
|
1227
1283
|
const e1 = checkError(args[1]);
|
|
1228
1284
|
if (e1) {
|
|
1229
1285
|
return e1;
|
|
1230
1286
|
}
|
|
1231
|
-
decSep = toStringRV(args[1]);
|
|
1287
|
+
decSep = toStringRV(topLeft(args[1]));
|
|
1232
1288
|
}
|
|
1233
1289
|
let grpSep = ",";
|
|
1234
1290
|
if (args.length > 2) {
|
|
@@ -1236,15 +1292,19 @@ export const fnNUMBERVALUE = args => {
|
|
|
1236
1292
|
if (e2) {
|
|
1237
1293
|
return e2;
|
|
1238
1294
|
}
|
|
1239
|
-
grpSep = toStringRV(args[2]);
|
|
1295
|
+
grpSep = toStringRV(topLeft(args[2]));
|
|
1240
1296
|
}
|
|
1241
1297
|
text = text.split(grpSep).join("");
|
|
1242
1298
|
if (decSep !== ".") {
|
|
1243
1299
|
text = text.replace(decSep, ".");
|
|
1244
1300
|
}
|
|
1245
|
-
// Handle percentage
|
|
1246
|
-
|
|
1247
|
-
|
|
1301
|
+
// Handle percentage. Excel divides by 100 for EACH trailing `%`, so
|
|
1302
|
+
// `NUMBERVALUE("50%%")` = 50 / 10000 = 0.005. Previously we only
|
|
1303
|
+
// recognised the first `%` and treated `50%%` as the literal string
|
|
1304
|
+
// "50%" → NaN → #VALUE!.
|
|
1305
|
+
let pctCount = 0;
|
|
1306
|
+
while (text.endsWith("%")) {
|
|
1307
|
+
pctCount++;
|
|
1248
1308
|
text = text.slice(0, -1);
|
|
1249
1309
|
}
|
|
1250
1310
|
// `Number("")` is 0, not NaN — reject empty / whitespace-only inputs
|
|
@@ -1256,7 +1316,7 @@ export const fnNUMBERVALUE = args => {
|
|
|
1256
1316
|
if (isNaN(n)) {
|
|
1257
1317
|
return ERRORS.VALUE;
|
|
1258
1318
|
}
|
|
1259
|
-
return rvNumber(
|
|
1319
|
+
return rvNumber(pctCount > 0 ? n / Math.pow(100, pctCount) : n);
|
|
1260
1320
|
};
|
|
1261
1321
|
// ============================================================================
|
|
1262
1322
|
// Excel 365 Text Functions: TEXTBEFORE, TEXTAFTER, TEXTSPLIT
|
|
@@ -1274,7 +1334,7 @@ export const fnNUMBERVALUE = args => {
|
|
|
1274
1334
|
function parseTextBeforeAfterTail(args) {
|
|
1275
1335
|
let inst = 1;
|
|
1276
1336
|
if (args.length > 2) {
|
|
1277
|
-
const instRV = toNumberRV(args[2]);
|
|
1337
|
+
const instRV = toNumberRV(topLeft(args[2]));
|
|
1278
1338
|
if (isError(instRV)) {
|
|
1279
1339
|
return instRV;
|
|
1280
1340
|
}
|
|
@@ -1282,7 +1342,7 @@ function parseTextBeforeAfterTail(args) {
|
|
|
1282
1342
|
}
|
|
1283
1343
|
let matchMode = 0;
|
|
1284
1344
|
if (args.length > 3) {
|
|
1285
|
-
const mmRV = toNumberRV(args[3]);
|
|
1345
|
+
const mmRV = toNumberRV(topLeft(args[3]));
|
|
1286
1346
|
if (isError(mmRV)) {
|
|
1287
1347
|
return mmRV;
|
|
1288
1348
|
}
|
|
@@ -1294,7 +1354,7 @@ function parseTextBeforeAfterTail(args) {
|
|
|
1294
1354
|
}
|
|
1295
1355
|
let matchEnd = 0;
|
|
1296
1356
|
if (args.length > 4) {
|
|
1297
|
-
const meRV = toNumberRV(args[4]);
|
|
1357
|
+
const meRV = toNumberRV(topLeft(args[4]));
|
|
1298
1358
|
if (isError(meRV)) {
|
|
1299
1359
|
return meRV;
|
|
1300
1360
|
}
|
|
@@ -1316,8 +1376,8 @@ export const fnTEXTBEFORE = args => {
|
|
|
1316
1376
|
if (e1) {
|
|
1317
1377
|
return e1;
|
|
1318
1378
|
}
|
|
1319
|
-
const text = toStringRV(args[0]);
|
|
1320
|
-
const delimiter = toStringRV(args[1]);
|
|
1379
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1380
|
+
const delimiter = toStringRV(topLeft(args[1]));
|
|
1321
1381
|
const tail = parseTextBeforeAfterTail(args);
|
|
1322
1382
|
if ("kind" in tail && tail.kind === RVKind.Error) {
|
|
1323
1383
|
return tail;
|
|
@@ -1370,8 +1430,8 @@ export const fnTEXTAFTER = args => {
|
|
|
1370
1430
|
if (e1) {
|
|
1371
1431
|
return e1;
|
|
1372
1432
|
}
|
|
1373
|
-
const text = toStringRV(args[0]);
|
|
1374
|
-
const delimiter = toStringRV(args[1]);
|
|
1433
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1434
|
+
const delimiter = toStringRV(topLeft(args[1]));
|
|
1375
1435
|
const tail = parseTextBeforeAfterTail(args);
|
|
1376
1436
|
if ("kind" in tail && tail.kind === RVKind.Error) {
|
|
1377
1437
|
return tail;
|
|
@@ -1416,21 +1476,21 @@ export const fnTEXTSPLIT = args => {
|
|
|
1416
1476
|
if (e0) {
|
|
1417
1477
|
return e0;
|
|
1418
1478
|
}
|
|
1419
|
-
const text = toStringRV(args[0]);
|
|
1479
|
+
const text = toStringRV(topLeft(args[0]));
|
|
1420
1480
|
let colDelimiter = "";
|
|
1421
1481
|
if (args.length > 1) {
|
|
1422
1482
|
const e1 = checkError(args[1]);
|
|
1423
1483
|
if (e1) {
|
|
1424
1484
|
return e1;
|
|
1425
1485
|
}
|
|
1426
|
-
colDelimiter = toStringRV(args[1]);
|
|
1486
|
+
colDelimiter = toStringRV(topLeft(args[1]));
|
|
1427
1487
|
}
|
|
1428
|
-
const rowDelimiter = args.length > 2 && args[2].kind !== RVKind.Blank ? toStringRV(args[2]) : "";
|
|
1488
|
+
const rowDelimiter = args.length > 2 && args[2].kind !== RVKind.Blank ? toStringRV(topLeft(args[2])) : "";
|
|
1429
1489
|
// `ignore_empty` (4th arg, default FALSE) — when TRUE, suppress empty
|
|
1430
1490
|
// fragments produced by consecutive delimiters.
|
|
1431
1491
|
let ignoreEmpty = false;
|
|
1432
1492
|
if (args.length > 3 && args[3].kind !== RVKind.Blank) {
|
|
1433
|
-
const ieRV = toBooleanRV(args[3]);
|
|
1493
|
+
const ieRV = toBooleanRV(topLeft(args[3]));
|
|
1434
1494
|
if (isError(ieRV)) {
|
|
1435
1495
|
return ieRV;
|
|
1436
1496
|
}
|
|
@@ -1442,7 +1502,7 @@ export const fnTEXTSPLIT = args => {
|
|
|
1442
1502
|
// consistent with Excel's specification for TEXTSPLIT.
|
|
1443
1503
|
let matchMode = 0;
|
|
1444
1504
|
if (args.length > 4 && args[4].kind !== RVKind.Blank) {
|
|
1445
|
-
const mmRV = toNumberRV(args[4]);
|
|
1505
|
+
const mmRV = toNumberRV(topLeft(args[4]));
|
|
1446
1506
|
if (isError(mmRV)) {
|
|
1447
1507
|
return mmRV;
|
|
1448
1508
|
}
|
|
@@ -168,10 +168,29 @@ export function calculateFormulasImpl(workbook) {
|
|
|
168
168
|
let evalOrder = topologicalSort(graph);
|
|
169
169
|
// ── Step 6: Evaluate ──
|
|
170
170
|
const session = new EvalSession();
|
|
171
|
+
// Convert user-registered functions (keyed opaquely on WorkbookLike)
|
|
172
|
+
// into the evaluator's typed `FunctionDescriptor` shape. We take a
|
|
173
|
+
// snapshot up front so later mutations to the workbook's map during
|
|
174
|
+
// evaluation can't observe a half-built state.
|
|
175
|
+
let userFunctions;
|
|
176
|
+
if (workbook.userFunctions && workbook.userFunctions.size > 0) {
|
|
177
|
+
const adapted = new Map();
|
|
178
|
+
for (const [name, desc] of workbook.userFunctions) {
|
|
179
|
+
const upperName = name.toUpperCase();
|
|
180
|
+
adapted.set(upperName, {
|
|
181
|
+
name: upperName,
|
|
182
|
+
minArity: desc.minArity,
|
|
183
|
+
maxArity: desc.maxArity,
|
|
184
|
+
invoke: desc.invoke
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
userFunctions = adapted;
|
|
188
|
+
}
|
|
171
189
|
const ctx = {
|
|
172
190
|
snapshot,
|
|
173
191
|
compiledFormulas: compiledMap,
|
|
174
|
-
currentSheet: snapshot.worksheets[0]?.name ?? ""
|
|
192
|
+
currentSheet: snapshot.worksheets[0]?.name ?? "",
|
|
193
|
+
userFunctions
|
|
175
194
|
};
|
|
176
195
|
// Evaluate in topological order
|
|
177
196
|
evaluateInOrder(evalOrder, compiledMap, results, ctx, session);
|
|
@@ -51,6 +51,13 @@ export function buildWritebackPlan(snapshot, compiled, results, previousSpills,
|
|
|
51
51
|
// Build ghost map from previous spills (validated against snapshot)
|
|
52
52
|
const ghostMap = new Map();
|
|
53
53
|
for (const [srcKey, region] of previousSpills) {
|
|
54
|
+
// Hoist the worksheet lookup once per region — the previous code did
|
|
55
|
+
// `snapshot.worksheetsById.get(…)` inside the inner cell loop,
|
|
56
|
+
// paying the Map lookup cost `region.rows × region.cols` times.
|
|
57
|
+
const ws = snapshot.worksheetsById.get(region.worksheetId);
|
|
58
|
+
if (!ws) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
54
61
|
for (let r = 0; r < region.rows; r++) {
|
|
55
62
|
for (let c = 0; c < region.cols; c++) {
|
|
56
63
|
if (r === 0 && c === 0) {
|
|
@@ -60,12 +67,9 @@ export function buildWritebackPlan(snapshot, compiled, results, previousSpills,
|
|
|
60
67
|
const targetCol = region.sourceCol + c;
|
|
61
68
|
const targetKey = spillCellKeyFromId(region.worksheetId, targetRow, targetCol);
|
|
62
69
|
// Validate ghost cell is still unmodified
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
if (isGhostUnmodified(cell, targetKey, previousGhosts)) {
|
|
67
|
-
ghostMap.set(targetKey, srcKey);
|
|
68
|
-
}
|
|
70
|
+
const cell = ws.cells.get(snapshotCellKey(targetRow, targetCol));
|
|
71
|
+
if (isGhostUnmodified(cell, targetKey, previousGhosts)) {
|
|
72
|
+
ghostMap.set(targetKey, srcKey);
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -159,6 +159,21 @@ export interface WorkbookLike {
|
|
|
159
159
|
properties?: {
|
|
160
160
|
date1904?: boolean;
|
|
161
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* User-registered custom functions exposed to the formula engine.
|
|
164
|
+
* Keys are uppercase canonical names; values are arity + invoke
|
|
165
|
+
* descriptors. When the evaluator encounters a call it consults this
|
|
166
|
+
* map before the global built-in registry, so users can shadow a
|
|
167
|
+
* built-in (e.g. replace `IRR` with a domain-specific variant) or
|
|
168
|
+
* add entirely new names.
|
|
169
|
+
*/
|
|
170
|
+
userFunctions?: ReadonlyMap<string, {
|
|
171
|
+
minArity: number;
|
|
172
|
+
maxArity: number;
|
|
173
|
+
invoke: (args: unknown[]) => unknown;
|
|
174
|
+
/** Reserved for future volatile-function wiring. */
|
|
175
|
+
volatile?: boolean;
|
|
176
|
+
}>;
|
|
162
177
|
}
|
|
163
178
|
/**
|
|
164
179
|
* Tracks a spill region: the source formula cell and the range of cells it
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { BoundExpr } from "../compile/bound-ast.js";
|
|
9
9
|
import type { CompiledFormula } from "../compile/compiled-formula.js";
|
|
10
10
|
import type { WorkbookSnapshot } from "../integration/workbook-snapshot.js";
|
|
11
|
+
import type { FunctionDescriptor } from "./function-registry.js";
|
|
11
12
|
import type { RuntimeValue } from "./values.js";
|
|
12
13
|
/**
|
|
13
14
|
* Cached formula evaluation result with both scalar and raw forms.
|
|
@@ -114,6 +115,13 @@ export interface EvalContext {
|
|
|
114
115
|
};
|
|
115
116
|
/** Local variable bindings from LET expressions. */
|
|
116
117
|
localBindings?: Map<string, RuntimeValue>;
|
|
118
|
+
/**
|
|
119
|
+
* User-registered functions that take precedence over the built-in
|
|
120
|
+
* registry. Lookup happens in `evaluateCall` — a matching
|
|
121
|
+
* descriptor here shadows any built-in of the same name. Keys are
|
|
122
|
+
* canonical uppercase names (prefix-stripped on register).
|
|
123
|
+
*/
|
|
124
|
+
readonly userFunctions?: ReadonlyMap<string, FunctionDescriptor>;
|
|
117
125
|
}
|
|
118
126
|
/**
|
|
119
127
|
* Evaluate a BoundExpr to produce a RuntimeValue.
|