@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
@@ -26,26 +26,9 @@ const _shared_1 = require("./_shared");
26
26
  function sameType(a, b) {
27
27
  return a.kind === b.kind;
28
28
  }
29
- function scalarIsNumber(v) {
30
- return v.kind === 1 /* RVKind.Number */;
31
- }
32
29
  function scalarIsString(v) {
33
30
  return v.kind === 2 /* RVKind.String */;
34
31
  }
35
- function scalarStringEquals(a, b) {
36
- return scalarIsString(a) && scalarIsString(b) && a.value.toLowerCase() === b.value.toLowerCase();
37
- }
38
- /**
39
- * Ordered comparison of two scalars. Numbers compared by value; strings by
40
- * case-insensitive lexical order. Returns NaN when the two operands have
41
- * incompatible types (e.g. number vs string) so callers can skip them.
42
- */
43
- /**
44
- * @deprecated Use `compareScalarsSameKind` from `runtime/values` directly —
45
- * the two are identical. Retained only as a local alias to keep the diff
46
- * small; callers inside this file are free to migrate.
47
- */
48
- const compareScalar = values_1.compareScalarsSameKind;
49
32
  // ============================================================================
50
33
  // Functions
51
34
  // ============================================================================
@@ -78,7 +61,7 @@ function fnINDEX(args) {
78
61
  return (0, values_1.topLeft)(args[0]);
79
62
  }
80
63
  const arr = args[0];
81
- const rowNumV = args.length > 1 ? (0, values_1.toNumberRV)(args[1]) : (0, values_1.rvNumber)(0);
64
+ const rowNumV = args.length > 1 ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[1])) : (0, values_1.rvNumber)(0);
82
65
  if ((0, values_1.isError)(rowNumV)) {
83
66
  return rowNumV;
84
67
  }
@@ -86,7 +69,7 @@ function fnINDEX(args) {
86
69
  // Without this, `INDEX(a, 1.5, 1)` would index into `arr.rows[0.5]`, which
87
70
  // in V8 silently returns `undefined` and corrupts downstream values.
88
71
  const rowNum = Math.trunc(rowNumV.value);
89
- const colNumV = args.length > 2 ? (0, values_1.toNumberRV)(args[2]) : (0, values_1.rvNumber)(0);
72
+ const colNumV = args.length > 2 ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2])) : (0, values_1.rvNumber)(0);
90
73
  if ((0, values_1.isError)(colNumV)) {
91
74
  return colNumV;
92
75
  }
@@ -103,6 +86,10 @@ function fnINDEX(args) {
103
86
  if (c < 0 || c >= arr.width) {
104
87
  return values_1.ERRORS.REF;
105
88
  }
89
+ // Single-row source: a whole-column extract collapses to the one cell.
90
+ if (arr.height === 1) {
91
+ return arr.rows[0][c];
92
+ }
106
93
  const rows = [];
107
94
  for (let r = 0; r < arr.height; r++) {
108
95
  rows.push([(0, _shared_1.getCell)(arr, r, c)]);
@@ -115,6 +102,12 @@ function fnINDEX(args) {
115
102
  if (r < 0 || r >= arr.height) {
116
103
  return values_1.ERRORS.REF;
117
104
  }
105
+ // Single-column source: a whole-row extract collapses to the one cell.
106
+ // Matches Excel's convention — `INDEX(A1:A5, 2)` yields the scalar A2,
107
+ // not a 1×1 array that downstream arithmetic has to implicit-intersect.
108
+ if (arr.width === 1) {
109
+ return arr.rows[r][0];
110
+ }
118
111
  return (0, values_1.rvArray)([[...arr.rows[r]]]);
119
112
  }
120
113
  // Single cell
@@ -134,7 +127,11 @@ function fnMATCH(args) {
134
127
  return values_1.ERRORS.NA;
135
128
  }
136
129
  const lookupArr = args[1];
137
- const matchTypeV = args.length > 2 ? (0, values_1.toNumberRV)(args[2]) : (0, values_1.rvNumber)(1);
130
+ // Blank `match_type` Excel default 1 (largest value lookup, ascending sort).
131
+ // Previously a blank coerced to 0 via toNumberRV and silently flipped the
132
+ // function to exact-match mode — a behaviour gap vs. Excel's documented
133
+ // default.
134
+ const matchTypeV = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2])) : (0, values_1.rvNumber)(1);
138
135
  if ((0, values_1.isError)(matchTypeV)) {
139
136
  return matchTypeV;
140
137
  }
