@cj-tech-master/excelts 9.3.1 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/modules/excel/cell.d.ts +18 -0
- package/dist/browser/modules/excel/cell.js +21 -0
- package/dist/browser/modules/excel/utils/cell-format.js +85 -13
- package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/browser/modules/excel/workbook.browser.js +49 -0
- package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/browser/modules/formula/compile/binder.js +48 -6
- package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
- package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/browser/modules/formula/functions/_shared.js +47 -0
- package/dist/browser/modules/formula/functions/conditional.js +103 -22
- package/dist/browser/modules/formula/functions/date.js +105 -23
- package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/browser/modules/formula/functions/engineering.js +103 -151
- package/dist/browser/modules/formula/functions/financial.js +210 -184
- package/dist/browser/modules/formula/functions/lookup.js +224 -157
- package/dist/browser/modules/formula/functions/math.d.ts +26 -0
- package/dist/browser/modules/formula/functions/math.js +249 -69
- package/dist/browser/modules/formula/functions/statistical.js +221 -171
- package/dist/browser/modules/formula/functions/text.js +112 -52
- package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
- package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
- package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
- package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
- package/dist/browser/modules/formula/runtime/values.js +20 -2
- package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/browser/modules/formula/syntax/ast.js +1 -0
- package/dist/browser/modules/formula/syntax/parser.js +29 -7
- package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
- package/dist/browser/modules/formula/syntax/token-types.js +9 -0
- package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/modules/excel/cell.js +21 -0
- package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
- package/dist/cjs/modules/excel/workbook.browser.js +49 -0
- package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/cjs/modules/formula/compile/binder.js +48 -6
- package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/cjs/modules/formula/functions/_shared.js +48 -0
- package/dist/cjs/modules/formula/functions/conditional.js +103 -22
- package/dist/cjs/modules/formula/functions/date.js +104 -22
- package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/cjs/modules/formula/functions/engineering.js +109 -157
- package/dist/cjs/modules/formula/functions/financial.js +209 -183
- package/dist/cjs/modules/formula/functions/lookup.js +224 -157
- package/dist/cjs/modules/formula/functions/math.js +254 -70
- package/dist/cjs/modules/formula/functions/statistical.js +222 -172
- package/dist/cjs/modules/formula/functions/text.js +112 -52
- package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
- package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
- package/dist/cjs/modules/formula/runtime/values.js +21 -2
- package/dist/cjs/modules/formula/syntax/parser.js +29 -7
- package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
- package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/esm/index.js +2 -0
- package/dist/esm/modules/excel/cell.js +21 -0
- package/dist/esm/modules/excel/utils/cell-format.js +85 -13
- package/dist/esm/modules/excel/workbook.browser.js +49 -0
- package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/esm/modules/formula/compile/binder.js +48 -6
- package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
- package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/esm/modules/formula/functions/_shared.js +47 -0
- package/dist/esm/modules/formula/functions/conditional.js +103 -22
- package/dist/esm/modules/formula/functions/date.js +105 -23
- package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/esm/modules/formula/functions/engineering.js +103 -151
- package/dist/esm/modules/formula/functions/financial.js +210 -184
- package/dist/esm/modules/formula/functions/lookup.js +224 -157
- package/dist/esm/modules/formula/functions/math.js +249 -69
- package/dist/esm/modules/formula/functions/statistical.js +221 -171
- package/dist/esm/modules/formula/functions/text.js +112 -52
- package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
- package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
- package/dist/esm/modules/formula/runtime/values.js +20 -2
- package/dist/esm/modules/formula/syntax/ast.js +1 -0
- package/dist/esm/modules/formula/syntax/parser.js +29 -7
- package/dist/esm/modules/formula/syntax/token-types.js +9 -0
- package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/iife/excelts.iife.js +1502 -1379
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +26 -26
- package/dist/types/index.d.ts +1 -0
- package/dist/types/modules/excel/cell.d.ts +18 -0
- package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/types/modules/formula/functions/math.d.ts +26 -0
- package/dist/types/modules/formula/materialize/types.d.ts +15 -0
- package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/types/modules/formula/runtime/values.d.ts +13 -0
- package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 ||
|
|
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 =
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
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
|
|
968
|
+
return values_1.ERRORS.VALUE;
|
|
902
969
|
}
|