@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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* are handled directly by the evaluator's special-form dispatch.
|
|
10
10
|
*/
|
|
11
11
|
import { stripFunctionPrefix } from "../syntax/token-types.js";
|
|
12
|
-
import { RVKind, BLANK, ERRORS, rvBoolean, rvNumber, rvString, topLeft } from "./values.js";
|
|
12
|
+
import { RVKind, BLANK, ERRORS, rvBoolean, rvNumber, rvString, toNumberRV, toStringRV, topLeft } from "./values.js";
|
|
13
13
|
// ============================================================================
|
|
14
14
|
// Registry
|
|
15
15
|
// ============================================================================
|
|
@@ -34,8 +34,17 @@ export function registerFunction(desc) {
|
|
|
34
34
|
* `_XLFN._XLWS.` prefixed variants by stripping the prefix before lookup
|
|
35
35
|
* (a no-op for plain names, so plain lookups also go through a single
|
|
36
36
|
* Map.get call — avoiding the double-lookup pattern used previously).
|
|
37
|
+
*
|
|
38
|
+
* Fast-path: the overwhelming majority of lookups use plain names
|
|
39
|
+
* (`SUM`, `IF`, `VLOOKUP`, …). Checking the prefix sentinel byte up
|
|
40
|
+
* front lets those callers skip the `slice`/`startsWith` machinery in
|
|
41
|
+
* `stripFunctionPrefix` entirely.
|
|
37
42
|
*/
|
|
38
43
|
export function lookupFunction(name) {
|
|
44
|
+
// Plain names don't start with `_` — skip the prefix-strip call.
|
|
45
|
+
if (name.length === 0 || name.charCodeAt(0) !== 95 /* `_` */) {
|
|
46
|
+
return registryMap.get(name);
|
|
47
|
+
}
|
|
39
48
|
return registryMap.get(stripFunctionPrefix(name));
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
@@ -105,20 +114,26 @@ function registerNativeInformationAndLogical() {
|
|
|
105
114
|
if (v.kind === RVKind.Error) {
|
|
106
115
|
return v;
|
|
107
116
|
}
|
|
108
|
-
|
|
117
|
+
// Excel coerces numeric strings and booleans for ISEVEN / ISODD
|
|
118
|
+
// (e.g. `ISEVEN("3")` → FALSE, `ISODD(TRUE)` → TRUE). Only genuine
|
|
119
|
+
// non-numeric text falls through to #VALUE!. Previously we rejected
|
|
120
|
+
// every non-Number kind outright.
|
|
121
|
+
const n = toNumberRV(v);
|
|
122
|
+
if (n.kind === RVKind.Error) {
|
|
109
123
|
return ERRORS.VALUE;
|
|
110
124
|
}
|
|
111
|
-
return rvBoolean(Math.floor(Math.abs(
|
|
125
|
+
return rvBoolean(Math.floor(Math.abs(n.value)) % 2 === 0);
|
|
112
126
|
});
|
|
113
127
|
defineEager("ISODD", 1, 1, args => {
|
|
114
128
|
const v = scalar(args);
|
|
115
129
|
if (v.kind === RVKind.Error) {
|
|
116
130
|
return v;
|
|
117
131
|
}
|
|
118
|
-
|
|
132
|
+
const n = toNumberRV(v);
|
|
133
|
+
if (n.kind === RVKind.Error) {
|
|
119
134
|
return ERRORS.VALUE;
|
|
120
135
|
}
|
|
121
|
-
return rvBoolean(Math.floor(Math.abs(
|
|
136
|
+
return rvBoolean(Math.floor(Math.abs(n.value)) % 2 === 1);
|
|
122
137
|
});
|
|
123
138
|
defineEager("N", 1, 1, args => {
|
|
124
139
|
const v = scalar(args);
|
|
@@ -169,6 +184,11 @@ function registerNativeInformationAndLogical() {
|
|
|
169
184
|
return map[v.code] !== undefined ? rvNumber(map[v.code]) : ERRORS.NA;
|
|
170
185
|
});
|
|
171
186
|
defineEager("NA", 0, 0, () => ERRORS.NA);
|
|
187
|
+
// TRUE() and FALSE() — Excel accepts both the literal and the
|
|
188
|
+
// zero-arg function form. The tokenizer routes `TRUE(` to a Function
|
|
189
|
+
// token, so we need to register these so the call binds.
|
|
190
|
+
defineEager("TRUE", 0, 0, () => rvBoolean(true));
|
|
191
|
+
defineEager("FALSE", 0, 0, () => rvBoolean(false));
|
|
172
192
|
// ── Stubs — limited implementations for functions that need runtime context ──
|
|
173
193
|
// INFO returns a handful of environment-describing strings. We implement
|
|
174
194
|
// the subset that's meaningful in a headless engine: `"release"` (engine
|
|
@@ -180,7 +200,10 @@ function registerNativeInformationAndLogical() {
|
|
|
180
200
|
if (args.length === 0) {
|
|
181
201
|
return ERRORS.NA;
|
|
182
202
|
}
|
|
183
|
-
|
|
203
|
+
// Implicit intersection — without topLeft, passing an array would
|
|
204
|
+
// route through the default `.value = ""` branch and silently
|
|
205
|
+
// surface #VALUE! instead of using the first cell.
|
|
206
|
+
const t = topLeft(args[0]);
|
|
184
207
|
if (t.kind === RVKind.Error) {
|
|
185
208
|
return t;
|
|
186
209
|
}
|
|
@@ -239,7 +262,10 @@ function registerNativeInformationAndLogical() {
|
|
|
239
262
|
if (url.kind === RVKind.Error) {
|
|
240
263
|
return url;
|
|
241
264
|
}
|
|
242
|
-
|
|
265
|
+
// Previously `String(url)` stringified a RuntimeValue object to
|
|
266
|
+
// the literal `"[object Object]"`. Route through `toStringRV`
|
|
267
|
+
// so numbers / booleans / blanks produce the expected text.
|
|
268
|
+
return rvString(toStringRV(url));
|
|
243
269
|
}
|
|
244
270
|
if (display.kind === RVKind.String) {
|
|
245
271
|
return display;
|
|
@@ -264,6 +290,21 @@ function registerNativeInformationAndLogical() {
|
|
|
264
290
|
if (v.kind === RVKind.Number) {
|
|
265
291
|
return rvBoolean(v.value === 0);
|
|
266
292
|
}
|
|
293
|
+
// Excel accepts "TRUE" / "FALSE" strings (case-insensitive) and
|
|
294
|
+
// Blank cells (treated as FALSE). Previously any non-boolean,
|
|
295
|
+
// non-numeric kind fell through to #VALUE!.
|
|
296
|
+
if (v.kind === RVKind.Blank) {
|
|
297
|
+
return rvBoolean(true);
|
|
298
|
+
}
|
|
299
|
+
if (v.kind === RVKind.String) {
|
|
300
|
+
const upper = v.value.toUpperCase();
|
|
301
|
+
if (upper === "TRUE") {
|
|
302
|
+
return rvBoolean(false);
|
|
303
|
+
}
|
|
304
|
+
if (upper === "FALSE") {
|
|
305
|
+
return rvBoolean(true);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
267
308
|
return ERRORS.VALUE;
|
|
268
309
|
});
|
|
269
310
|
defineEager("AND", 1, 255, args => boolAggregate(args, true, (cur, val) => cur && val));
|
|
@@ -379,6 +420,11 @@ function registerNativeTextFunctions() {
|
|
|
379
420
|
defineEager("PROPER", 1, 1, fnPROPER);
|
|
380
421
|
defineEager("SUBSTITUTE", 3, 4, fnSUBSTITUTE);
|
|
381
422
|
defineEager("REPLACE", 4, 4, fnREPLACE);
|
|
423
|
+
// REPLACEB is REPLACE's double-byte alias. In non-DBCS locales Excel
|
|
424
|
+
// treats them identically, so aliasing to the same implementation
|
|
425
|
+
// matches behaviour without duplicating logic (matches the existing
|
|
426
|
+
// LEFTB / RIGHTB / MIDB / LENB / FINDB / SEARCHB wiring below).
|
|
427
|
+
defineEager("REPLACEB", 4, 4, fnREPLACE);
|
|
382
428
|
defineEager("FIND", 2, 3, fnFIND);
|
|
383
429
|
defineEager("FINDB", 2, 3, fnFIND);
|
|
384
430
|
defineEager("SEARCH", 2, 3, fnSEARCH);
|
|
@@ -674,7 +720,7 @@ function registerNativeStatisticalFunctions() {
|
|
|
674
720
|
// ============================================================================
|
|
675
721
|
// Native Math Functions
|
|
676
722
|
// ============================================================================
|
|
677
|
-
import { fnSUM, fnAVERAGE, fnMIN, fnMAX, fnCOUNT, fnCOUNTA, fnCOUNTBLANK, fnPRODUCT, fnSUMPRODUCT, fnABS, fnCEILING, fnFLOOR, fnINT, fnMOD, fnPOWER, fnROUND, fnROUNDDOWN, fnROUNDUP, fnSQRT, fnSQRTPI, fnLN, fnLOG, fnLOG10, fnEXP, fnPI, fnRAND, fnRANDBETWEEN, fnSIGN, fnTRUNC, fnSUMSQ, fnGCD, fnLCM, fnEVEN, fnODD, fnMROUND, fnQUOTIENT, fnBASE, fnDECIMAL, fnROMAN, fnARABIC, fnDEGREES, fnRADIANS, fnSUMX2MY2, fnSUMX2PY2, fnSUMXMY2, fnMULTINOMIAL, fnFACT as fnMathFACT, fnFACTDOUBLE as fnMathFACTDOUBLE, fnCOMBIN as fnMathCOMBIN, fnCOMBINA as fnMathCOMBINA, fnPERMUT as fnMathPERMUT, fnSIN, fnCOS, fnTAN, fnASIN, fnACOS, fnATAN, fnATAN2, fnSINH, fnCOSH, fnTANH, fnASINH, fnACOSH, fnATANH, fnSEC, fnCSC, fnCOT, fnSECH, fnCSCH, fnCOTH, fnACOT, fnACOTH, fnMMULT, fnMDETERM, fnMINVERSE, fnMUNIT, fnSERIESSUM } from "../functions/math.js";
|
|
723
|
+
import { fnSUM, fnAVERAGE, fnMIN, fnMAX, fnCOUNT, fnCOUNTA, fnCOUNTBLANK, fnPRODUCT, fnSUMPRODUCT, fnABS, fnCEILING, fnCEILING_MATH, fnCEILING_PRECISE, fnFLOOR, fnFLOOR_MATH, fnFLOOR_PRECISE, fnINT, fnMOD, fnPOWER, fnROUND, fnROUNDDOWN, fnROUNDUP, fnSQRT, fnSQRTPI, fnLN, fnLOG, fnLOG10, fnEXP, fnPI, fnRAND, fnRANDBETWEEN, fnSIGN, fnTRUNC, fnSUMSQ, fnGCD, fnLCM, fnEVEN, fnODD, fnMROUND, fnQUOTIENT, fnBASE, fnDECIMAL, fnROMAN, fnARABIC, fnDEGREES, fnRADIANS, fnSUMX2MY2, fnSUMX2PY2, fnSUMXMY2, fnMULTINOMIAL, fnFACT as fnMathFACT, fnFACTDOUBLE as fnMathFACTDOUBLE, fnCOMBIN as fnMathCOMBIN, fnCOMBINA as fnMathCOMBINA, fnPERMUT as fnMathPERMUT, fnSIN, fnCOS, fnTAN, fnASIN, fnACOS, fnATAN, fnATAN2, fnSINH, fnCOSH, fnTANH, fnASINH, fnACOSH, fnATANH, fnSEC, fnCSC, fnCOT, fnSECH, fnCSCH, fnCOTH, fnACOT, fnACOTH, fnMMULT, fnMDETERM, fnMINVERSE, fnMUNIT, fnSERIESSUM } from "../functions/math.js";
|
|
678
724
|
function registerNativeMathFunctions() {
|
|
679
725
|
defineEager("SUM", 1, 255, fnSUM);
|
|
680
726
|
defineEager("AVERAGE", 1, 255, fnAVERAGE);
|
|
@@ -692,12 +738,12 @@ function registerNativeMathFunctions() {
|
|
|
692
738
|
defineEager("SERIESSUM", 4, 4, fnSERIESSUM);
|
|
693
739
|
defineEager("ABS", 1, 1, fnABS);
|
|
694
740
|
defineEager("CEILING", 2, 2, fnCEILING);
|
|
695
|
-
defineEager("CEILING.MATH", 1, 3,
|
|
696
|
-
defineEager("CEILING.PRECISE", 1, 2,
|
|
697
|
-
defineEager("ISO.CEILING", 1, 2,
|
|
741
|
+
defineEager("CEILING.MATH", 1, 3, fnCEILING_MATH);
|
|
742
|
+
defineEager("CEILING.PRECISE", 1, 2, fnCEILING_PRECISE);
|
|
743
|
+
defineEager("ISO.CEILING", 1, 2, fnCEILING_PRECISE);
|
|
698
744
|
defineEager("FLOOR", 2, 2, fnFLOOR);
|
|
699
|
-
defineEager("FLOOR.MATH", 1, 3,
|
|
700
|
-
defineEager("FLOOR.PRECISE", 1, 2,
|
|
745
|
+
defineEager("FLOOR.MATH", 1, 3, fnFLOOR_MATH);
|
|
746
|
+
defineEager("FLOOR.PRECISE", 1, 2, fnFLOOR_PRECISE);
|
|
701
747
|
defineEager("INT", 1, 1, fnINT);
|
|
702
748
|
defineEager("MOD", 2, 2, fnMOD);
|
|
703
749
|
defineEager("POWER", 2, 2, fnPOWER);
|
|
@@ -112,10 +112,28 @@ export function rvArray(rows, originRow, originCol, subtotalMask, hiddenRowMask)
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
return buildArrayValue(normalisedRows, height, width, originRow, originCol, subtotalMask, hiddenRowMask);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fast-path rectangular ArrayValue constructor.
|
|
119
|
+
*
|
|
120
|
+
* Callers that have already produced strictly-rectangular `rows` (every
|
|
121
|
+
* row is the same length — the length they explicitly `new Array(width)`
|
|
122
|
+
* allocated) can skip the two-pass width-scan + padding loop in
|
|
123
|
+
* `rvArray`. Examples: `buildRangeArray`, `broadcastBinaryOp`,
|
|
124
|
+
* `evaluateArrayLiteral`, `TRANSPOSE` — they all know `width` up front.
|
|
125
|
+
*
|
|
126
|
+
* Rows MUST be rectangular; passing ragged data will silently surface as
|
|
127
|
+
* `undefined` cells downstream.
|
|
128
|
+
*/
|
|
129
|
+
export function rvArrayRect(rows, height, width, originRow, originCol, subtotalMask, hiddenRowMask) {
|
|
130
|
+
return buildArrayValue(rows, height, width, originRow, originCol, subtotalMask, hiddenRowMask);
|
|
131
|
+
}
|
|
132
|
+
function buildArrayValue(rows, height, width, originRow, originCol, subtotalMask, hiddenRowMask) {
|
|
115
133
|
return originRow !== undefined
|
|
116
134
|
? {
|
|
117
135
|
kind: RVKind.Array,
|
|
118
|
-
rows
|
|
136
|
+
rows,
|
|
119
137
|
height,
|
|
120
138
|
width,
|
|
121
139
|
originRow,
|
|
@@ -125,7 +143,7 @@ export function rvArray(rows, originRow, originCol, subtotalMask, hiddenRowMask)
|
|
|
125
143
|
}
|
|
126
144
|
: {
|
|
127
145
|
kind: RVKind.Array,
|
|
128
|
-
rows
|
|
146
|
+
rows,
|
|
129
147
|
height,
|
|
130
148
|
width,
|
|
131
149
|
...(subtotalMask ? { subtotalMask } : {}),
|
|
@@ -25,4 +25,5 @@ export var NodeType;
|
|
|
25
25
|
NodeType[NodeType["RowRangeRef"] = 14] = "RowRangeRef";
|
|
26
26
|
NodeType[NodeType["StructuredRef"] = 15] = "StructuredRef";
|
|
27
27
|
NodeType[NodeType["Missing"] = 16] = "Missing";
|
|
28
|
+
NodeType[NodeType["UnionRef"] = 17] = "UnionRef";
|
|
28
29
|
})(NodeType || (NodeType = {}));
|
|
@@ -22,9 +22,15 @@ function prefixBindingPower(op) {
|
|
|
22
22
|
switch (op) {
|
|
23
23
|
case "+":
|
|
24
24
|
case "-":
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
25
|
+
// Excel's unary `-` binds TIGHTER than `^` — unique among
|
|
26
|
+
// spreadsheets and most programming languages. Microsoft's
|
|
27
|
+
// published precedence table places "Negation (as in –1)" at
|
|
28
|
+
// rank 1 (highest) and "Exponentiation" at rank 4.
|
|
29
|
+
// =-2^2 → (-2)^2 → 4 (NOT -(2^2) = -4)
|
|
30
|
+
// =-2^3 → (-2)^3 → -8
|
|
31
|
+
// Previously we used 55 which routed via `^` (60/61) and produced
|
|
32
|
+
// `-(2^3)` — matching most languages but not Excel.
|
|
33
|
+
return 70;
|
|
28
34
|
default:
|
|
29
35
|
return 0;
|
|
30
36
|
}
|
|
@@ -49,7 +55,11 @@ function infixBindingPower(op) {
|
|
|
49
55
|
case "/":
|
|
50
56
|
return [40, 41];
|
|
51
57
|
case "^":
|
|
52
|
-
|
|
58
|
+
// Excel is unusual: `^` is LEFT-associative (not right-associative
|
|
59
|
+
// like the math convention). `=2^3^2` evaluates to `(2^3)^2 = 64`,
|
|
60
|
+
// not `2^(3^2) = 512`. Using right-associative precedence silently
|
|
61
|
+
// diverged from Excel for any stacked exponent.
|
|
62
|
+
return [60, 61];
|
|
53
63
|
// Intersection operator — whitespace between two refs. In Excel
|
|
54
64
|
// precedence this sits between `:` (range, already handled at the
|
|
55
65
|
// tokenizer level) and unary +/-. Left-associative, binds tighter
|
|
@@ -246,12 +256,24 @@ class Parser {
|
|
|
246
256
|
this.next();
|
|
247
257
|
return { type: NodeType.Error, value: t.value };
|
|
248
258
|
}
|
|
249
|
-
// Parenthesized expression
|
|
259
|
+
// Parenthesized expression — also the syntactic entry point for
|
|
260
|
+
// reference unions: `(A1:B2, D4:E5)` is a multi-area reference
|
|
261
|
+
// that `INDEX(..., area_num)` can index into. A single expression
|
|
262
|
+
// inside parens is just an expression group (no UnionRef wrapper).
|
|
250
263
|
if (t.type === TokenType.OpenParen) {
|
|
251
264
|
this.next();
|
|
252
|
-
const
|
|
265
|
+
const first = this.parseExpr(0);
|
|
266
|
+
if (this.peek()?.type === TokenType.Comma) {
|
|
267
|
+
const areas = [first];
|
|
268
|
+
while (this.peek()?.type === TokenType.Comma) {
|
|
269
|
+
this.next(); // consume ','
|
|
270
|
+
areas.push(this.parseExpr(0));
|
|
271
|
+
}
|
|
272
|
+
this.expect(TokenType.CloseParen);
|
|
273
|
+
return { type: NodeType.UnionRef, areas };
|
|
274
|
+
}
|
|
253
275
|
this.expect(TokenType.CloseParen);
|
|
254
|
-
return
|
|
276
|
+
return first;
|
|
255
277
|
}
|
|
256
278
|
// Array constant: {1,2;3,4}
|
|
257
279
|
if (t.type === TokenType.OpenBrace) {
|
|
@@ -47,8 +47,17 @@ export var TokenType;
|
|
|
47
47
|
* The input may or may not already be uppercased — this helper does not
|
|
48
48
|
* alter case; callers that compare against an uppercase table should
|
|
49
49
|
* uppercase first (or compare case-insensitively).
|
|
50
|
+
*
|
|
51
|
+
* Fast path: plain names (99%+ of call sites) start with a letter, so
|
|
52
|
+
* checking the first code unit before the `startsWith` machinery lets
|
|
53
|
+
* those lookups skip the allocation.
|
|
50
54
|
*/
|
|
51
55
|
export function stripFunctionPrefix(name) {
|
|
56
|
+
// `_` is code unit 95 — ASCII letters are 65..90 / 97..122. The XLFN
|
|
57
|
+
// prefix is the only legitimate name shape that begins with `_`.
|
|
58
|
+
if (name.length === 0 || name.charCodeAt(0) !== 95) {
|
|
59
|
+
return name;
|
|
60
|
+
}
|
|
52
61
|
if (name.startsWith("_XLFN._XLWS.")) {
|
|
53
62
|
return name.slice(12);
|
|
54
63
|
}
|
|
@@ -442,26 +442,56 @@ export function tokenize(formula) {
|
|
|
442
442
|
// String literals
|
|
443
443
|
if (ch === '"') {
|
|
444
444
|
i++; // skip opening quote
|
|
445
|
-
|
|
445
|
+
// Fast path: walk forward looking for a closing quote. In the common
|
|
446
|
+
// case (no escaped quotes) we emit a single `slice` rather than
|
|
447
|
+
// growing a string byte by byte. The slow path falls back to the
|
|
448
|
+
// explicit escape-aware concat when we actually see `""`.
|
|
449
|
+
const start = i;
|
|
446
450
|
let closed = false;
|
|
451
|
+
let firstEscape = -1;
|
|
447
452
|
while (i < len) {
|
|
448
453
|
if (formula[i] === '"') {
|
|
449
454
|
if (i + 1 < len && formula[i + 1] === '"') {
|
|
450
|
-
|
|
451
|
-
str += '"';
|
|
452
|
-
i += 2;
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
i++; // skip closing quote
|
|
456
|
-
closed = true;
|
|
455
|
+
firstEscape = i;
|
|
457
456
|
break;
|
|
458
457
|
}
|
|
458
|
+
closed = true;
|
|
459
|
+
break;
|
|
459
460
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
i++;
|
|
462
|
+
}
|
|
463
|
+
let str;
|
|
464
|
+
if (firstEscape !== -1) {
|
|
465
|
+
// At least one escaped quote — use the classic byte-by-byte loop
|
|
466
|
+
// starting from the first escape so the prefix can still come
|
|
467
|
+
// from `slice`.
|
|
468
|
+
str = formula.slice(start, firstEscape);
|
|
469
|
+
i = firstEscape;
|
|
470
|
+
while (i < len) {
|
|
471
|
+
if (formula[i] === '"') {
|
|
472
|
+
if (i + 1 < len && formula[i + 1] === '"') {
|
|
473
|
+
str += '"';
|
|
474
|
+
i += 2;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
i++;
|
|
478
|
+
closed = true;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
str += formula[i];
|
|
484
|
+
i++;
|
|
485
|
+
}
|
|
463
486
|
}
|
|
464
487
|
}
|
|
488
|
+
else if (closed) {
|
|
489
|
+
str = formula.slice(start, i);
|
|
490
|
+
i++; // consume closing quote
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
str = formula.slice(start, i);
|
|
494
|
+
}
|
|
465
495
|
if (!closed) {
|
|
466
496
|
// Unterminated string literal — reject at tokenize time so we
|
|
467
497
|
// never hand the parser a truncated value that could alias to a
|
|
@@ -723,21 +753,48 @@ export function tokenize(formula) {
|
|
|
723
753
|
// Quoted sheet name: 'Sheet Name'! or 3D ref 'Sheet1:Sheet3'!
|
|
724
754
|
if (ch === "'") {
|
|
725
755
|
i++; // skip opening quote
|
|
726
|
-
|
|
756
|
+
// Fast path: scan to the first `'` — most sheet names don't contain
|
|
757
|
+
// an escaped quote, so we can slice the prefix verbatim instead of
|
|
758
|
+
// growing a string byte-by-byte.
|
|
759
|
+
const start = i;
|
|
760
|
+
let firstEscape = -1;
|
|
727
761
|
while (i < len) {
|
|
728
762
|
if (formula[i] === "'") {
|
|
729
763
|
if (i + 1 < len && formula[i + 1] === "'") {
|
|
730
|
-
|
|
731
|
-
|
|
764
|
+
firstEscape = i;
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
i++;
|
|
770
|
+
}
|
|
771
|
+
let sheetName;
|
|
772
|
+
if (firstEscape !== -1) {
|
|
773
|
+
sheetName = formula.slice(start, firstEscape);
|
|
774
|
+
i = firstEscape;
|
|
775
|
+
// Slow path from the first escape: consume `''` pairs and
|
|
776
|
+
// literal characters until the terminating single `'`.
|
|
777
|
+
while (i < len) {
|
|
778
|
+
if (formula[i] === "'") {
|
|
779
|
+
if (i + 1 < len && formula[i + 1] === "'") {
|
|
780
|
+
sheetName += "'";
|
|
781
|
+
i += 2;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
i++;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
732
787
|
}
|
|
733
788
|
else {
|
|
734
|
-
|
|
735
|
-
|
|
789
|
+
sheetName += formula[i];
|
|
790
|
+
i++;
|
|
736
791
|
}
|
|
737
792
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
sheetName = formula.slice(start, i);
|
|
796
|
+
if (i < len && formula[i] === "'") {
|
|
797
|
+
i++; // consume closing quote
|
|
741
798
|
}
|
|
742
799
|
}
|
|
743
800
|
// Expect ! after
|