@@ -162,6 +159,12 @@ function fnMATCH(args) {
162
159
  wildcardRe = null;
163
160
  }
164
161
  }
162
+ // Pre-compute the literal (unescaped + lowercased) lookup string
163
+ // once. The old code called `unescapeExcelWildcard(lookupValue.value).toLowerCase()`
164
+ // inside the hot per-cell loop, paying O(n) per cell for what is
165
+ // a constant expression. (For ranges without strings the literal
166
+ // is never consulted — `scalarIsString(fi)` short-circuits first.)
167
+ const lookupLiteralLc = lookupStr !== null && !hasWildcard ? (0, _shared_1.unescapeExcelWildcard)(lookupStr).toLowerCase() : null;
165
168
  for (let i = 0; i < flat.length; i++) {
166
169
  if ((0, values_1.scalarEquals)(flat[i], lookupValue)) {
167
170
  return (0, values_1.rvNumber)(i + 1);
@@ -173,7 +176,7 @@ function fnMATCH(args) {
173
176
  return (0, values_1.rvNumber)(i + 1);
174
177
  }
175
178
  }
176
- else {
179
+ else if (lookupLiteralLc !== null) {
177
180
  // No unescaped wildcard — but the pattern may still contain
178
181
  // `~*` / `~?` / `~~` escape sequences that should reduce to
179
182
  // their literal character before comparison. Calling
@@ -181,8 +184,7 @@ function fnMATCH(args) {
181
184
  // SEARCH and the criteria predicate use; without it,
182
185
  // `MATCH("a~*b", ...)` would literally look for `"a~*b"`
183
186
  // instead of `"a*b"`.
184
- const literal = (0, _shared_1.unescapeExcelWildcard)(lookupValue.value).toLowerCase();
185
- if (fi.value.toLowerCase() === literal) {
187
+ if (fi.value.toLowerCase() === lookupLiteralLc) {
186
188
  return (0, values_1.rvNumber)(i + 1);
187
189
  }
188
190
  }
@@ -195,23 +197,18 @@ function fnMATCH(args) {
195
197
  let bestIdx = -1;
196
198
  for (let i = 0; i < flat.length; i++) {
197
199
  const v = flat[i];
198
- if (sameType(v, lookupValue)) {
199
- if (scalarIsNumber(v) && scalarIsNumber(lookupValue)) {
200
- if (v.value <= lookupValue.value) {
201
- bestIdx = i;
202
- }
203
- else {
204
- break;
205
- }
206
- }
207
- else if (scalarIsString(v) && scalarIsString(lookupValue)) {
208
- if (v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
209
- bestIdx = i;
210
- }
211
- else {
212
- break;
213
- }
214
- }
200
+ if (v.kind !== lookupValue.kind) {
201
+ continue;
202
+ }
203
+ const cmp = (0, values_1.compareScalarsSameKind)(v, lookupValue);
204
+ if (!Number.isFinite(cmp)) {
205
+ continue;
206
+ }
207
+ if (cmp <= 0) {
208
+ bestIdx = i;
209
+ }
210
+ else {
211
+ break;
215
212
  }
216
213
  }
217
214
  return bestIdx >= 0 ? (0, values_1.rvNumber)(bestIdx + 1) : values_1.ERRORS.NA;
@@ -220,23 +217,18 @@ function fnMATCH(args) {
220
217
  let bestIdx = -1;
221
218
  for (let i = 0; i < flat.length; i++) {
222
219
  const v = flat[i];
223
- if (sameType(v, lookupValue)) {
224
- if (scalarIsNumber(v) && scalarIsNumber(lookupValue)) {
225
- if (v.value >= lookupValue.value) {
226
- bestIdx = i;
227
- }
228
- else {
229
- break;
230
- }
231
- }
232
- else if (scalarIsString(v) && scalarIsString(lookupValue)) {
233
- if (v.value.toLowerCase() >= lookupValue.value.toLowerCase()) {
234
- bestIdx = i;
235
- }
236
- else {
237
- break;
238
- }
239
- }
220
+ if (v.kind !== lookupValue.kind) {
221
+ continue;
222
+ }
223
+ const cmp = (0, values_1.compareScalarsSameKind)(v, lookupValue);
224
+ if (!Number.isFinite(cmp)) {
225
+ continue;
226
+ }
227
+ if (cmp >= 0) {
228
+ bestIdx = i;
229
+ }
230
+ else {
231
+ break;
240
232
  }
241
233
  }
242
234
  return bestIdx >= 0 ? (0, values_1.rvNumber)(bestIdx + 1) : values_1.ERRORS.NA;
@@ -250,13 +242,18 @@ function fnVLOOKUP(args) {
250
242
  return values_1.ERRORS.NA;
251
243
  }
252
244
  const table = args[1];
253
- const colIndexV = (0, values_1.toNumberRV)(args[2]);
245
+ const colIndexV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2]));
254
246
  if ((0, values_1.isError)(colIndexV)) {
255
247
  return colIndexV;
256
248
  }
257
249
  // VLOOKUP truncates the column index toward zero before bounds checks.
258
250
  const colIndex = Math.trunc(colIndexV.value);
259
- const rangeLookupV = args.length > 3 ? (0, values_1.toBooleanRV)(args[3]) : { kind: 3 /* RVKind.Boolean */, value: true };
251
+ // Blank `range_lookup` Excel default TRUE. A blank coerces through
252
+ // `toBooleanRV` to FALSE which silently flips to exact match —
253
+ // opposite of Excel's documented default.
254
+ const rangeLookupV = args.length > 3 && args[3].kind !== 0 /* RVKind.Blank */
255
+ ? (0, values_1.toBooleanRV)((0, values_1.topLeft)(args[3]))
256
+ : { kind: 3 /* RVKind.Boolean */, value: true };
260
257
  if ((0, values_1.isError)(rangeLookupV)) {
261
258
  return rangeLookupV;
262
259
  }
@@ -265,39 +262,65 @@ function fnVLOOKUP(args) {
265
262
  return values_1.ERRORS.REF;
266
263
  }
267
264
  if (!rangeLookup) {
268
- // Exact match
265
+ // Exact match — `scalarEquals` already handles case-insensitive string
266
+ // comparison (see runtime/values.ts), so the earlier `scalarStringEquals`
267
+ // fallback was dead code.
268
+ //
269
+ // Excel's VLOOKUP supports wildcards (`*`, `?`, `~*`, `~?`, `~~`) in
270
+ // the exact-match mode (range_lookup=FALSE). Three paths, chosen by
271
+ // what the lookup string contains:
272
+ // - unescaped wildcard (`*` or `?`) → regex match
273
+ // - only escape sequences (`~*`, `~?`, `~~`) → unescape then
274
+ // literal case-insensitive compare (so `"a~*b"` matches `"a*b"`)
275
+ // - neither → plain `scalarEquals`
276
+ const lookupStr = lookupValue.kind === 2 /* RVKind.String */ ? lookupValue.value : null;
277
+ let wildcardRe = null;
278
+ let literalLc = null;
279
+ if (lookupStr !== null && (0, _shared_1.hasUnescapedWildcard)(lookupStr)) {
280
+ try {
281
+ wildcardRe = new RegExp("^" + (0, _shared_1.excelWildcardToRegex)(lookupStr) + "$", "i");
282
+ }
283
+ catch {
284
+ wildcardRe = null;
285
+ }
286
+ }
287
+ else if (lookupStr !== null && /~[*?~]/.test(lookupStr)) {
288
+ literalLc = (0, _shared_1.unescapeExcelWildcard)(lookupStr).toLowerCase();
289
+ }
269
290
  for (let r = 0; r < table.height; r++) {
270
291
  const cell = (0, _shared_1.getCell)(table, r, 0);
271
292
  if ((0, values_1.scalarEquals)(cell, lookupValue)) {
272
293
  return (0, _shared_1.getCell)(table, r, colIndex - 1);
273
294
  }
274
- if (scalarStringEquals(cell, lookupValue)) {
295
+ if (wildcardRe && cell.kind === 2 /* RVKind.String */ && wildcardRe.test(cell.value)) {
296
+ return (0, _shared_1.getCell)(table, r, colIndex - 1);
297
+ }
298
+ if (literalLc !== null &&
299
+ cell.kind === 2 /* RVKind.String */ &&
300
+ cell.value.toLowerCase() === literalLc) {
275
301
  return (0, _shared_1.getCell)(table, r, colIndex - 1);
276
302
  }
277
303
  }
278
304
  return values_1.ERRORS.NA;
279
305
  }
280
- // Approximate match: sorted ascending by first column.
306
+ // Approximate match: sorted ascending by first column. Binary-search
307
+ // style isn't safe here (Excel allows mixed-type entries which break
308
+ // monotonicity), so walk until we overshoot.
281
309
  let bestRow = -1;
282
310
  for (let r = 0; r < table.height; r++) {
283
311
  const v = (0, _shared_1.getCell)(table, r, 0);
284
- if (sameType(v, lookupValue)) {
285
- if (scalarIsNumber(v) && scalarIsNumber(lookupValue)) {
286
- if (v.value <= lookupValue.value) {
287
- bestRow = r;
288
- }
289
- else {
290
- break;
291
- }
292
- }
293
- else if (scalarIsString(v) && scalarIsString(lookupValue)) {
294
- if (v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
295
- bestRow = r;
296
- }
297
- else {
298
- break;
299
- }
300
- }
312
+ if (v.kind !== lookupValue.kind) {
313
+ continue;
314
+ }
315
+ const cmp = (0, values_1.compareScalarsSameKind)(v, lookupValue);
316
+ if (!Number.isFinite(cmp)) {
317
+ continue;
318
+ }
319
+ if (cmp <= 0) {
320
+ bestRow = r;
321
+ }
322
+ else {
323
+ break;
301
324
  }
302
325
  }
303
326
  return bestRow >= 0 ? (0, _shared_1.getCell)(table, bestRow, colIndex - 1) : values_1.ERRORS.NA;
@@ -311,13 +334,16 @@ function fnHLOOKUP(args) {
311
334
  return values_1.ERRORS.NA;
312
335
  }
313
336
  const table = args[1];
314
- const rowIndexV = (0, values_1.toNumberRV)(args[2]);
337
+ const rowIndexV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2]));
315
338
  if ((0, values_1.isError)(rowIndexV)) {
316
339
  return rowIndexV;
317
340
  }
318
341
  // HLOOKUP truncates the row index toward zero before bounds checks.
319
342
  const rowIndex = Math.trunc(rowIndexV.value);
320
- const rangeLookupV = args.length > 3 ? (0, values_1.toBooleanRV)(args[3]) : { kind: 3 /* RVKind.Boolean */, value: true };
343
+ // Blank `range_lookup` Excel default TRUE (see VLOOKUP rationale).
344
+ const rangeLookupV = args.length > 3 && args[3].kind !== 0 /* RVKind.Blank */
345
+ ? (0, values_1.toBooleanRV)((0, values_1.topLeft)(args[3]))
346
+ : { kind: 3 /* RVKind.Boolean */, value: true };
321
347
  if ((0, values_1.isError)(rangeLookupV)) {
322
348
  return rangeLookupV;
323
349
  }
@@ -326,8 +352,33 @@ function fnHLOOKUP(args) {
326
352
  return values_1.ERRORS.REF;
327
353
  }
328
354
  if (!rangeLookup) {
355
+ // Exact match — supports wildcards on string lookups (see VLOOKUP
356
+ // for full rationale; the paths mirror one another).
357
+ const lookupStr = lookupValue.kind === 2 /* RVKind.String */ ? lookupValue.value : null;
358
+ let wildcardRe = null;
359
+ let literalLc = null;
360
+ if (lookupStr !== null && (0, _shared_1.hasUnescapedWildcard)(lookupStr)) {
361
+ try {
362
+ wildcardRe = new RegExp("^" + (0, _shared_1.excelWildcardToRegex)(lookupStr) + "$", "i");
363
+ }
364
+ catch {
365
+ wildcardRe = null;
366
+ }
367
+ }
368
+ else if (lookupStr !== null && /~[*?~]/.test(lookupStr)) {
369
+ literalLc = (0, _shared_1.unescapeExcelWildcard)(lookupStr).toLowerCase();
370
+ }
329
371
  for (let c = 0; c < table.width; c++) {
330
- if ((0, values_1.scalarEquals)((0, _shared_1.getCell)(table, 0, c), lookupValue)) {
372
+ const cell = (0, _shared_1.getCell)(table, 0, c);
373
+ if ((0, values_1.scalarEquals)(cell, lookupValue)) {
374
+ return (0, _shared_1.getCell)(table, rowIndex - 1, c);
375
+ }
376
+ if (wildcardRe && cell.kind === 2 /* RVKind.String */ && wildcardRe.test(cell.value)) {
377
+ return (0, _shared_1.getCell)(table, rowIndex - 1, c);
378
+ }
379
+ if (literalLc !== null &&
380
+ cell.kind === 2 /* RVKind.String */ &&
381
+ cell.value.toLowerCase() === literalLc) {
331
382
  return (0, _shared_1.getCell)(table, rowIndex - 1, c);
332
383
  }
333
384
  }
@@ -336,18 +387,18 @@ function fnHLOOKUP(args) {
336
387
  let bestCol = -1;
337
388
  for (let c = 0; c < table.width; c++) {
338
389
  const hv = (0, _shared_1.getCell)(table, 0, c);
339
- if (sameType(hv, lookupValue)) {
340
- // For approximate match, find largest <= lookupValue
341
- if (scalarIsNumber(hv) && scalarIsNumber(lookupValue)) {
342
- if (hv.value <= lookupValue.value) {
343
- bestCol = c;
344
- }
345
- }
346
- else if (scalarIsString(hv) && scalarIsString(lookupValue)) {
347
- if (hv.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
348
- bestCol = c;
349
- }
350
- }
390
+ if (hv.kind !== lookupValue.kind) {
391
+ continue;
392
+ }
393
+ const cmp = (0, values_1.compareScalarsSameKind)(hv, lookupValue);
394
+ if (!Number.isFinite(cmp)) {
395
+ continue;
396
+ }
397
+ // Approximate match: pick the largest value <= lookupValue. We don't
398
+ // break early when we overshoot because HLOOKUP's legacy behaviour
399
+ // scans the whole row even for unsorted data.
400
+ if (cmp <= 0) {
401
+ bestCol = c;
351
402
  }
352
403
  }
353
404
  return bestCol >= 0 ? (0, _shared_1.getCell)(table, rowIndex - 1, bestCol) : values_1.ERRORS.NA;
@@ -365,17 +416,30 @@ function fnXLOOKUP(args) {
365
416
  return values_1.ERRORS.VALUE;
366
417
  }
367
418
  const returnArr = args[2];
368
- const ifNotFound = args.length > 3 ? (0, values_1.topLeft)(args[3]) : null;
369
- const matchModeV = args.length > 4 ? (0, values_1.toNumberRV)(args[4]) : (0, values_1.rvNumber)(0);
419
+ // Blank `if_not_found` treat as omitted (default #N/A). Without
420
+ // this guard, an explicitly-blank fourth slot would produce BLANK as
421
+ // the fallback, which differs from Excel's "omitted → #N/A" default
422
+ // and from what a user intuitively expects.
423
+ const ifNotFound = args.length > 3 && args[3].kind !== 0 /* RVKind.Blank */ ? (0, values_1.topLeft)(args[3]) : null;
424
+ // Blank `match_mode` → 0 (exact); any non-{-1, 0, 1, 2} is rejected.
425
+ const matchModeV = args.length > 4 && args[4].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[4])) : (0, values_1.rvNumber)(0);
370
426
  if ((0, values_1.isError)(matchModeV)) {
371
427
  return matchModeV;
372
428
  }
373
429
  const matchMode = matchModeV.value;
374
- const searchModeV = args.length > 5 ? (0, values_1.toNumberRV)(args[5]) : (0, values_1.rvNumber)(1);
430
+ if (matchMode !== 0 && matchMode !== -1 && matchMode !== 1 && matchMode !== 2) {
431
+ return values_1.ERRORS.VALUE;
432
+ }
433
+ // Blank `search_mode` → 1 (first-to-last). Previously a blank coerced
434
+ // to 0 which silently passed through but is not a valid search mode.
435
+ const searchModeV = args.length > 5 && args[5].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[5])) : (0, values_1.rvNumber)(1);
375
436
  if ((0, values_1.isError)(searchModeV)) {
376
437
  return searchModeV;
377
438
  }
378
439
  const searchMode = searchModeV.value;
440
+ if (searchMode !== 1 && searchMode !== -1 && searchMode !== 2 && searchMode !== -2) {
441
+ return values_1.ERRORS.VALUE;
442
+ }
379
443
  // Flatten lookup array to 1D
380
444
  const flat = [];
381
445
  const isRow = lookupArr.height === 1;
@@ -480,10 +544,6 @@ function fnXLOOKUP(args) {
480
544
  foundIdx = i;
481
545
  break;
482
546
  }
483
- if (scalarStringEquals(flat[i], lookupValue)) {
484
- foundIdx = i;
485
- break;
486
- }
487
547
  }
488
548
  }
489
549
  else if (matchMode === -1) {
@@ -586,16 +646,23 @@ function fnXMATCH(args) {
586
646
  return values_1.ERRORS.VALUE;
587
647
  }
588
648
  const lookupArr = args[1];
589
- const matchModeV = args.length > 2 ? (0, values_1.toNumberRV)(args[2]) : (0, values_1.rvNumber)(0);
649
+ // Blank `match_mode` 0 (exact). Same validation as XLOOKUP.
650
+ const matchModeV = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2])) : (0, values_1.rvNumber)(0);
590
651
  if ((0, values_1.isError)(matchModeV)) {
591
652
  return matchModeV;
592
653
  }
