@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
|
@@ -10,26 +10,9 @@ import { excelWildcardToRegex, getCell, hasUnescapedWildcard, unescapeExcelWildc
|
|
|
10
10
|
function sameType(a, b) {
|
|
11
11
|
return a.kind === b.kind;
|
|
12
12
|
}
|
|
13
|
-
function scalarIsNumber(v) {
|
|
14
|
-
return v.kind === RVKind.Number;
|
|
15
|
-
}
|
|
16
13
|
function scalarIsString(v) {
|
|
17
14
|
return v.kind === RVKind.String;
|
|
18
15
|
}
|
|
19
|
-
function scalarStringEquals(a, b) {
|
|
20
|
-
return scalarIsString(a) && scalarIsString(b) && a.value.toLowerCase() === b.value.toLowerCase();
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Ordered comparison of two scalars. Numbers compared by value; strings by
|
|
24
|
-
* case-insensitive lexical order. Returns NaN when the two operands have
|
|
25
|
-
* incompatible types (e.g. number vs string) so callers can skip them.
|
|
26
|
-
*/
|
|
27
|
-
/**
|
|
28
|
-
* @deprecated Use `compareScalarsSameKind` from `runtime/values` directly —
|
|
29
|
-
* the two are identical. Retained only as a local alias to keep the diff
|
|
30
|
-
* small; callers inside this file are free to migrate.
|
|
31
|
-
*/
|
|
32
|
-
const compareScalar = compareScalarsSameKind;
|
|
33
16
|
// ============================================================================
|
|
34
17
|
// Functions
|
|
35
18
|
// ============================================================================
|
|
@@ -62,7 +45,7 @@ export function fnINDEX(args) {
|
|
|
62
45
|
return topLeft(args[0]);
|
|
63
46
|
}
|
|
64
47
|
const arr = args[0];
|
|
65
|
-
const rowNumV = args.length > 1 ? toNumberRV(args[1]) : rvNumber(0);
|
|
48
|
+
const rowNumV = args.length > 1 ? toNumberRV(topLeft(args[1])) : rvNumber(0);
|
|
66
49
|
if (isError(rowNumV)) {
|
|
67
50
|
return rowNumV;
|
|
68
51
|
}
|
|
@@ -70,7 +53,7 @@ export function fnINDEX(args) {
|
|
|
70
53
|
// Without this, `INDEX(a, 1.5, 1)` would index into `arr.rows[0.5]`, which
|
|
71
54
|
// in V8 silently returns `undefined` and corrupts downstream values.
|
|
72
55
|
const rowNum = Math.trunc(rowNumV.value);
|
|
73
|
-
const colNumV = args.length > 2 ? toNumberRV(args[2]) : rvNumber(0);
|
|
56
|
+
const colNumV = args.length > 2 ? toNumberRV(topLeft(args[2])) : rvNumber(0);
|
|
74
57
|
if (isError(colNumV)) {
|
|
75
58
|
return colNumV;
|
|
76
59
|
}
|
|
@@ -87,6 +70,10 @@ export function fnINDEX(args) {
|
|
|
87
70
|
if (c < 0 || c >= arr.width) {
|
|
88
71
|
return ERRORS.REF;
|
|
89
72
|
}
|
|
73
|
+
// Single-row source: a whole-column extract collapses to the one cell.
|
|
74
|
+
if (arr.height === 1) {
|
|
75
|
+
return arr.rows[0][c];
|
|
76
|
+
}
|
|
90
77
|
const rows = [];
|
|
91
78
|
for (let r = 0; r < arr.height; r++) {
|
|
92
79
|
rows.push([getCell(arr, r, c)]);
|
|
@@ -99,6 +86,12 @@ export function fnINDEX(args) {
|
|
|
99
86
|
if (r < 0 || r >= arr.height) {
|
|
100
87
|
return ERRORS.REF;
|
|
101
88
|
}
|
|
89
|
+
// Single-column source: a whole-row extract collapses to the one cell.
|
|
90
|
+
// Matches Excel's convention — `INDEX(A1:A5, 2)` yields the scalar A2,
|
|
91
|
+
// not a 1×1 array that downstream arithmetic has to implicit-intersect.
|
|
92
|
+
if (arr.width === 1) {
|
|
93
|
+
return arr.rows[r][0];
|
|
94
|
+
}
|
|
102
95
|
return rvArray([[...arr.rows[r]]]);
|
|
103
96
|
}
|
|
104
97
|
// Single cell
|
|
@@ -118,7 +111,11 @@ export function fnMATCH(args) {
|
|
|
118
111
|
return ERRORS.NA;
|
|
119
112
|
}
|
|
120
113
|
const lookupArr = args[1];
|
|
121
|
-
|
|
114
|
+
// Blank `match_type` → Excel default 1 (largest value ≤ lookup, ascending sort).
|
|
115
|
+
// Previously a blank coerced to 0 via toNumberRV and silently flipped the
|
|
116
|
+
// function to exact-match mode — a behaviour gap vs. Excel's documented
|
|
117
|
+
// default.
|
|
118
|
+
const matchTypeV = args.length > 2 && args[2].kind !== RVKind.Blank ? toNumberRV(topLeft(args[2])) : rvNumber(1);
|
|
122
119
|
if (isError(matchTypeV)) {
|
|
123
120
|
return matchTypeV;
|
|
124
121
|
}
|
|
@@ -146,6 +143,12 @@ export function fnMATCH(args) {
|
|
|
146
143
|
wildcardRe = null;
|
|
147
144
|
}
|
|
148
145
|
}
|
|
146
|
+
// Pre-compute the literal (unescaped + lowercased) lookup string
|
|
147
|
+
// once. The old code called `unescapeExcelWildcard(lookupValue.value).toLowerCase()`
|
|
148
|
+
// inside the hot per-cell loop, paying O(n) per cell for what is
|
|
149
|
+
// a constant expression. (For ranges without strings the literal
|
|
150
|
+
// is never consulted — `scalarIsString(fi)` short-circuits first.)
|
|
151
|
+
const lookupLiteralLc = lookupStr !== null && !hasWildcard ? unescapeExcelWildcard(lookupStr).toLowerCase() : null;
|
|
149
152
|
for (let i = 0; i < flat.length; i++) {
|
|
150
153
|
if (scalarEquals(flat[i], lookupValue)) {
|
|
151
154
|
return rvNumber(i + 1);
|
|
@@ -157,7 +160,7 @@ export function fnMATCH(args) {
|
|
|
157
160
|
return rvNumber(i + 1);
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
|
-
else {
|
|
163
|
+
else if (lookupLiteralLc !== null) {
|
|
161
164
|
// No unescaped wildcard — but the pattern may still contain
|
|
162
165
|
// `~*` / `~?` / `~~` escape sequences that should reduce to
|
|
163
166
|
// their literal character before comparison. Calling
|
|
@@ -165,8 +168,7 @@ export function fnMATCH(args) {
|
|
|
165
168
|
// SEARCH and the criteria predicate use; without it,
|
|
166
169
|
// `MATCH("a~*b", ...)` would literally look for `"a~*b"`
|
|
167
170
|
// instead of `"a*b"`.
|
|
168
|
-
|
|
169
|
-
if (fi.value.toLowerCase() === literal) {
|
|
171
|
+
if (fi.value.toLowerCase() === lookupLiteralLc) {
|
|
170
172
|
return rvNumber(i + 1);
|
|
171
173
|
}
|
|
172
174
|
}
|
|
@@ -179,23 +181,18 @@ export function fnMATCH(args) {
|
|
|
179
181
|
let bestIdx = -1;
|
|
180
182
|
for (let i = 0; i < flat.length; i++) {
|
|
181
183
|
const v = flat[i];
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
184
|
+
if (v.kind !== lookupValue.kind) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const cmp = compareScalarsSameKind(v, lookupValue);
|
|
188
|
+
if (!Number.isFinite(cmp)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (cmp <= 0) {
|
|
192
|
+
bestIdx = i;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
break;
|
|
199
196
|
}
|
|
200
197
|
}
|
|
201
198
|
return bestIdx >= 0 ? rvNumber(bestIdx + 1) : ERRORS.NA;
|
|
@@ -204,23 +201,18 @@ export function fnMATCH(args) {
|
|
|
204
201
|
let bestIdx = -1;
|
|
205
202
|
for (let i = 0; i < flat.length; i++) {
|
|
206
203
|
const v = flat[i];
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
204
|
+
if (v.kind !== lookupValue.kind) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const cmp = compareScalarsSameKind(v, lookupValue);
|
|
208
|
+
if (!Number.isFinite(cmp)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (cmp >= 0) {
|
|
212
|
+
bestIdx = i;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
break;
|
|
224
216
|
}
|
|
225
217
|
}
|
|
226
218
|
return bestIdx >= 0 ? rvNumber(bestIdx + 1) : ERRORS.NA;
|
|
@@ -234,13 +226,18 @@ export function fnVLOOKUP(args) {
|
|
|
234
226
|
return ERRORS.NA;
|
|
235
227
|
}
|
|
236
228
|
const table = args[1];
|
|
237
|
-
const colIndexV = toNumberRV(args[2]);
|
|
229
|
+
const colIndexV = toNumberRV(topLeft(args[2]));
|
|
238
230
|
if (isError(colIndexV)) {
|
|
239
231
|
return colIndexV;
|
|
240
232
|
}
|
|
241
233
|
// VLOOKUP truncates the column index toward zero before bounds checks.
|
|
242
234
|
const colIndex = Math.trunc(colIndexV.value);
|
|
243
|
-
|
|
235
|
+
// Blank `range_lookup` → Excel default TRUE. A blank coerces through
|
|
236
|
+
// `toBooleanRV` to FALSE which silently flips to exact match —
|
|
237
|
+
// opposite of Excel's documented default.
|
|
238
|
+
const rangeLookupV = args.length > 3 && args[3].kind !== RVKind.Blank
|
|
239
|
+
? toBooleanRV(topLeft(args[3]))
|
|
240
|
+
: { kind: RVKind.Boolean, value: true };
|
|
244
241
|
if (isError(rangeLookupV)) {
|
|
245
242
|
return rangeLookupV;
|
|
246
243
|
}
|
|
@@ -249,39 +246,65 @@ export function fnVLOOKUP(args) {
|
|
|
249
246
|
return ERRORS.REF;
|
|
250
247
|
}
|
|
251
248
|
if (!rangeLookup) {
|
|
252
|
-
// Exact match
|
|
249
|
+
// Exact match — `scalarEquals` already handles case-insensitive string
|
|
250
|
+
// comparison (see runtime/values.ts), so the earlier `scalarStringEquals`
|
|
251
|
+
// fallback was dead code.
|
|
252
|
+
//
|
|
253
|
+
// Excel's VLOOKUP supports wildcards (`*`, `?`, `~*`, `~?`, `~~`) in
|
|
254
|
+
// the exact-match mode (range_lookup=FALSE). Three paths, chosen by
|
|
255
|
+
// what the lookup string contains:
|
|
256
|
+
// - unescaped wildcard (`*` or `?`) → regex match
|
|
257
|
+
// - only escape sequences (`~*`, `~?`, `~~`) → unescape then
|
|
258
|
+
// literal case-insensitive compare (so `"a~*b"` matches `"a*b"`)
|
|
259
|
+
// - neither → plain `scalarEquals`
|
|
260
|
+
const lookupStr = lookupValue.kind === RVKind.String ? lookupValue.value : null;
|
|
261
|
+
let wildcardRe = null;
|
|
262
|
+
let literalLc = null;
|
|
263
|
+
if (lookupStr !== null && hasUnescapedWildcard(lookupStr)) {
|
|
264
|
+
try {
|
|
265
|
+
wildcardRe = new RegExp("^" + excelWildcardToRegex(lookupStr) + "$", "i");
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
wildcardRe = null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (lookupStr !== null && /~[*?~]/.test(lookupStr)) {
|
|
272
|
+
literalLc = unescapeExcelWildcard(lookupStr).toLowerCase();
|
|
273
|
+
}
|
|
253
274
|
for (let r = 0; r < table.height; r++) {
|
|
254
275
|
const cell = getCell(table, r, 0);
|
|
255
276
|
if (scalarEquals(cell, lookupValue)) {
|
|
256
277
|
return getCell(table, r, colIndex - 1);
|
|
257
278
|
}
|
|
258
|
-
if (
|
|
279
|
+
if (wildcardRe && cell.kind === RVKind.String && wildcardRe.test(cell.value)) {
|
|
280
|
+
return getCell(table, r, colIndex - 1);
|
|
281
|
+
}
|
|
282
|
+
if (literalLc !== null &&
|
|
283
|
+
cell.kind === RVKind.String &&
|
|
284
|
+
cell.value.toLowerCase() === literalLc) {
|
|
259
285
|
return getCell(table, r, colIndex - 1);
|
|
260
286
|
}
|
|
261
287
|
}
|
|
262
288
|
return ERRORS.NA;
|
|
263
289
|
}
|
|
264
|
-
// Approximate match: sorted ascending by first column.
|
|
290
|
+
// Approximate match: sorted ascending by first column. Binary-search
|
|
291
|
+
// style isn't safe here (Excel allows mixed-type entries which break
|
|
292
|
+
// monotonicity), so walk until we overshoot.
|
|
265
293
|
let bestRow = -1;
|
|
266
294
|
for (let r = 0; r < table.height; r++) {
|
|
267
295
|
const v = getCell(table, r, 0);
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
296
|
+
if (v.kind !== lookupValue.kind) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const cmp = compareScalarsSameKind(v, lookupValue);
|
|
300
|
+
if (!Number.isFinite(cmp)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (cmp <= 0) {
|
|
304
|
+
bestRow = r;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
break;
|
|
285
308
|
}
|
|
286
309
|
}
|
|
287
310
|
return bestRow >= 0 ? getCell(table, bestRow, colIndex - 1) : ERRORS.NA;
|
|
@@ -295,13 +318,16 @@ export function fnHLOOKUP(args) {
|
|
|
295
318
|
return ERRORS.NA;
|
|
296
319
|
}
|
|
297
320
|
const table = args[1];
|
|
298
|
-
const rowIndexV = toNumberRV(args[2]);
|
|
321
|
+
const rowIndexV = toNumberRV(topLeft(args[2]));
|
|
299
322
|
if (isError(rowIndexV)) {
|
|
300
323
|
return rowIndexV;
|
|
301
324
|
}
|
|
302
325
|
// HLOOKUP truncates the row index toward zero before bounds checks.
|
|
303
326
|
const rowIndex = Math.trunc(rowIndexV.value);
|
|
304
|
-
|
|
327
|
+
// Blank `range_lookup` → Excel default TRUE (see VLOOKUP rationale).
|
|
328
|
+
const rangeLookupV = args.length > 3 && args[3].kind !== RVKind.Blank
|
|
329
|
+
? toBooleanRV(topLeft(args[3]))
|
|
330
|
+
: { kind: RVKind.Boolean, value: true };
|
|
305
331
|
if (isError(rangeLookupV)) {
|
|
306
332
|
return rangeLookupV;
|
|
307
333
|
}
|
|
@@ -310,8 +336,33 @@ export function fnHLOOKUP(args) {
|
|
|
310
336
|
return ERRORS.REF;
|
|
311
337
|
}
|
|
312
338
|
if (!rangeLookup) {
|
|
339
|
+
// Exact match — supports wildcards on string lookups (see VLOOKUP
|
|
340
|
+
// for full rationale; the paths mirror one another).
|
|
341
|
+
const lookupStr = lookupValue.kind === RVKind.String ? lookupValue.value : null;
|
|
342
|
+
let wildcardRe = null;
|
|
343
|
+
let literalLc = null;
|
|
344
|
+
if (lookupStr !== null && hasUnescapedWildcard(lookupStr)) {
|
|
345
|
+
try {
|
|
346
|
+
wildcardRe = new RegExp("^" + excelWildcardToRegex(lookupStr) + "$", "i");
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
wildcardRe = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (lookupStr !== null && /~[*?~]/.test(lookupStr)) {
|
|
353
|
+
literalLc = unescapeExcelWildcard(lookupStr).toLowerCase();
|
|
354
|
+
}
|
|
313
355
|
for (let c = 0; c < table.width; c++) {
|
|
314
|
-
|
|
356
|
+
const cell = getCell(table, 0, c);
|
|
357
|
+
if (scalarEquals(cell, lookupValue)) {
|
|
358
|
+
return getCell(table, rowIndex - 1, c);
|
|
359
|
+
}
|
|
360
|
+
if (wildcardRe && cell.kind === RVKind.String && wildcardRe.test(cell.value)) {
|
|
361
|
+
return getCell(table, rowIndex - 1, c);
|
|
362
|
+
}
|
|
363
|
+
if (literalLc !== null &&
|
|
364
|
+
cell.kind === RVKind.String &&
|
|
365
|
+
cell.value.toLowerCase() === literalLc) {
|
|
315
366
|
return getCell(table, rowIndex - 1, c);
|
|
316
367
|
}
|
|
317
368
|
}
|
|
@@ -320,18 +371,18 @@ export function fnHLOOKUP(args) {
|
|
|
320
371
|
let bestCol = -1;
|
|
321
372
|
for (let c = 0; c < table.width; c++) {
|
|
322
373
|
const hv = getCell(table, 0, c);
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
374
|
+
if (hv.kind !== lookupValue.kind) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const cmp = compareScalarsSameKind(hv, lookupValue);
|
|
378
|
+
if (!Number.isFinite(cmp)) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
// Approximate match: pick the largest value <= lookupValue. We don't
|
|
382
|
+
// break early when we overshoot because HLOOKUP's legacy behaviour
|
|
383
|
+
// scans the whole row even for unsorted data.
|
|
384
|
+
if (cmp <= 0) {
|
|
385
|
+
bestCol = c;
|
|
335
386
|
}
|
|
336
387
|
}
|
|
337
388
|
return bestCol >= 0 ? getCell(table, rowIndex - 1, bestCol) : ERRORS.NA;
|
|
@@ -349,17 +400,30 @@ export function fnXLOOKUP(args) {
|
|
|
349
400
|
return ERRORS.VALUE;
|
|
350
401
|
}
|
|
351
402
|
const returnArr = args[2];
|
|
352
|
-
|
|
353
|
-
|
|
403
|
+
// Blank `if_not_found` → treat as omitted (default #N/A). Without
|
|
404
|
+
// this guard, an explicitly-blank fourth slot would produce BLANK as
|
|
405
|
+
// the fallback, which differs from Excel's "omitted → #N/A" default
|
|
406
|
+
// and from what a user intuitively expects.
|
|
407
|
+
const ifNotFound = args.length > 3 && args[3].kind !== RVKind.Blank ? topLeft(args[3]) : null;
|
|
408
|
+
// Blank `match_mode` → 0 (exact); any non-{-1, 0, 1, 2} is rejected.
|
|
409
|
+
const matchModeV = args.length > 4 && args[4].kind !== RVKind.Blank ? toNumberRV(topLeft(args[4])) : rvNumber(0);
|
|
354
410
|
if (isError(matchModeV)) {
|
|
355
411
|
return matchModeV;
|
|
356
412
|
}
|
|
357
413
|
const matchMode = matchModeV.value;
|
|
358
|
-
|
|
414
|
+
if (matchMode !== 0 && matchMode !== -1 && matchMode !== 1 && matchMode !== 2) {
|
|
415
|
+
return ERRORS.VALUE;
|
|
416
|
+
}
|
|
417
|
+
// Blank `search_mode` → 1 (first-to-last). Previously a blank coerced
|
|
418
|
+
// to 0 which silently passed through but is not a valid search mode.
|
|
419
|
+
const searchModeV = args.length > 5 && args[5].kind !== RVKind.Blank ? toNumberRV(topLeft(args[5])) : rvNumber(1);
|
|
359
420
|
if (isError(searchModeV)) {
|
|
360
421
|
return searchModeV;
|
|
361
422
|
}
|
|
362
423
|
const searchMode = searchModeV.value;
|
|
424
|
+
if (searchMode !== 1 && searchMode !== -1 && searchMode !== 2 && searchMode !== -2) {
|
|
425
|
+
return ERRORS.VALUE;
|
|
426
|
+
}
|
|
363
427
|
// Flatten lookup array to 1D
|
|
364
428
|
const flat = [];
|
|
365
429
|
const isRow = lookupArr.height === 1;
|
|
@@ -464,10 +528,6 @@ export function fnXLOOKUP(args) {
|
|
|
464
528
|
foundIdx = i;
|
|
465
529
|
break;
|
|
466
530
|
}
|
|
467
|
-
if (scalarStringEquals(flat[i], lookupValue)) {
|
|
468
|
-
foundIdx = i;
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
531
|
}
|
|
472
532
|
}
|
|
473
533
|
else if (matchMode === -1) {
|
|
@@ -570,16 +630,23 @@ export function fnXMATCH(args) {
|
|
|
570
630
|
return ERRORS.VALUE;
|
|
571
631
|
}
|
|
572
632
|
const lookupArr = args[1];
|
|
573
|
-
|
|
633
|
+
// Blank `match_mode` → 0 (exact). Same validation as XLOOKUP.
|
|
634
|
+
const matchModeV = args.length > 2 && args[2].kind !== RVKind.Blank ? toNumberRV(topLeft(args[2])) : rvNumber(0);
|
|
574
635
|
if (isError(matchModeV)) {
|
|
575
636
|
return matchModeV;
|
|
576
637
|
}
|
|
577
638
|
const matchMode = matchModeV.value;
|
|
578
|
-
|
|
639
|
+
if (matchMode !== 0 && matchMode !== -1 && matchMode !== 1 && matchMode !== 2) {
|
|
640
|
+
return ERRORS.VALUE;
|
|
641
|
+
}
|
|
642
|
+
const searchModeV = args.length > 3 && args[3].kind !== RVKind.Blank ? toNumberRV(topLeft(args[3])) : rvNumber(1);
|
|
579
643
|
if (isError(searchModeV)) {
|
|
580
644
|
return searchModeV;
|
|
581
645
|
}
|
|
582
646
|
const searchMode = searchModeV.value;
|
|
647
|
+
if (searchMode !== 1 && searchMode !== -1 && searchMode !== 2 && searchMode !== -2) {
|
|
648
|
+
return ERRORS.VALUE;
|
|
649
|
+
}
|
|
583
650
|
const flat = [];
|
|
584
651
|
if (lookupArr.height === 1) {
|
|
585
652
|
for (let c = 0; c < lookupArr.width; c++) {
|
|
@@ -599,9 +666,6 @@ export function fnXMATCH(args) {
|
|
|
599
666
|
if (scalarEquals(flat[i], lookupValue)) {
|
|
600
667
|
return rvNumber(i + 1);
|
|
601
668
|
}
|
|
602
|
-
if (scalarStringEquals(flat[i], lookupValue)) {
|
|
603
|
-
return rvNumber(i + 1);
|
|
604
|
-
}
|
|
605
669
|
}
|
|
606
670
|
return ERRORS.NA;
|
|
607
671
|
}
|
|
@@ -649,11 +713,11 @@ export function fnXMATCH(args) {
|
|
|
649
713
|
// Next-smaller-or-equal: largest item <= lookupValue.
|
|
650
714
|
let best = -1;
|
|
651
715
|
for (let i = 0; i < flat.length; i++) {
|
|
652
|
-
const cmp =
|
|
716
|
+
const cmp = compareScalarsSameKind(flat[i], lookupValue);
|
|
653
717
|
if (Number.isNaN(cmp)) {
|
|
654
718
|
continue;
|
|
655
719
|
}
|
|
656
|
-
if (cmp <= 0 && (best === -1 ||
|
|
720
|
+
if (cmp <= 0 && (best === -1 || compareScalarsSameKind(flat[i], flat[best]) > 0)) {
|
|
657
721
|
best = i;
|
|
658
722
|
}
|
|
659
723
|
}
|
|
@@ -663,11 +727,11 @@ export function fnXMATCH(args) {
|
|
|
663
727
|
// Next-larger-or-equal: smallest item >= lookupValue.
|
|
664
728
|
let best = -1;
|
|
665
729
|
for (let i = 0; i < flat.length; i++) {
|
|
666
|
-
const cmp =
|
|
730
|
+
const cmp = compareScalarsSameKind(flat[i], lookupValue);
|
|
667
731
|
if (Number.isNaN(cmp)) {
|
|
668
732
|
continue;
|
|
669
733
|
}
|
|
670
|
-
if (cmp >= 0 && (best === -1 ||
|
|
734
|
+
if (cmp >= 0 && (best === -1 || compareScalarsSameKind(flat[i], flat[best]) < 0)) {
|
|
671
735
|
best = i;
|
|
672
736
|
}
|
|
673
737
|
}
|
|
@@ -676,12 +740,12 @@ export function fnXMATCH(args) {
|
|
|
676
740
|
return ERRORS.NA;
|
|
677
741
|
}
|
|
678
742
|
export function fnADDRESS(args) {
|
|
679
|
-
const rowNumV = toNumberRV(args[0]);
|
|
743
|
+
const rowNumV = toNumberRV(topLeft(args[0]));
|
|
680
744
|
if (isError(rowNumV)) {
|
|
681
745
|
return rowNumV;
|
|
682
746
|
}
|
|
683
747
|
const rowNum = Math.trunc(rowNumV.value);
|
|
684
|
-
const colNumV = toNumberRV(args[1]);
|
|
748
|
+
const colNumV = toNumberRV(topLeft(args[1]));
|
|
685
749
|
if (isError(colNumV)) {
|
|
686
750
|
return colNumV;
|
|
687
751
|
}
|
|
@@ -692,7 +756,10 @@ export function fnADDRESS(args) {
|
|
|
692
756
|
if (!Number.isFinite(rowNum) || !Number.isFinite(colNum) || rowNum < 1 || colNum < 1) {
|
|
693
757
|
return ERRORS.VALUE;
|
|
694
758
|
}
|
|
695
|
-
|
|
759
|
+
// Blank `abs_num` → Excel default 1 (fully absolute). Without the
|
|
760
|
+
// blank guard, `toNumberRV(BLANK)` coerces to 0 which falls outside
|
|
761
|
+
// the 1..4 range and surfaces a spurious #VALUE!.
|
|
762
|
+
const absNumV = args.length > 2 && args[2].kind !== RVKind.Blank ? toNumberRV(topLeft(args[2])) : rvNumber(1);
|
|
696
763
|
if (isError(absNumV)) {
|
|
697
764
|
return absNumV;
|
|
698
765
|
}
|
|
@@ -704,7 +771,7 @@ export function fnADDRESS(args) {
|
|
|
704
771
|
// a1 style (true/default) vs r1c1 (false)
|
|
705
772
|
const a1Arg = args.length > 3 ? topLeft(args[3]) : { kind: RVKind.Boolean, value: true };
|
|
706
773
|
const a1 = a1Arg.kind === RVKind.Boolean ? a1Arg.value : true;
|
|
707
|
-
const sheetText = args.length > 4 ? toStringRV(args[4]) : "";
|
|
774
|
+
const sheetText = args.length > 4 ? toStringRV(topLeft(args[4])) : "";
|
|
708
775
|
if (!a1) {
|
|
709
776
|
// R1C1 style
|
|
710
777
|
const rPart = absNum === 1 || absNum === 2 ? `R${rowNum}` : `R[${rowNum}]`;
|
|
@@ -777,20 +844,7 @@ export function fnLOOKUP(args) {
|
|
|
777
844
|
flat.push(getCell(lookupArr, r, 0));
|
|
778
845
|
}
|
|
779
846
|
}
|
|
780
|
-
|
|
781
|
-
for (let i = 0; i < flat.length; i++) {
|
|
782
|
-
const v = flat[i];
|
|
783
|
-
if (sameType(v, lookupValue)) {
|
|
784
|
-
if (scalarIsNumber(v) && scalarIsNumber(lookupValue) && v.value <= lookupValue.value) {
|
|
785
|
-
bestIdx = i;
|
|
786
|
-
}
|
|
787
|
-
else if (scalarIsString(v) &&
|
|
788
|
-
scalarIsString(lookupValue) &&
|
|
789
|
-
v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
|
|
790
|
-
bestIdx = i;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
847
|
+
const bestIdx = findLastLessEqual(flat, lookupValue);
|
|
794
848
|
if (bestIdx === -1) {
|
|
795
849
|
return ERRORS.NA;
|
|
796
850
|
}
|
|
@@ -811,38 +865,48 @@ export function fnLOOKUP(args) {
|
|
|
811
865
|
return ERRORS.NA;
|
|
812
866
|
}
|
|
813
867
|
if (cols >= rows) {
|
|
814
|
-
|
|
868
|
+
// Array-form LOOKUP, horizontal orientation: lookup runs along first
|
|
869
|
+
// row; result is pulled from the last row of the same column.
|
|
870
|
+
const firstRow = [];
|
|
815
871
|
for (let c = 0; c < cols; c++) {
|
|
816
|
-
|
|
817
|
-
if (sameType(v, lookupValue)) {
|
|
818
|
-
if (scalarIsNumber(v) && scalarIsNumber(lookupValue) && v.value <= lookupValue.value) {
|
|
819
|
-
bestIdx = c;
|
|
820
|
-
}
|
|
821
|
-
else if (scalarIsString(v) &&
|
|
822
|
-
scalarIsString(lookupValue) &&
|
|
823
|
-
v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
|
|
824
|
-
bestIdx = c;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
872
|
+
firstRow.push(getCell(lookupArr, 0, c));
|
|
827
873
|
}
|
|
874
|
+
const bestIdx = findLastLessEqual(firstRow, lookupValue);
|
|
828
875
|
return bestIdx >= 0 ? getCell(lookupArr, rows - 1, bestIdx) : ERRORS.NA;
|
|
829
876
|
}
|
|
830
|
-
|
|
877
|
+
// Array-form LOOKUP, vertical orientation: lookup runs down first column;
|
|
878
|
+
// result is pulled from the last column of the same row.
|
|
879
|
+
const firstCol = [];
|
|
831
880
|
for (let r = 0; r < rows; r++) {
|
|
832
|
-
|
|
833
|
-
if (sameType(v, lookupValue)) {
|
|
834
|
-
if (scalarIsNumber(v) && scalarIsNumber(lookupValue) && v.value <= lookupValue.value) {
|
|
835
|
-
bestIdx = r;
|
|
836
|
-
}
|
|
837
|
-
else if (scalarIsString(v) &&
|
|
838
|
-
scalarIsString(lookupValue) &&
|
|
839
|
-
v.value.toLowerCase() <= lookupValue.value.toLowerCase()) {
|
|
840
|
-
bestIdx = r;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
881
|
+
firstCol.push(getCell(lookupArr, r, 0));
|
|
843
882
|
}
|
|
883
|
+
const bestIdx = findLastLessEqual(firstCol, lookupValue);
|
|
844
884
|
return bestIdx >= 0 ? getCell(lookupArr, bestIdx, cols - 1) : ERRORS.NA;
|
|
845
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* Linear scan for the largest same-kind value that is `<= target`.
|
|
888
|
+
* Returns the flat-index of that value, or `-1` when no same-kind
|
|
889
|
+
* value qualifies. Uses `compareScalarsSameKind` so numbers compare by
|
|
890
|
+
* value, strings case-insensitively — the same ordering Excel uses
|
|
891
|
+
* for legacy LOOKUP / VLOOKUP / HLOOKUP approximate matches.
|
|
892
|
+
*/
|
|
893
|
+
function findLastLessEqual(flat, target) {
|
|
894
|
+
let bestIdx = -1;
|
|
895
|
+
for (let i = 0; i < flat.length; i++) {
|
|
896
|
+
const v = flat[i];
|
|
897
|
+
if (v.kind !== target.kind) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const cmp = compareScalarsSameKind(v, target);
|
|
901
|
+
if (!Number.isFinite(cmp)) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (cmp <= 0) {
|
|
905
|
+
bestIdx = i;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return bestIdx;
|
|
909
|
+
}
|
|
846
910
|
export function fnTRANSPOSE(args) {
|
|
847
911
|
if (!isArray(args[0])) {
|
|
848
912
|
const sv = topLeft(args[0]);
|
|
@@ -871,10 +935,13 @@ export function fnAREAS(args) {
|
|
|
871
935
|
if (args.length === 0) {
|
|
872
936
|
return ERRORS.VALUE;
|
|
873
937
|
}
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
938
|
+
// Normally unreachable — the evaluator's reference-aware path in
|
|
939
|
+
// `evaluateCall` intercepts AREAS before eager dereference happens,
|
|
940
|
+
// so by the time this fallback runs the reference has already been
|
|
941
|
+
// flattened into a dereferenced array (losing the area count).
|
|
942
|
+
// Keep the fallback behaviour aligned with the intercept path:
|
|
943
|
+
// arrays and scalars that reach here are not references and should
|
|
944
|
+
// surface as `#VALUE!`.
|
|
878
945
|
const a = args[0];
|
|
879
946
|
if (a.kind === RVKind.Error) {
|
|
880
947
|
return a;
|
|
@@ -882,5 +949,5 @@ export function fnAREAS(args) {
|
|
|
882
949
|
if (a.kind === RVKind.Reference) {
|
|
883
950
|
return rvNumber(a.areas.length);
|
|
884
951
|
}
|
|
885
|
-
return
|
|
952
|
+
return ERRORS.VALUE;
|
|
886
953
|
}
|
|
@@ -42,7 +42,33 @@ export declare const fnPRODUCT: NativeFn;
|
|
|
42
42
|
export declare const fnSUMPRODUCT: NativeFn;
|
|
43
43
|
export declare const fnABS: NativeFn;
|
|
44
44
|
export declare const fnCEILING: NativeFn;
|
|
45
|
+
/**
|
|
46
|
+
* CEILING.MATH(number, [significance], [mode]) — rounds away from zero
|
|
47
|
+
* by default, or toward zero when `mode` is non-zero AND `number` is
|
|
48
|
+
* negative. Significance is always interpreted by absolute value.
|
|
49
|
+
*
|
|
50
|
+
* Different from CEILING: negative numbers with positive significance
|
|
51
|
+
* are valid (Excel does NOT require same sign), and there is an extra
|
|
52
|
+
* `mode` switch that flips the rounding direction for negatives.
|
|
53
|
+
*/
|
|
54
|
+
export declare const fnCEILING_MATH: NativeFn;
|
|
55
|
+
/**
|
|
56
|
+
* CEILING.PRECISE / ISO.CEILING — always rounds toward +∞ (irrespective
|
|
57
|
+
* of sign), using the absolute value of significance.
|
|
58
|
+
*/
|
|
59
|
+
export declare const fnCEILING_PRECISE: NativeFn;
|
|
45
60
|
export declare const fnFLOOR: NativeFn;
|
|
61
|
+
/**
|
|
62
|
+
* FLOOR.MATH(number, [significance], [mode]) — rounds toward zero by
|
|
63
|
+
* default, or away from zero when `mode` is non-zero AND `number` is
|
|
64
|
+
* negative. Uses `|significance|` so negative significance never
|
|
65
|
+
* produces #NUM!.
|
|
66
|
+
*/
|
|
67
|
+
export declare const fnFLOOR_MATH: NativeFn;
|
|
68
|
+
/**
|
|
69
|
+
* FLOOR.PRECISE — always rounds toward −∞ using `|significance|`.
|
|
70
|
+
*/
|
|
71
|
+
export declare const fnFLOOR_PRECISE: NativeFn;
|
|
46
72
|
export declare const fnINT: NativeFn;
|
|
47
73
|
export declare const fnMOD: NativeFn;
|
|
48
74
|
export declare const fnPOWER: NativeFn;
|