@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
@@ -38,6 +38,11 @@ export declare function registerFunction(desc: FunctionDescriptor): void;
38
38
  * `_XLFN._XLWS.` prefixed variants by stripping the prefix before lookup
39
39
  * (a no-op for plain names, so plain lookups also go through a single
40
40
  * Map.get call — avoiding the double-lookup pattern used previously).
41
+ *
42
+ * Fast-path: the overwhelming majority of lookups use plain names
43
+ * (`SUM`, `IF`, `VLOOKUP`, …). Checking the prefix sentinel byte up
44
+ * front lets those callers skip the `slice`/`startsWith` machinery in
45
+ * `stripFunctionPrefix` entirely.
41
46
  */
42
47
  export declare function lookupFunction(name: string): FunctionDescriptor | undefined;
43
48
  /**
@@ -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);
@@ -156,6 +156,19 @@ export declare function rvString(value: string): StringValue;
156
156
  export declare function rvBoolean(value: boolean): BooleanValue;
157
157
  export declare function rvError(code: ErrorCode): ErrorValue;
158
158
  export declare function rvArray(rows: ScalarValue[][], originRow?: number, originCol?: number, subtotalMask?: readonly (readonly boolean[])[], hiddenRowMask?: readonly boolean[]): ArrayValue;
159
+ /**
160
+ * Fast-path rectangular ArrayValue constructor.
161
+ *
162
+ * Callers that have already produced strictly-rectangular `rows` (every
163
+ * row is the same length — the length they explicitly `new Array(width)`
164
+ * allocated) can skip the two-pass width-scan + padding loop in
165
+ * `rvArray`. Examples: `buildRangeArray`, `broadcastBinaryOp`,
166
+ * `evaluateArrayLiteral`, `TRANSPOSE` — they all know `width` up front.
167
+ *
168
+ * Rows MUST be rectangular; passing ragged data will silently surface as
169
+ * `undefined` cells downstream.
170
+ */
171
+ export declare function rvArrayRect(rows: ScalarValue[][], height: number, width: number, originRow?: number, originCol?: number, subtotalMask?: readonly (readonly boolean[])[], hiddenRowMask?: readonly boolean[]): ArrayValue;
159
172
  export declare function rvRef(sheet: string, top: number, left: number, bottom: number, right: number): ReferenceValue;
160
173
  export declare function rvCellRef(sheet: string, row: number, col: number): ReferenceValue;
161
174
  export declare function rvLambda(params: string[], body: BoundExpr, closureBindings?: ReadonlyMap<string, RuntimeValue>): LambdaValue;
@@ -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 } : {}),
@@ -20,7 +20,8 @@ export declare const enum NodeType {
20
20
  ColRangeRef = 13,
21
21
  RowRangeRef = 14,
22
22
  StructuredRef = 15,
23
- Missing = 16
23
+ Missing = 16,
24
+ UnionRef = 17
24
25
  }