593
654
  const matchMode = matchModeV.value;
594
- const searchModeV = args.length > 3 ? (0, values_1.toNumberRV)(args[3]) : (0, values_1.rvNumber)(1);
655
+ if (matchMode !== 0 && matchMode !== -1 && matchMode !== 1 && matchMode !== 2) {
656
+ return values_1.ERRORS.VALUE;
657
+ }
658
+ const searchModeV = args.length > 3 && args[3].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[3])) : (0, values_1.rvNumber)(1);
595
659
  if ((0, values_1.isError)(searchModeV)) {
596
660
  return searchModeV;
597
661
  }
598
662
  const searchMode = searchModeV.value;
663
+ if (searchMode !== 1 && searchMode !== -1 && searchMode !== 2 && searchMode !== -2) {
664
+ return values_1.ERRORS.VALUE;
665
+ }
599
666
  const flat = [];
600
667
  if (lookupArr.height === 1) {
601
668
  for (let c = 0; c < lookupArr.width; c++) {
@@ -615,9 +682,6 @@ function fnXMATCH(args) {
615
682
  if ((0, values_1.scalarEquals)(flat[i], lookupValue)) {
616
683
  return (0, values_1.rvNumber)(i + 1);
617
684
  }
618
- if (scalarStringEquals(flat[i], lookupValue)) {
619
- return (0, values_1.rvNumber)(i + 1);
620
- }
621
685
  }
622
686
  return values_1.ERRORS.NA;
623
687
  }
@@ -665,11 +729,11 @@ function fnXMATCH(args) {
665
729
  // Next-smaller-or-equal: largest item <= lookupValue.
666
730
  let best = -1;
667
731
  for (let i = 0; i < flat.length; i++) {
668
- const cmp = compareScalar(flat[i], lookupValue);
732
+ const cmp = (0, values_1.compareScalarsSameKind)(flat[i], lookupValue);
669
733
  if (Number.isNaN(cmp)) {
670
734
  continue;
671
735
  }
672
- if (cmp <= 0 && (best === -1 || compareScalar(flat[i], flat[best]) > 0)) {
736
+ if (cmp <= 0 && (best === -1 || (0, values_1.compareScalarsSameKind)(flat[i], flat[best]) > 0)) {
673
737
  best = i;
674
738
  }
675
739
  }
@@ -679,11 +743,11 @@ function fnXMATCH(args) {
679
743
  // Next-larger-or-equal: smallest item >= lookupValue.
680
744
  let best = -1;
681
745
  for (let i = 0; i < flat.length; i++) {
682
- const cmp = compareScalar(flat[i], lookupValue);
746
+ const cmp = (0, values_1.compareScalarsSameKind)(flat[i], lookupValue);
683
747
  if (Number.isNaN(cmp)) {
684
748
  continue;
685
749
  }
686
- if (cmp >= 0 && (best === -1 || compareScalar(flat[i], flat[best]) < 0)) {
750
+ if (cmp >= 0 && (best === -1 || (0, values_1.compareScalarsSameKind)(flat[i], flat[best]) < 0)) {
687
751
  best = i;
688
752
  }
689
753
  }
@@ -692,12 +756,12 @@ function fnXMATCH(args) {
692
756
  return values_1.ERRORS.NA;
693
757
  }
694
758
  function fnADDRESS(args) {
695
- const rowNumV = (0, values_1.toNumberRV)(args[0]);
759
+ const rowNumV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[0]));
696
760
  if ((0, values_1.isError)(rowNumV)) {
697
761
  return rowNumV;
698
762
  }
699
763
  const rowNum = Math.trunc(rowNumV.value);
700
- const colNumV = (0, values_1.toNumberRV)(args[1]);
764
+ const colNumV = (0, values_1.toNumberRV)((0, values_1.topLeft)(args[1]));
701
765
  if ((0, values_1.isError)(colNumV)) {
702
766
  return colNumV;
703
767
  }
@@ -708,7 +772,10 @@ function fnADDRESS(args) {
708
772
  if (!Number.isFinite(rowNum) || !Number.isFinite(colNum) || rowNum < 1 || colNum < 1) {
709
773
  return values_1.ERRORS.VALUE;
710
774
  }
711
- const absNumV = args.length > 2 ? (0, values_1.toNumberRV)(args[2]) : (0, values_1.rvNumber)(1);
775
+ // Blank `abs_num` Excel default 1 (fully absolute). Without the
776
+ // blank guard, `toNumberRV(BLANK)` coerces to 0 which falls outside
777
+ // the 1..4 range and surfaces a spurious #VALUE!.
778
+ const absNumV = args.length > 2 && args[2].kind !== 0 /* RVKind.Blank */ ? (0, values_1.toNumberRV)((0, values_1.topLeft)(args[2])) : (0, values_1.rvNumber)(1);
712
779
  if ((0, values_1.isError)(absNumV)) {
713
780
  return absNumV;
714
781
  }
@@ -720,7 +787,7 @@ function fnADDRESS(args) {
720
787
  // a1 style (true/default) vs r1c1 (false)
721
788
  const a1Arg = args.length > 3 ? (0, values_1.topLeft)(args[3]) : { kind: 3 /* RVKind.Boolean */, value: true };
722
789
  const a1 = a1Arg.kind === 3 /* RVKind.Boolean */ ? a1Arg.value : true;
723
- const sheetText = args.length > 4 ? (0, values_1.toStringRV)(args[4]) : "";
790
+ const sheetText = args.length > 4 ? (0, values_1.toStringRV)((0, values_1.topLeft)(args[4])) : "";
724
791
  if (!a1) {
725
792
  // R1C1 style
726
793
  const rPart = absNum === 1 || absNum === 2 ? `R${rowNum}` : `R[${rowNum}]`;
@@ -793,20 +860,7 @@ function fnLOOKUP(args) {
793
860
  flat.push((0, _shared_1.getCell)(lookupArr, r, 0));
794
861
  }
795
862
  }
796
- let bestIdx = -1;
797
- for (let i = 0; i < flat.length; i++) {
798
- const v = flat[i];
799
- if (sameType(v, lookupValue)) {
800
- if (scalarIsNumber(v) && scalarIsNumber(lookupValue) && v.value <= lookupValue.value) {
801
- bestIdx = i;
802
- }
803
- else if (scalarIsString(v) &&
804
- scalarIsString(lookupValue) &&
805
- v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
806
- bestIdx = i;
807
- }
808
- }
809
- }
863
+ const bestIdx = findLastLessEqual(flat, lookupValue);
810
864
  if (bestIdx === -1) {
811
865
  return values_1.ERRORS.NA;
812
866
  }
@@ -827,38 +881,48 @@ function fnLOOKUP(args) {
827
881
  return values_1.ERRORS.NA;
828
882
  }
829
883
  if (cols >= rows) {
830
- let bestIdx = -1;
884
+ // Array-form LOOKUP, horizontal orientation: lookup runs along first
885
+ // row; result is pulled from the last row of the same column.
886
+ const firstRow = [];
831
887
  for (let c = 0; c < cols; c++) {
832
- const v = (0, _shared_1.getCell)(lookupArr, 0, c);
833
- if (sameType(v, lookupValue)) {
834
- if (scalarIsNumber(v) && scalarIsNumber(lookupValue) && v.value <= lookupValue.value) {
835
- bestIdx = c;
836
- }
837
- else if (scalarIsString(v) &&
838
- scalarIsString(lookupValue) &&
839
- v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
840
- bestIdx = c;
841
- }
842
- }
888
+ firstRow.push((0, _shared_1.getCell)(lookupArr, 0, c));
843
889
  }
890
+ const bestIdx = findLastLessEqual(firstRow, lookupValue);
844
891
  return bestIdx >= 0 ? (0, _shared_1.getCell)(lookupArr, rows - 1, bestIdx) : values_1.ERRORS.NA;
845
892
  }
846
- let bestIdx = -1;
893
+ // Array-form LOOKUP, vertical orientation: lookup runs down first column;
894
+ // result is pulled from the last column of the same row.
895
+ const firstCol = [];
847
896
  for (let r = 0; r < rows; r++) {
848
- const v = (0, _shared_1.getCell)(lookupArr, r, 0);
849
- if (sameType(v, lookupValue)) {
850
- if (scalarIsNumber(v) && scalarIsNumber(lookupValue) && v.value <= lookupValue.value) {
851
- bestIdx = r;
852
- }
853
- else if (scalarIsString(v) &&
854
- scalarIsString(lookupValue) &&
855
- v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
856
- bestIdx = r;
857
- }
858
- }
897
+ firstCol.push((0, _shared_1.getCell)(lookupArr, r, 0));
859
898
  }
899
+ const bestIdx = findLastLessEqual(firstCol, lookupValue);
860
900
  return bestIdx >= 0 ? (0, _shared_1.getCell)(lookupArr, bestIdx, cols - 1) : values_1.ERRORS.NA;
861
901
  }
902
+ /**
903
+ * Linear scan for the largest same-kind value that is `<= target`.
904
+ * Returns the flat-index of that value, or `-1` when no same-kind
905
+ * value qualifies. Uses `compareScalarsSameKind` so numbers compare by
906
+ * value, strings case-insensitively — the same ordering Excel uses
907
+ * for legacy LOOKUP / VLOOKUP / HLOOKUP approximate matches.
908
+ */
909
+ function findLastLessEqual(flat, target) {
910
+ let bestIdx = -1;
911
+ for (let i = 0; i < flat.length; i++) {
912
+ const v = flat[i];
913
+ if (v.kind !== target.kind) {
914
+ continue;
915
+ }
916
+ const cmp = (0, values_1.compareScalarsSameKind)(v, target);
917
+ if (!Number.isFinite(cmp)) {
918
+ continue;
919
+ }
920
+ if (cmp <= 0) {
921
+ bestIdx = i;
922
+ }
923
+ }
924
+ return bestIdx;
925
+ }
862
926
  function fnTRANSPOSE(args) {
863
927
  if (!(0, values_1.isArray)(args[0])) {
864
928
  const sv = (0, values_1.topLeft)(args[0]);
@@ -887,10 +951,13 @@ function fnAREAS(args) {
887
951
  if (args.length === 0) {
888
952
  return values_1.ERRORS.VALUE;
889
953
  }
890
- // Error in the argument propagates (Excel parity). A reference value
891
- // counts as one area; the engine does not yet build multi-area
892
- // references via `(A1, B1)` union syntax when that lands, the arity
893
- // should be `v.areas.length`, not a hardcoded 1.
954
+ // Normally unreachable the evaluator's reference-aware path in
955
+ // `evaluateCall` intercepts AREAS before eager dereference happens,
956
+ // so by the time this fallback runs the reference has already been
957
+ // flattened into a dereferenced array (losing the area count).
958
+ // Keep the fallback behaviour aligned with the intercept path:
959
+ // arrays and scalars that reach here are not references and should
960
+ // surface as `#VALUE!`.
894
961
  const a = args[0];
895
962
  if (a.kind === 4 /* RVKind.Error */) {
896
963
  return a;
@@ -898,5 +965,5 @@ function fnAREAS(args) {
898
965
  if (a.kind === 6 /* RVKind.Reference */) {
899
966
  return (0, values_1.rvNumber)(a.areas.length);
900
967
  }
901
- return (0, values_1.rvNumber)(1);
968
+ return values_1.ERRORS.VALUE;
902
969
  }