@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.
Files changed (109) hide show
  1. package/dist/browser/index.d.ts +1 -0
  2. package/dist/browser/index.js +2 -0
  3. package/dist/browser/modules/excel/cell.d.ts +18 -0
  4. package/dist/browser/modules/excel/cell.js +21 -0
  5. package/dist/browser/modules/excel/utils/cell-format.js +85 -13
  6. package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
  7. package/dist/browser/modules/excel/workbook.browser.js +49 -0
  8. package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
  9. package/dist/browser/modules/formula/compile/binder.js +48 -6
  10. package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
  11. package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
  12. package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
  13. package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
  14. package/dist/browser/modules/formula/functions/_shared.js +47 -0
  15. package/dist/browser/modules/formula/functions/conditional.js +103 -22
  16. package/dist/browser/modules/formula/functions/date.js +105 -23
  17. package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
  18. package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
  19. package/dist/browser/modules/formula/functions/engineering.js +103 -151
  20. package/dist/browser/modules/formula/functions/financial.js +210 -184
  21. package/dist/browser/modules/formula/functions/lookup.js +224 -157
  22. package/dist/browser/modules/formula/functions/math.d.ts +26 -0
  23. package/dist/browser/modules/formula/functions/math.js +249 -69
  24. package/dist/browser/modules/formula/functions/statistical.js +221 -171
  25. package/dist/browser/modules/formula/functions/text.js +112 -52
  26. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
  27. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
  28. package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
  29. package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
  30. package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
  31. package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
  32. package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
  33. package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
  34. package/dist/browser/modules/formula/runtime/values.js +20 -2
  35. package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
  36. package/dist/browser/modules/formula/syntax/ast.js +1 -0
  37. package/dist/browser/modules/formula/syntax/parser.js +29 -7
  38. package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
  39. package/dist/browser/modules/formula/syntax/token-types.js +9 -0
  40. package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
  41. package/dist/cjs/index.js +7 -2
  42. package/dist/cjs/modules/excel/cell.js +21 -0
  43. package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
  44. package/dist/cjs/modules/excel/workbook.browser.js +49 -0
  45. package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
  46. package/dist/cjs/modules/formula/compile/binder.js +48 -6
  47. package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
  48. package/dist/cjs/modules/formula/functions/_shared.js +48 -0
  49. package/dist/cjs/modules/formula/functions/conditional.js +103 -22
  50. package/dist/cjs/modules/formula/functions/date.js +104 -22
  51. package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
  52. package/dist/cjs/modules/formula/functions/engineering.js +109 -157
  53. package/dist/cjs/modules/formula/functions/financial.js +209 -183
  54. package/dist/cjs/modules/formula/functions/lookup.js +224 -157
  55. package/dist/cjs/modules/formula/functions/math.js +254 -70
  56. package/dist/cjs/modules/formula/functions/statistical.js +222 -172
  57. package/dist/cjs/modules/formula/functions/text.js +112 -52
  58. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
  59. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
  60. package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
  61. package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
  62. package/dist/cjs/modules/formula/runtime/values.js +21 -2
  63. package/dist/cjs/modules/formula/syntax/parser.js +29 -7
  64. package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
  65. package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
  66. package/dist/esm/index.js +2 -0
  67. package/dist/esm/modules/excel/cell.js +21 -0
  68. package/dist/esm/modules/excel/utils/cell-format.js +85 -13
  69. package/dist/esm/modules/excel/workbook.browser.js +49 -0
  70. package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
  71. package/dist/esm/modules/formula/compile/binder.js +48 -6
  72. package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
  73. package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
  74. package/dist/esm/modules/formula/functions/_shared.js +47 -0
  75. package/dist/esm/modules/formula/functions/conditional.js +103 -22
  76. package/dist/esm/modules/formula/functions/date.js +105 -23
  77. package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
  78. package/dist/esm/modules/formula/functions/engineering.js +103 -151
  79. package/dist/esm/modules/formula/functions/financial.js +210 -184
  80. package/dist/esm/modules/formula/functions/lookup.js +224 -157
  81. package/dist/esm/modules/formula/functions/math.js +249 -69
  82. package/dist/esm/modules/formula/functions/statistical.js +221 -171
  83. package/dist/esm/modules/formula/functions/text.js +112 -52
  84. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
  85. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
  86. package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
  87. package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
  88. package/dist/esm/modules/formula/runtime/values.js +20 -2
  89. package/dist/esm/modules/formula/syntax/ast.js +1 -0
  90. package/dist/esm/modules/formula/syntax/parser.js +29 -7
  91. package/dist/esm/modules/formula/syntax/token-types.js +9 -0
  92. package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
  93. package/dist/iife/excelts.iife.js +1502 -1379
  94. package/dist/iife/excelts.iife.js.map +1 -1
  95. package/dist/iife/excelts.iife.min.js +26 -26
  96. package/dist/types/index.d.ts +1 -0
  97. package/dist/types/modules/excel/cell.d.ts +18 -0
  98. package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
  99. package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
  100. package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
  101. package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
  102. package/dist/types/modules/formula/functions/math.d.ts +26 -0
  103. package/dist/types/modules/formula/materialize/types.d.ts +15 -0
  104. package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
  105. package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
  106. package/dist/types/modules/formula/runtime/values.d.ts +13 -0
  107. package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
  108. package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
  109. 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
- if (v.kind !== RVKind.Number) {
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(v.value)) % 2 === 0);
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
- if (v.kind !== RVKind.Number) {
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(v.value)) % 2 === 1);
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
- const t = args[0];
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
- return rvString(url.kind === RVKind.String ? url.value : String(url));
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, fnCEILING);
696
- defineEager("CEILING.PRECISE", 1, 2, fnCEILING);
697
- defineEager("ISO.CEILING", 1, 2, fnCEILING);
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, fnFLOOR);
700
- defineEager("FLOOR.PRECISE", 1, 2, fnFLOOR);
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: normalisedRows,
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: normalisedRows,
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
- // Must be lower than ^ (60/61) so that -2^3 parses as -(2^3), not (-2)^3.
26
- // Excel: -2^2 = -4, not 4.
27
- return 55;
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
- return [61, 60]; // right-associative
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 expr = this.parseExpr(0);
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 expr;
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
- let str = "";
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
- // Escaped quote
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
- else {
461
- str += formula[i];
462
- i++;
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
- let sheetName = "";
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
- sheetName += "'";
731
- i += 2;
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
- i++; // skip closing quote
735
- break;
789
+ sheetName += formula[i];
790
+ i++;
736
791
  }
737
792
  }
738
- else {
739
- sheetName += formula[i];
740
- i++;
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