25
26
  export interface NumberNode {
26
27
  type: NodeType.Number;
@@ -126,4 +127,15 @@ export interface StructuredRefNode {
126
127
  export interface MissingNode {
127
128
  type: NodeType.Missing;
128
129
  }
129
- export type AstNode = NumberNode | StringNode | BooleanNode | ErrorNode | CellRefNode | RangeRefNode | BinaryOpNode | UnaryOpNode | FunctionCallNode | ArrayNode | PercentNode | NameNode | ColRangeRefNode | RowRangeRefNode | StructuredRefNode | MissingNode;
130
+ /**
131
+ * A reference union produced by the comma operator inside
132
+ * parentheses — `(A1:B2, D4:E5)`. Each member is itself a
133
+ * reference-producing sub-expression. INDEX's `area_num` parameter
134
+ * selects which element of the union to read.
135
+ */
136
+ export interface UnionRefNode {
137
+ type: NodeType.UnionRef;
138
+ /** The union members, in syntactic order. */
139
+ areas: AstNode[];
140
+ }
141
+ export type AstNode = NumberNode | StringNode | BooleanNode | ErrorNode | CellRefNode | RangeRefNode | BinaryOpNode | UnaryOpNode | FunctionCallNode | ArrayNode | PercentNode | NameNode | ColRangeRefNode | RowRangeRefNode | StructuredRefNode | MissingNode | UnionRefNode;
@@ -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) {
@@ -148,6 +148,10 @@ export interface IntersectToken {
148
148
  * The input may or may not already be uppercased — this helper does not
149
149
  * alter case; callers that compare against an uppercase table should
150
150
  * uppercase first (or compare case-insensitively).
151
+ *
152
+ * Fast path: plain names (99%+ of call sites) start with a letter, so
153
+ * checking the first code unit before the `startsWith` machinery lets
154
+ * those lookups skip the allocation.
151
155
  */
152
156
  export declare function stripFunctionPrefix(name: string): string;
153
157
  export type Token = NumberToken | StringToken | BooleanToken | ErrorToken | CellRefToken | RangeToken | SheetRefToken | FunctionToken | OperatorToken | OpenParenToken | CloseParenToken | CommaToken | ColonToken | PercentToken | OpenBraceToken | CloseBraceToken | SemicolonToken | UnaryPrefixToken | NameToken | ColRangeToken | RowRangeToken | StructuredRefToken | AtSignToken | IntersectToken;
@@ -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
package/dist/cjs/index.js CHANGED
@@ -17,8 +17,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
17
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.PdfError = exports.PageSizes = exports.excelToPdf = exports.pdf = exports.uint8ArrayToString = exports.stringToUint8Array = exports.toUint8Array = exports.concatUint8Arrays = exports.getRootCause = exports.getErrorChain = exports.errorToJSON = exports.toError = exports.BaseError = exports.getSupportedFormats = exports.DateFormatter = exports.DateParser = exports.xmlDecode = exports.xmlEncode = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.excelToDate = exports.dateToExcel = exports.encodeRange = exports.decodeRange = exports.encodeCell = exports.decodeCell = exports.encodeRow = exports.decodeRow = exports.encodeCol = exports.decodeCol = exports.DefinedNames = exports.createCsvFormatterStream = exports.createCsvParserStream = exports.CsvFormatterStream = exports.CsvParserStream = exports.createTextWatermarkImage = exports.WorksheetReader = exports.WorksheetWriter = exports.WorkbookReader = exports.WorkbookWriter = exports.FormCheckbox = exports.DataValidations = exports.Table = exports.Image = exports.Range = exports.Cell = exports.Column = exports.Row = exports.Worksheet = exports.Workbook = void 0;
21
- exports.MarkdownParseError = exports.MarkdownError = exports.MaxItemsExceededError = exports.ImageError = exports.TableError = exports.PivotTableError = exports.WorksheetNameError = exports.XmlParseError = exports.InvalidValueTypeError = exports.MergeConflictError = exports.RowOutOfBoundsError = exports.ColumnOutOfBoundsError = exports.InvalidAddressError = exports.ExcelStreamStateError = exports.ExcelNotSupportedError = exports.ExcelDownloadError = exports.ExcelFileError = exports.isExcelError = exports.ExcelError = exports.isPdfError = exports.PdfStructureError = exports.PdfFontError = exports.PdfRenderError = void 0;
20
+ exports.pdf = exports.uint8ArrayToString = exports.stringToUint8Array = exports.toUint8Array = exports.concatUint8Arrays = exports.getRootCause = exports.getErrorChain = exports.errorToJSON = exports.toError = exports.BaseError = exports.getSupportedFormats = exports.DateFormatter = exports.DateParser = exports.xmlDecode = exports.xmlEncode = exports.uint8ArrayToBase64 = exports.base64ToUint8Array = exports.excelToDate = exports.dateToExcel = exports.isDateDisplayFormat = exports.formatCellValue = exports.getCellDisplayText = exports.encodeRange = exports.decodeRange = exports.encodeCell = exports.decodeCell = exports.encodeRow = exports.decodeRow = exports.encodeCol = exports.decodeCol = exports.DefinedNames = exports.createCsvFormatterStream = exports.createCsvParserStream = exports.CsvFormatterStream = exports.CsvParserStream = exports.createTextWatermarkImage = exports.WorksheetReader = exports.WorksheetWriter = exports.WorkbookReader = exports.WorkbookWriter = exports.FormCheckbox = exports.DataValidations = exports.Table = exports.Image = exports.Range = exports.Cell = exports.Column = exports.Row = exports.Worksheet = exports.Workbook = void 0;
21
+ exports.MarkdownParseError = exports.MarkdownError = exports.MaxItemsExceededError = exports.ImageError = exports.TableError = exports.PivotTableError = exports.WorksheetNameError = exports.XmlParseError = exports.InvalidValueTypeError = exports.MergeConflictError = exports.RowOutOfBoundsError = exports.ColumnOutOfBoundsError = exports.InvalidAddressError = exports.ExcelStreamStateError = exports.ExcelNotSupportedError = exports.ExcelDownloadError = exports.ExcelFileError = exports.isExcelError = exports.ExcelError = exports.isPdfError = exports.PdfStructureError = exports.PdfFontError = exports.PdfRenderError = exports.PdfError = exports.PageSizes = exports.excelToPdf = void 0;
22
22
  var workbook_1 = require("./modules/excel/workbook.js");
23
23
  Object.defineProperty(exports, "Workbook", { enumerable: true, get: function () { return workbook_1.Workbook; } });
24
24
  var worksheet_1 = require("./modules/excel/worksheet.js");
@@ -91,6 +91,11 @@ Object.defineProperty(exports, "decodeCell", { enumerable: true, get: function (
91
91
  Object.defineProperty(exports, "encodeCell", { enumerable: true, get: function () { return address_1.encodeCell; } });
92
92
  Object.defineProperty(exports, "decodeRange", { enumerable: true, get: function () { return address_1.decodeRange; } });
93
93
  Object.defineProperty(exports, "encodeRange", { enumerable: true, get: function () { return address_1.encodeRange; } });
94
+ // Cell display-text helpers (apply numFmt to produce an Excel-style string)
95
+ var cell_format_1 = require("./modules/excel/utils/cell-format.js");
96
+ Object.defineProperty(exports, "getCellDisplayText", { enumerable: true, get: function () { return cell_format_1.getCellDisplayText; } });
97
+ Object.defineProperty(exports, "formatCellValue", { enumerable: true, get: function () { return cell_format_1.formatCellValue; } });
98
+ Object.defineProperty(exports, "isDateDisplayFormat", { enumerable: true, get: function () { return cell_format_1.isDateDisplayFormat; } });
94
99
  // Date conversion (Excel serial dates <-> JS Date)
95
100
  var utils_base_1 = require("./utils/utils.base.js");
96
101
  Object.defineProperty(exports, "dateToExcel", { enumerable: true, get: function () { return utils_base_1.dateToExcel; } });
@@ -4,6 +4,7 @@ exports.Cell = void 0;
4
4
  const enums_1 = require("./enums.js");
5
5
  const errors_1 = require("./errors.js");
6
6
  const note_1 = require("./note.js");
7
+ const cell_format_1 = require("./utils/cell-format.js");
7
8
  const col_cache_1 = require("./utils/col-cache.js");
8
9
  const copy_style_1 = require("./utils/copy-style.js");
9
10
  const shared_formula_1 = require("./utils/shared-formula.js");
@@ -274,6 +275,26 @@ class Cell {
274
275
  get text() {
275
276
  return this._value.toString();
276
277
  }
278
+ /**
279
+ * The cell's display text — the value formatted the way Excel would render
280
+ * it, applying the cell's `numFmt`. For a Date cell with `numFmt` `"mm-dd-yy"`,
281
+ * this returns e.g. `"04-12-19"` rather than the JS `Date.prototype.toString()`
282
+ * output you'd get from `cell.text`.
283
+ *
284
+ * Handles primitive values, dates, and formula results. For rich text,
285
+ * hyperlinks, errors, and other complex types, falls back to `cell.text`.
286
+ *
287
+ * Note: numFmt codes that are locale-dependent in Excel (e.g. built-in
288
+ * numFmtId 14 renders as `dd.mm.yyyy` under German locale but is stored
289
+ * as `mm-dd-yy`) are applied literally — excelts does not perform
290
+ * Excel's locale-based format substitution. If you need a specific date
291
+ * style across cells regardless of per-cell numFmts, call the exported
292
+ * {@link getCellDisplayText} helper with a `dateFormat` argument, or use
293
+ * `worksheet.toJSON({ dateFormat })`.
294
+ */
295
+ get displayText() {
296
+ return (0, cell_format_1.getCellDisplayText)(this);
297
+ }
277
298
  get html() {
278
299
  return (0, under_dash_1.escapeHtml)(this.text);
279
300
  }