@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
|
@@ -124,9 +124,13 @@ function evaluate(expr, ctx, session) {
|
|
|
124
124
|
case 1 /* BoundExprKind.Literal */:
|
|
125
125
|
return evaluateLiteral(expr);
|
|
126
126
|
case 2 /* BoundExprKind.CellRef */:
|
|
127
|
-
|
|
127
|
+
// Inlined: the wrapper function did nothing beyond forwarding.
|
|
128
|
+
// Cell refs are the hottest AST node in the evaluator; saving one
|
|
129
|
+
// call frame per reference meaningfully shortens the trace for
|
|
130
|
+
// workbooks with tens of thousands of cells.
|
|
131
|
+
return (0, values_1.rvCellRef)(expr.sheet, expr.row, expr.col);
|
|
128
132
|
case 3 /* BoundExprKind.AreaRef */:
|
|
129
|
-
return
|
|
133
|
+
return (0, values_1.rvRef)(expr.sheet, expr.top, expr.left, expr.bottom, expr.right);
|
|
130
134
|
case 4 /* BoundExprKind.ColRangeRef */:
|
|
131
135
|
return evaluateColRange(expr, ctx, session);
|
|
132
136
|
case 5 /* BoundExprKind.RowRangeRef */:
|
|
@@ -151,6 +155,8 @@ function evaluate(expr, ctx, session) {
|
|
|
151
155
|
return evaluateLambdaExpr(expr, ctx);
|
|
152
156
|
case 15 /* BoundExprKind.StructuredRef */:
|
|
153
157
|
return evaluateStructuredRef(expr, ctx, session);
|
|
158
|
+
case 16 /* BoundExprKind.UnionRef */:
|
|
159
|
+
return evaluateUnionRef(expr, ctx, session);
|
|
154
160
|
default:
|
|
155
161
|
return assertNever(expr);
|
|
156
162
|
}
|
|
@@ -179,15 +185,13 @@ function evaluateLiteral(expr) {
|
|
|
179
185
|
// ============================================================================
|
|
180
186
|
// Cell Reference
|
|
181
187
|
// ============================================================================
|
|
182
|
-
function evaluateCellRef(expr, ctx, session) {
|
|
183
|
-
return (0, values_1.rvCellRef)(expr.sheet, expr.row, expr.col);
|
|
184
|
-
}
|
|
185
188
|
// ============================================================================
|
|
186
|
-
// Area Reference
|
|
189
|
+
// Cell / Area Reference — inlined at the `evaluate` switch above.
|
|
187
190
|
// ============================================================================
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
// Single-arg wrappers used to exist here (`evaluateCellRef` / `evaluateAreaRef`);
|
|
192
|
+
// they did nothing beyond forwarding to `rvCellRef` / `rvRef`. Since the
|
|
193
|
+
// evaluator dispatches these on every cell in every formula, we inline
|
|
194
|
+
// them at the caller to shave one call frame.
|
|
191
195
|
function evaluateColRange(expr, ctx, session) {
|
|
192
196
|
const ws = ctx.snapshot.worksheetsByName.get(expr.sheet.toLowerCase());
|
|
193
197
|
if (!ws || !ws.dimensions) {
|
|
@@ -257,23 +261,47 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
257
261
|
const ws = ctx.snapshot.worksheetsByName.get(sheet.toLowerCase());
|
|
258
262
|
const cells = ws?.cells;
|
|
259
263
|
const wsHiddenRows = ws?.hiddenRows;
|
|
264
|
+
// Use the canonical sheet name for cache-key computation so that a
|
|
265
|
+
// mis-cased sheet identifier (e.g. `sheet1` when the workbook has
|
|
266
|
+
// `Sheet1`) doesn't bypass the compiled-formula / result caches. The
|
|
267
|
+
// map is keyed by snapshot.worksheets[].name, so any divergence would
|
|
268
|
+
// cause spurious re-compilation.
|
|
269
|
+
const canonicalSheet = ws?.name ?? sheet;
|
|
260
270
|
const compiledFormulas = ctx.compiledFormulas;
|
|
261
271
|
const resultCache = session.resultCache;
|
|
262
272
|
// Hoist the recording guard — when not recording, skip recordAccess
|
|
263
273
|
// entirely in the loop instead of paying the function-call overhead.
|
|
264
274
|
const recording = session.recordingKey !== null;
|
|
265
|
-
const
|
|
275
|
+
const height = bottom - top + 1;
|
|
276
|
+
const width = right - left + 1;
|
|
277
|
+
// Missing worksheet: emit an all-BLANK rectangle without entering the
|
|
278
|
+
// hot path. The tokenizer and binder usually report these as #REF! at
|
|
279
|
+
// compile time, but `evaluateRef3D` and runtime INDIRECT can still
|
|
280
|
+
// synthesise refs into non-existent sheets.
|
|
281
|
+
if (!cells) {
|
|
282
|
+
const rows = new Array(height);
|
|
283
|
+
for (let r = 0; r < height; r++) {
|
|
284
|
+
rows[r] = new Array(width).fill(values_1.BLANK);
|
|
285
|
+
}
|
|
286
|
+
if (recording) {
|
|
287
|
+
for (let r = top; r <= bottom; r++) {
|
|
288
|
+
for (let c = left; c <= right; c++) {
|
|
289
|
+
session.recordAccess(sheet, r, c);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return (0, values_1.rvArrayRect)(rows, height, width, top, left);
|
|
294
|
+
}
|
|
295
|
+
const rows = new Array(height);
|
|
266
296
|
// Lazily-allocated masks: only materialized once we encounter a
|
|
267
297
|
// SUBTOTAL/AGGREGATE cell or a hidden row inside the range. For the
|
|
268
298
|
// common case (plain data range, no hidden rows) we never touch these
|
|
269
299
|
// and emit an ArrayValue without extra metadata.
|
|
270
300
|
let subtotalMask;
|
|
271
301
|
let hiddenRowMask;
|
|
272
|
-
const height = bottom - top + 1;
|
|
273
|
-
const width = right - left + 1;
|
|
274
302
|
for (let r = top; r <= bottom; r++) {
|
|
275
|
-
const row = [];
|
|
276
303
|
const ri = r - top;
|
|
304
|
+
const row = new Array(width);
|
|
277
305
|
// Record row visibility — SUBTOTAL 1xx / AGGREGATE opt 5/7 use it.
|
|
278
306
|
if (wsHiddenRows?.has(r)) {
|
|
279
307
|
if (!hiddenRowMask) {
|
|
@@ -282,25 +310,21 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
282
310
|
hiddenRowMask[ri] = true;
|
|
283
311
|
}
|
|
284
312
|
for (let c = left; c <= right; c++) {
|
|
313
|
+
const ci = c - left;
|
|
285
314
|
if (recording) {
|
|
286
|
-
session.recordAccess(
|
|
287
|
-
}
|
|
288
|
-
// Missing worksheet: matches getCellValue's BLANK fallback.
|
|
289
|
-
if (!cells) {
|
|
290
|
-
row.push(values_1.BLANK);
|
|
291
|
-
continue;
|
|
315
|
+
session.recordAccess(canonicalSheet, r, c);
|
|
292
316
|
}
|
|
293
317
|
const cell = cells.get((0, workbook_snapshot_1.snapshotCellKey)(r, c));
|
|
294
318
|
if (!cell) {
|
|
295
319
|
// No snapshot cell yet — might still be inside a live spill
|
|
296
320
|
// (e.g. reading A2..A5 while A1 = SEQUENCE(5) is still being
|
|
297
321
|
// materialized). See `readLiveSpill` for the lookup path.
|
|
298
|
-
const live = readLiveSpill(
|
|
299
|
-
row
|
|
322
|
+
const live = readLiveSpill(canonicalSheet, r, c, session);
|
|
323
|
+
row[ci] = live ? (0, values_1.topLeft)(live) : values_1.BLANK;
|
|
300
324
|
continue;
|
|
301
325
|
}
|
|
302
326
|
if (cell.formulaKind !== "none" && cell.formula) {
|
|
303
|
-
const fKey = (0, workbook_snapshot_1.formulaCellKey)(
|
|
327
|
+
const fKey = (0, workbook_snapshot_1.formulaCellKey)(canonicalSheet, r, c);
|
|
304
328
|
const compiled = compiledFormulas.get(fKey);
|
|
305
329
|
// Mark SUBTOTAL/AGGREGATE output cells so an outer SUBTOTAL /
|
|
306
330
|
// AGGREGATE over this range knows to skip them (Excel's
|
|
@@ -312,15 +336,15 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
312
336
|
subtotalMask[i] = new Array(width).fill(false);
|
|
313
337
|
}
|
|
314
338
|
}
|
|
315
|
-
subtotalMask[ri][
|
|
339
|
+
subtotalMask[ri][ci] = true;
|
|
316
340
|
}
|
|
317
341
|
const cached = resultCache.get(fKey);
|
|
318
342
|
if (cached !== undefined) {
|
|
319
|
-
row
|
|
343
|
+
row[ci] = (0, values_1.topLeft)(cached.scalar);
|
|
320
344
|
continue;
|
|
321
345
|
}
|
|
322
346
|
if (compiled) {
|
|
323
|
-
row
|
|
347
|
+
row[ci] = (0, values_1.topLeft)(evaluateFormula(compiled, ctx, session));
|
|
324
348
|
continue;
|
|
325
349
|
}
|
|
326
350
|
}
|
|
@@ -328,16 +352,16 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
328
352
|
// that a fresh dynamic-array spill is about to overwrite. Prefer
|
|
329
353
|
// the live value when a master is registered so this-pass SUM /
|
|
330
354
|
// LOOKUP / etc. see the new spill immediately.
|
|
331
|
-
const live = readLiveSpill(
|
|
355
|
+
const live = readLiveSpill(canonicalSheet, r, c, session);
|
|
332
356
|
if (live) {
|
|
333
|
-
row
|
|
357
|
+
row[ci] = (0, values_1.topLeft)(live);
|
|
334
358
|
continue;
|
|
335
359
|
}
|
|
336
|
-
row
|
|
360
|
+
row[ci] = (0, values_1.topLeft)((0, values_1.fromSnapshotValue)(cell.value));
|
|
337
361
|
}
|
|
338
|
-
rows
|
|
362
|
+
rows[ri] = row;
|
|
339
363
|
}
|
|
340
|
-
return (0, values_1.
|
|
364
|
+
return (0, values_1.rvArrayRect)(rows, height, width, top, left, subtotalMask, hiddenRowMask);
|
|
341
365
|
}
|
|
342
366
|
// ============================================================================
|
|
343
367
|
// Dereference: Reference → concrete value
|
|
@@ -436,15 +460,25 @@ function dereferenceValue(v, ctx, session) {
|
|
|
436
460
|
// Get Cell Value from Snapshot
|
|
437
461
|
// ============================================================================
|
|
438
462
|
function getCellValue(sheetName, row, col, ctx, session) {
|
|
463
|
+
const ws = ctx.snapshot.worksheetsByName.get(sheetName.toLowerCase());
|
|
464
|
+
if (!ws) {
|
|
465
|
+
// Record the access before returning — still a valid dependency
|
|
466
|
+
// edge even if the sheet is missing (it'll surface as BLANK
|
|
467
|
+
// downstream, but an INDIRECT-produced read should still register).
|
|
468
|
+
if (session.recordingKey !== null) {
|
|
469
|
+
session.recordAccess(sheetName, row, col);
|
|
470
|
+
}
|
|
471
|
+
return values_1.BLANK;
|
|
472
|
+
}
|
|
473
|
+
// Normalise to the canonical sheet name so downstream cache keys
|
|
474
|
+
// (compiled formulas, result cache, spill map) all hit the same
|
|
475
|
+
// entries regardless of how the caller cased the input.
|
|
476
|
+
const canonicalSheet = ws.name;
|
|
439
477
|
// Record this access for runtime dependency tracking. Inline guard
|
|
440
478
|
// avoids the function call overhead in the common case where no
|
|
441
479
|
// recording is active (formulas without dynamic refs).
|
|
442
480
|
if (session.recordingKey !== null) {
|
|
443
|
-
session.recordAccess(
|
|
444
|
-
}
|
|
445
|
-
const ws = ctx.snapshot.worksheetsByName.get(sheetName.toLowerCase());
|
|
446
|
-
if (!ws) {
|
|
447
|
-
return values_1.BLANK;
|
|
481
|
+
session.recordAccess(canonicalSheet, row, col);
|
|
448
482
|
}
|
|
449
483
|
const cellKey = (0, workbook_snapshot_1.snapshotCellKey)(row, col);
|
|
450
484
|
const cell = ws.cells.get(cellKey);
|
|
@@ -454,11 +488,18 @@ function getCellValue(sheetName, row, col, ctx, session) {
|
|
|
454
488
|
// right array element. Matters when a downstream formula like
|
|
455
489
|
// `SUM(A1:A5)` runs before materialize writes the ghost cells for
|
|
456
490
|
// `A1 = SEQUENCE(5)` into the snapshot.
|
|
457
|
-
return readLiveSpill(
|
|
491
|
+
return readLiveSpill(canonicalSheet, row, col, session) ?? values_1.BLANK;
|
|
458
492
|
}
|
|
459
493
|
// If this cell has a formula, evaluate it
|
|
460
494
|
if (cell.formulaKind !== "none" && cell.formula) {
|
|
461
|
-
|
|
495
|
+
// Use `ws.name` (the snapshot's canonical case) rather than the
|
|
496
|
+
// caller's `sheetName`, which could arrive mis-cased (e.g. the
|
|
497
|
+
// user wrote `sheet1!A1` but the workbook has `Sheet1`). The
|
|
498
|
+
// compiled-formula map is keyed by the canonical form, so a
|
|
499
|
+
// mis-cased key would produce a spurious cache-miss + re-compile
|
|
500
|
+
// per read — and worse, a different key than the one the
|
|
501
|
+
// write-back cycle will later upsert.
|
|
502
|
+
const fKey = (0, workbook_snapshot_1.formulaCellKey)(canonicalSheet, row, col);
|
|
462
503
|
// Check cache — return scalar form for dependency resolution
|
|
463
504
|
const cached = session.resultCache.get(fKey);
|
|
464
505
|
if (cached !== undefined) {
|
|
@@ -474,7 +515,7 @@ function getCellValue(sheetName, row, col, ctx, session) {
|
|
|
474
515
|
// (e.g. a value that exists in the snapshot from a previous calc
|
|
475
516
|
// cycle but is about to be overwritten by a fresh spill). Prefer the
|
|
476
517
|
// live value when a master is registered.
|
|
477
|
-
const spill = readLiveSpill(
|
|
518
|
+
const spill = readLiveSpill(canonicalSheet, row, col, session);
|
|
478
519
|
if (spill) {
|
|
479
520
|
return spill;
|
|
480
521
|
}
|
|
@@ -798,45 +839,45 @@ function applyScalarBinaryOp(op, left, right) {
|
|
|
798
839
|
return !isFinite(result) ? values_1.ERRORS.NUM : (0, values_1.rvNumber)(result);
|
|
799
840
|
}
|
|
800
841
|
function compareScalars(left, right, op) {
|
|
801
|
-
// Normalize blanks to a neutral form of the opposing kind so formulas like
|
|
802
|
-
// `"" = A1` (where A1 is blank) compare equal. Without this normalisation
|
|
803
|
-
// Excel would route us to the cross-type tiebreak below.
|
|
804
|
-
const l = left.kind === 0 /* RVKind.Blank */
|
|
805
|
-
? right.kind === 2 /* RVKind.String */
|
|
806
|
-
? (0, values_1.rvString)("")
|
|
807
|
-
: right.kind === 3 /* RVKind.Boolean */
|
|
808
|
-
? (0, values_1.rvBoolean)(false)
|
|
809
|
-
: (0, values_1.rvNumber)(0)
|
|
810
|
-
: left;
|
|
811
|
-
const r = right.kind === 0 /* RVKind.Blank */
|
|
812
|
-
? left.kind === 2 /* RVKind.String */
|
|
813
|
-
? (0, values_1.rvString)("")
|
|
814
|
-
: left.kind === 3 /* RVKind.Boolean */
|
|
815
|
-
? (0, values_1.rvBoolean)(false)
|
|
816
|
-
: (0, values_1.rvNumber)(0)
|
|
817
|
-
: right;
|
|
818
842
|
let cmp;
|
|
819
|
-
|
|
820
|
-
|
|
843
|
+
// Fast path — same kind, no blanks. Covers the vast majority of
|
|
844
|
+
// comparisons and avoids the blank-normalisation allocation dance
|
|
845
|
+
// below. compareScalarsSameKind handles Number/String/Boolean/Blank
|
|
846
|
+
// intrinsically; the NaN case only arises for Error kinds.
|
|
847
|
+
if (left.kind === right.kind && left.kind !== 0 /* RVKind.Blank */) {
|
|
848
|
+
cmp = (0, values_1.compareScalarsSameKind)(left, right);
|
|
821
849
|
if (!Number.isFinite(cmp)) {
|
|
822
850
|
cmp = 0;
|
|
823
851
|
}
|
|
824
852
|
}
|
|
825
853
|
else {
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
854
|
+
// Normalize blanks to a neutral form of the opposing kind so formulas like
|
|
855
|
+
// `"" = A1` (where A1 is blank) compare equal. Without this normalisation
|
|
856
|
+
// Excel would route us to the cross-type tiebreak below.
|
|
857
|
+
const l = left.kind === 0 /* RVKind.Blank */
|
|
858
|
+
? right.kind === 2 /* RVKind.String */
|
|
859
|
+
? (0, values_1.rvString)("")
|
|
860
|
+
: right.kind === 3 /* RVKind.Boolean */
|
|
861
|
+
? (0, values_1.rvBoolean)(false)
|
|
862
|
+
: (0, values_1.rvNumber)(0)
|
|
863
|
+
: left;
|
|
864
|
+
const r = right.kind === 0 /* RVKind.Blank */
|
|
865
|
+
? left.kind === 2 /* RVKind.String */
|
|
866
|
+
? (0, values_1.rvString)("")
|
|
867
|
+
: left.kind === 3 /* RVKind.Boolean */
|
|
868
|
+
? (0, values_1.rvBoolean)(false)
|
|
869
|
+
: (0, values_1.rvNumber)(0)
|
|
870
|
+
: right;
|
|
871
|
+
if (l.kind === r.kind) {
|
|
872
|
+
cmp = (0, values_1.compareScalarsSameKind)(l, r);
|
|
873
|
+
if (!Number.isFinite(cmp)) {
|
|
874
|
+
cmp = 0;
|
|
833
875
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
cmp = order(l) - order(r);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
// Excel orders scalar kinds: Number < String < Boolean < Error/Blank.
|
|
879
|
+
cmp = scalarKindOrder(l) - scalarKindOrder(r);
|
|
880
|
+
}
|
|
840
881
|
}
|
|
841
882
|
switch (op) {
|
|
842
883
|
case "=":
|
|
@@ -855,6 +896,19 @@ function compareScalars(left, right, op) {
|
|
|
855
896
|
return false;
|
|
856
897
|
}
|
|
857
898
|
}
|
|
899
|
+
/** Kind-priority used by Excel when comparing scalars of different kinds. */
|
|
900
|
+
function scalarKindOrder(v) {
|
|
901
|
+
switch (v.kind) {
|
|
902
|
+
case 1 /* RVKind.Number */:
|
|
903
|
+
return 0;
|
|
904
|
+
case 2 /* RVKind.String */:
|
|
905
|
+
return 1;
|
|
906
|
+
case 3 /* RVKind.Boolean */:
|
|
907
|
+
return 2;
|
|
908
|
+
default:
|
|
909
|
+
return 3; // Error / Blank — callers rarely reach this branch.
|
|
910
|
+
}
|
|
911
|
+
}
|
|
858
912
|
function broadcastBinaryOp(op, left, right) {
|
|
859
913
|
const lArr = left.kind === 5 /* RVKind.Array */ ? left : null;
|
|
860
914
|
const rArr = right.kind === 5 /* RVKind.Array */ ? right : null;
|
|
@@ -879,28 +933,37 @@ function broadcastBinaryOp(op, left, right) {
|
|
|
879
933
|
// inner loop (outRows × outCols calls) was pure overhead.
|
|
880
934
|
const lScalarFallback = lArr ? undefined : (0, values_1.topLeft)(left);
|
|
881
935
|
const rScalarFallback = rArr ? undefined : (0, values_1.topLeft)(right);
|
|
882
|
-
|
|
936
|
+
// Hoist broadcast-flag checks outside the hot loop. The four flags are
|
|
937
|
+
// constant for the entire operation; JS engines usually fold these, but
|
|
938
|
+
// turning them into `const` locals lets the inner loop read bools rather
|
|
939
|
+
// than recomputing `=== 1` each cell.
|
|
940
|
+
const lRowBroadcast = lRows === 1;
|
|
941
|
+
const lColBroadcast = lCols === 1;
|
|
942
|
+
const rRowBroadcast = rRows === 1;
|
|
943
|
+
const rColBroadcast = rCols === 1;
|
|
944
|
+
const rows = new Array(outRows);
|
|
883
945
|
for (let r = 0; r < outRows; r++) {
|
|
884
|
-
|
|
946
|
+
// Cache the row handle once per output row — avoids `arr.rows[lR]`
|
|
947
|
+
// double-indexing per cell. When the left operand broadcasts along
|
|
948
|
+
// rows (height 1) we read the same row handle every iteration.
|
|
949
|
+
const lRow = lArr ? lArr.rows[lRowBroadcast ? 0 : r] : undefined;
|
|
950
|
+
const rRow = rArr ? rArr.rows[rRowBroadcast ? 0 : r] : undefined;
|
|
951
|
+
const outRow = new Array(outCols);
|
|
885
952
|
for (let c = 0; c < outCols; c++) {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const rVal = rArr ? rArr.rows[rR][rC] : rScalarFallback;
|
|
896
|
-
row.push(applyScalarBinaryOp(op, lVal, rVal));
|
|
897
|
-
}
|
|
898
|
-
rows.push(row);
|
|
953
|
+
const lVal = lRow
|
|
954
|
+
? lRow[lColBroadcast ? 0 : c]
|
|
955
|
+
: lScalarFallback;
|
|
956
|
+
const rVal = rRow
|
|
957
|
+
? rRow[rColBroadcast ? 0 : c]
|
|
958
|
+
: rScalarFallback;
|
|
959
|
+
outRow[c] = applyScalarBinaryOp(op, lVal, rVal);
|
|
960
|
+
}
|
|
961
|
+
rows[r] = outRow;
|
|
899
962
|
}
|
|
900
963
|
// Propagate origin metadata
|
|
901
964
|
const originRow = lArr?.originRow ?? rArr?.originRow;
|
|
902
965
|
const originCol = lArr?.originCol ?? rArr?.originCol;
|
|
903
|
-
return (0, values_1.
|
|
966
|
+
return (0, values_1.rvArrayRect)(rows, outRows, outCols, originRow, originCol);
|
|
904
967
|
}
|
|
905
968
|
// ============================================================================
|
|
906
969
|
// Unary Operations
|
|
@@ -992,16 +1055,43 @@ function evaluateCall(expr, ctx, session) {
|
|
|
992
1055
|
if (expr.args.length === 1 && isSimpleRefFunction(canonical)) {
|
|
993
1056
|
const raw = evaluate(expr.args[0], ctx, session);
|
|
994
1057
|
if (raw.kind === 6 /* RVKind.Reference */ && raw.areas.length > 0) {
|
|
995
|
-
|
|
1058
|
+
// ROW / COLUMN pick the top-left coordinate of the first area —
|
|
1059
|
+
// matches Excel's reference-position rule for multi-area refs.
|
|
1060
|
+
// ROWS / COLUMNS match the flattened shape that `dereferenceValue`
|
|
1061
|
+
// would produce: for a multi-area union we stack all areas
|
|
1062
|
+
// vertically, so ROWS sums every area's height and COLUMNS takes
|
|
1063
|
+
// the max width. This keeps `ROWS(union)` consistent with
|
|
1064
|
+
// `SUMPRODUCT(–(union=union))` / `SUM(union)` — all of which see
|
|
1065
|
+
// the stacked view — instead of silently dropping the tail areas.
|
|
1066
|
+
const areas = raw.areas;
|
|
996
1067
|
switch (canonical) {
|
|
997
1068
|
case "ROW":
|
|
998
|
-
return (0, values_1.rvNumber)(
|
|
1069
|
+
return (0, values_1.rvNumber)(areas[0].top);
|
|
999
1070
|
case "COLUMN":
|
|
1000
|
-
return (0, values_1.rvNumber)(
|
|
1001
|
-
case "ROWS":
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1071
|
+
return (0, values_1.rvNumber)(areas[0].left);
|
|
1072
|
+
case "ROWS": {
|
|
1073
|
+
if (areas.length === 1) {
|
|
1074
|
+
return (0, values_1.rvNumber)(areas[0].bottom - areas[0].top + 1);
|
|
1075
|
+
}
|
|
1076
|
+
let total = 0;
|
|
1077
|
+
for (const a of areas) {
|
|
1078
|
+
total += a.bottom - a.top + 1;
|
|
1079
|
+
}
|
|
1080
|
+
return (0, values_1.rvNumber)(total);
|
|
1081
|
+
}
|
|
1082
|
+
case "COLUMNS": {
|
|
1083
|
+
if (areas.length === 1) {
|
|
1084
|
+
return (0, values_1.rvNumber)(areas[0].right - areas[0].left + 1);
|
|
1085
|
+
}
|
|
1086
|
+
let maxW = 0;
|
|
1087
|
+
for (const a of areas) {
|
|
1088
|
+
const w = a.right - a.left + 1;
|
|
1089
|
+
if (w > maxW) {
|
|
1090
|
+
maxW = w;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return (0, values_1.rvNumber)(maxW);
|
|
1094
|
+
}
|
|
1005
1095
|
}
|
|
1006
1096
|
}
|
|
1007
1097
|
}
|
|
@@ -1014,10 +1104,66 @@ function evaluateCall(expr, ctx, session) {
|
|
|
1014
1104
|
if (canonical === "CELL") {
|
|
1015
1105
|
return evaluateCELL(expr.args, ctx, session);
|
|
1016
1106
|
}
|
|
1107
|
+
// ── INDEX reference-aware path ──
|
|
1108
|
+
// Excel's INDEX takes an optional fourth `area_num` that selects
|
|
1109
|
+
// which member of a multi-area reference to index into, e.g.
|
|
1110
|
+
// `INDEX((A1:B2, D4:E5), 1, 1, 2) = D4`. We need to see the raw
|
|
1111
|
+
// ReferenceValue (not its flattened deref array) to support this.
|
|
1112
|
+
if (canonical === "INDEX") {
|
|
1113
|
+
const result = tryEvaluateINDEX(expr.args, ctx, session);
|
|
1114
|
+
if (result !== undefined) {
|
|
1115
|
+
return result;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// ── AREAS reference-aware path ──
|
|
1119
|
+
// `AREAS((A1:B2, D4:E5))` should return 2, not 1. The standard
|
|
1120
|
+
// eager-dereference path flattens multi-area references into a single
|
|
1121
|
+
// stacked ArrayValue, losing the area count. Intercept here so we
|
|
1122
|
+
// see the raw ReferenceValue.
|
|
1123
|
+
if (canonical === "AREAS" && expr.args.length === 1) {
|
|
1124
|
+
const raw = evaluate(expr.args[0], ctx, session);
|
|
1125
|
+
if ((0, values_1.isError)(raw)) {
|
|
1126
|
+
return raw;
|
|
1127
|
+
}
|
|
1128
|
+
if (raw.kind === 6 /* RVKind.Reference */) {
|
|
1129
|
+
return (0, values_1.rvNumber)(raw.areas.length);
|
|
1130
|
+
}
|
|
1131
|
+
// AREAS expects a reference-producing argument. Non-references
|
|
1132
|
+
// (literals, scalars, arrays) are rejected with #VALUE! in Excel,
|
|
1133
|
+
// not silently coerced to `1`. Previously we returned 1 which hid
|
|
1134
|
+
// caller bugs like `AREAS(42)` that should surface as errors.
|
|
1135
|
+
return values_1.ERRORS.VALUE;
|
|
1136
|
+
}
|
|
1017
1137
|
// Evaluate all arguments eagerly and dereference references
|
|
1018
1138
|
const args = expr.args.map(arg => dereferenceValue(evaluate(arg, ctx, session), ctx, session));
|
|
1019
|
-
// Look up function
|
|
1020
|
-
|
|
1139
|
+
// Look up function via the canonical (prefix-stripped) name computed
|
|
1140
|
+
// above. Previously we passed `expr.name`, forcing `lookupFunction`
|
|
1141
|
+
// to re-strip the prefix on every call; reusing `canonical` skips
|
|
1142
|
+
// that redundant check.
|
|
1143
|
+
//
|
|
1144
|
+
// User-registered functions take precedence over the built-in
|
|
1145
|
+
// registry — this lets callers shadow a built-in (e.g. replace
|
|
1146
|
+
// `IRR` with a domain-specific variant) or register entirely new
|
|
1147
|
+
// names (`MYFN`). The resulting descriptor still goes through the
|
|
1148
|
+
// same arity check.
|
|
1149
|
+
const userDesc = ctx.userFunctions?.get(canonical);
|
|
1150
|
+
if (userDesc) {
|
|
1151
|
+
// Validate arity first — same rule as built-ins.
|
|
1152
|
+
if (args.length < userDesc.minArity || args.length > userDesc.maxArity) {
|
|
1153
|
+
return values_1.ERRORS.VALUE;
|
|
1154
|
+
}
|
|
1155
|
+
// User-supplied code can throw; catch at the boundary so a buggy
|
|
1156
|
+
// custom function surfaces as `#VALUE!` rather than tearing down
|
|
1157
|
+
// the whole calculation pass. Any RuntimeValue return (including
|
|
1158
|
+
// error values the user constructed intentionally) passes through.
|
|
1159
|
+
try {
|
|
1160
|
+
return userDesc.invoke(args);
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
return values_1.ERRORS.VALUE;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const desc = (0, function_registry_1.lookupFunction)(canonical);
|
|
1021
1167
|
if (desc) {
|
|
1022
1168
|
// Validate arity — produce #VALUE! for wrong argument count
|
|
1023
1169
|
if (args.length < desc.minArity || args.length > desc.maxArity) {
|
|
@@ -1040,60 +1186,10 @@ function evaluateCall(expr, ctx, session) {
|
|
|
1040
1186
|
}
|
|
1041
1187
|
return desc.invoke(args);
|
|
1042
1188
|
}
|
|
1043
|
-
case "ISFORMULA":
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
// Excel's behavior for non-reference arguments.
|
|
1048
|
-
const raw = expr.args[0];
|
|
1049
|
-
if (raw && raw.kind === 2 /* BoundExprKind.CellRef */) {
|
|
1050
|
-
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1051
|
-
if (!ws) {
|
|
1052
|
-
return values_1.ERRORS.REF;
|
|
1053
|
-
}
|
|
1054
|
-
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(raw.row, raw.col));
|
|
1055
|
-
return (0, values_1.rvBoolean)(cell !== undefined && cell.formulaKind !== "none");
|
|
1056
|
-
}
|
|
1057
|
-
if (raw && raw.kind === 3 /* BoundExprKind.AreaRef */) {
|
|
1058
|
-
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1059
|
-
if (!ws) {
|
|
1060
|
-
return values_1.ERRORS.REF;
|
|
1061
|
-
}
|
|
1062
|
-
// ISFORMULA on an area ref inspects the top-left cell.
|
|
1063
|
-
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(raw.top, raw.left));
|
|
1064
|
-
return (0, values_1.rvBoolean)(cell !== undefined && cell.formulaKind !== "none");
|
|
1065
|
-
}
|
|
1066
|
-
return values_1.ERRORS.NA;
|
|
1067
|
-
}
|
|
1068
|
-
case "FORMULATEXT": {
|
|
1069
|
-
// FORMULATEXT returns the formula source text at the referenced cell,
|
|
1070
|
-
// or #N/A if the cell has no formula. Non-reference arguments also
|
|
1071
|
-
// yield #N/A.
|
|
1072
|
-
const raw = expr.args[0];
|
|
1073
|
-
if (raw && raw.kind === 2 /* BoundExprKind.CellRef */) {
|
|
1074
|
-
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1075
|
-
if (!ws) {
|
|
1076
|
-
return values_1.ERRORS.REF;
|
|
1077
|
-
}
|
|
1078
|
-
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(raw.row, raw.col));
|
|
1079
|
-
if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
|
|
1080
|
-
return (0, values_1.rvString)(`=${cell.formula}`);
|
|
1081
|
-
}
|
|
1082
|
-
return values_1.ERRORS.NA;
|
|
1083
|
-
}
|
|
1084
|
-
if (raw && raw.kind === 3 /* BoundExprKind.AreaRef */) {
|
|
1085
|
-
const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
|
|
1086
|
-
if (!ws) {
|
|
1087
|
-
return values_1.ERRORS.REF;
|
|
1088
|
-
}
|
|
1089
|
-
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(raw.top, raw.left));
|
|
1090
|
-
if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
|
|
1091
|
-
return (0, values_1.rvString)(`=${cell.formula}`);
|
|
1092
|
-
}
|
|
1093
|
-
return values_1.ERRORS.NA;
|
|
1094
|
-
}
|
|
1095
|
-
return values_1.ERRORS.NA;
|
|
1096
|
-
}
|
|
1189
|
+
case "ISFORMULA":
|
|
1190
|
+
return evaluateISFORMULA(expr.args, ctx, session);
|
|
1191
|
+
case "FORMULATEXT":
|
|
1192
|
+
return evaluateFORMULATEXT(expr.args, ctx, session);
|
|
1097
1193
|
default:
|
|
1098
1194
|
return desc.invoke(args);
|
|
1099
1195
|
}
|
|
@@ -1449,7 +1545,23 @@ function evaluateOFFSET(args, ctx, session) {
|
|
|
1449
1545
|
baseWidth = refExpr.right - refExpr.left + 1;
|
|
1450
1546
|
}
|
|
1451
1547
|
else {
|
|
1452
|
-
|
|
1548
|
+
// Evaluate — the base might be produced at runtime (INDIRECT, a
|
|
1549
|
+
// chained OFFSET, a defined name that bound to a reference, etc.).
|
|
1550
|
+
// Only accept single-area references; multi-area refs (3D / union)
|
|
1551
|
+
// are rejected by Excel too.
|
|
1552
|
+
const evaluated = evaluate(refExpr, ctx, session);
|
|
1553
|
+
if ((0, values_1.isError)(evaluated)) {
|
|
1554
|
+
return evaluated;
|
|
1555
|
+
}
|
|
1556
|
+
if (evaluated.kind !== 6 /* RVKind.Reference */ || evaluated.areas.length !== 1) {
|
|
1557
|
+
return values_1.ERRORS.VALUE;
|
|
1558
|
+
}
|
|
1559
|
+
const area = evaluated.areas[0];
|
|
1560
|
+
baseRow = area.top;
|
|
1561
|
+
baseCol = area.left;
|
|
1562
|
+
baseSheet = area.sheet;
|
|
1563
|
+
baseHeight = area.bottom - area.top + 1;
|
|
1564
|
+
baseWidth = area.right - area.left + 1;
|
|
1453
1565
|
}
|
|
1454
1566
|
const rowsVal = (0, values_1.topLeft)(evalDeref(args[1], ctx, session));
|
|
1455
1567
|
const rowsNum = (0, values_1.toNumberRV)(rowsVal);
|
|
@@ -1570,19 +1682,50 @@ function evaluateMAP(args, ctx, session) {
|
|
|
1570
1682
|
if (args.length < 2) {
|
|
1571
1683
|
return values_1.ERRORS.VALUE;
|
|
1572
1684
|
}
|
|
1573
|
-
|
|
1685
|
+
// Excel's MAP accepts N source arrays + 1 lambda; the lambda must take
|
|
1686
|
+
// exactly N parameters. Previously this implementation only read the
|
|
1687
|
+
// first array and ignored args[1..N-1], so `MAP(A1:A3, B1:B3,
|
|
1688
|
+
// LAMBDA(a,b, a+b))` silently behaved as `MAP(A1:A3, LAMBDA(a, a))`.
|
|
1574
1689
|
const lambdaVal = evalDeref(args[args.length - 1], ctx, session);
|
|
1575
1690
|
if (!(0, values_1.isLambda)(lambdaVal)) {
|
|
1576
1691
|
return values_1.ERRORS.VALUE;
|
|
1577
1692
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1693
|
+
const arrayArgs = [];
|
|
1694
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
1695
|
+
arrayArgs.push(evalDeref(args[i], ctx, session));
|
|
1696
|
+
}
|
|
1697
|
+
if (lambdaVal.params.length !== arrayArgs.length) {
|
|
1698
|
+
return values_1.ERRORS.VALUE;
|
|
1699
|
+
}
|
|
1700
|
+
// Determine shape — all arrays must agree. Scalar args broadcast to
|
|
1701
|
+
// the majority shape. If every arg is a scalar, invoke the lambda
|
|
1702
|
+
// once with them all.
|
|
1703
|
+
let height = 1;
|
|
1704
|
+
let width = 1;
|
|
1705
|
+
for (const a of arrayArgs) {
|
|
1706
|
+
if (a.kind === 5 /* RVKind.Array */) {
|
|
1707
|
+
height = Math.max(height, a.height);
|
|
1708
|
+
width = Math.max(width, a.width);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const allScalar = arrayArgs.every(a => a.kind !== 5 /* RVKind.Array */);
|
|
1712
|
+
if (allScalar) {
|
|
1713
|
+
return invokeLambda(lambdaVal, arrayArgs, ctx, session);
|
|
1714
|
+
}
|
|
1715
|
+
// Shape-mismatch rejection for arrays that neither broadcast nor
|
|
1716
|
+
// agree: each non-scalar array must match the target shape exactly
|
|
1717
|
+
// (Excel does not broadcast beyond scalar).
|
|
1718
|
+
for (const a of arrayArgs) {
|
|
1719
|
+
if (a.kind === 5 /* RVKind.Array */ && (a.height !== height || a.width !== width)) {
|
|
1720
|
+
return values_1.ERRORS.VALUE;
|
|
1721
|
+
}
|
|
1580
1722
|
}
|
|
1581
1723
|
const rows = [];
|
|
1582
|
-
for (
|
|
1724
|
+
for (let r = 0; r < height; r++) {
|
|
1583
1725
|
const outRow = [];
|
|
1584
|
-
for (
|
|
1585
|
-
|
|
1726
|
+
for (let c = 0; c < width; c++) {
|
|
1727
|
+
const callArgs = arrayArgs.map(a => a.kind === 5 /* RVKind.Array */ ? a.rows[r][c] : a);
|
|
1728
|
+
outRow.push((0, values_1.topLeft)(invokeLambda(lambdaVal, callArgs, ctx, session)));
|
|
1586
1729
|
}
|
|
1587
1730
|
rows.push(outRow);
|
|
1588
1731
|
}
|
|
@@ -1598,10 +1741,20 @@ function evaluateREDUCE(args, ctx, session) {
|
|
|
1598
1741
|
if (!(0, values_1.isLambda)(lambdaVal)) {
|
|
1599
1742
|
return values_1.ERRORS.VALUE;
|
|
1600
1743
|
}
|
|
1744
|
+
// REDUCE's reducer takes exactly 2 parameters: the accumulator and
|
|
1745
|
+
// the current value. A mismatched arity silently bound `acc` but
|
|
1746
|
+
// left `value` undefined (or silently dropped extra params).
|
|
1747
|
+
if (lambdaVal.params.length !== 2) {
|
|
1748
|
+
return values_1.ERRORS.VALUE;
|
|
1749
|
+
}
|
|
1601
1750
|
if (arrVal.kind === 5 /* RVKind.Array */) {
|
|
1602
1751
|
for (const row of arrVal.rows) {
|
|
1603
1752
|
for (const cell of row) {
|
|
1604
|
-
acc
|
|
1753
|
+
// `acc` may legitimately be an array (the lambda returning one
|
|
1754
|
+
// is allowed — VSTACK inside the reducer, etc.). Only flatten
|
|
1755
|
+
// scalars when passing to the lambda; keeping the array shape
|
|
1756
|
+
// means subsequent iterations see the lambda's full output.
|
|
1757
|
+
acc = invokeLambda(lambdaVal, [acc, cell], ctx, session);
|
|
1605
1758
|
}
|
|
1606
1759
|
}
|
|
1607
1760
|
}
|
|
@@ -1609,7 +1762,7 @@ function evaluateREDUCE(args, ctx, session) {
|
|
|
1609
1762
|
// Scalar input: Excel treats the scalar as a 1×1 array and invokes the
|
|
1610
1763
|
// reducer exactly once. Previously we returned `init` unchanged, which
|
|
1611
1764
|
// silently broke `REDUCE(0, some_scalar, lambda)` callers.
|
|
1612
|
-
acc = invokeLambda(lambdaVal, [
|
|
1765
|
+
acc = invokeLambda(lambdaVal, [acc, (0, values_1.topLeft)(arrVal)], ctx, session);
|
|
1613
1766
|
}
|
|
1614
1767
|
return acc;
|
|
1615
1768
|
}
|
|
@@ -1623,12 +1776,20 @@ function evaluateSCAN(args, ctx, session) {
|
|
|
1623
1776
|
if (!(0, values_1.isLambda)(lambdaVal)) {
|
|
1624
1777
|
return values_1.ERRORS.VALUE;
|
|
1625
1778
|
}
|
|
1779
|
+
// SCAN's reducer takes (acc, value) — same arity rule as REDUCE.
|
|
1780
|
+
if (lambdaVal.params.length !== 2) {
|
|
1781
|
+
return values_1.ERRORS.VALUE;
|
|
1782
|
+
}
|
|
1626
1783
|
const rows = [];
|
|
1627
1784
|
if (arrVal.kind === 5 /* RVKind.Array */) {
|
|
1628
1785
|
for (const row of arrVal.rows) {
|
|
1629
1786
|
const outRow = [];
|
|
1630
1787
|
for (const cell of row) {
|
|
1631
|
-
acc
|
|
1788
|
+
// Pass `acc` through without topLeft so the lambda sees the full
|
|
1789
|
+
// previous result (matches the REDUCE fix). Each output cell
|
|
1790
|
+
// still captures the scalar top-left of `acc` so the result
|
|
1791
|
+
// grid stays rectangular.
|
|
1792
|
+
acc = invokeLambda(lambdaVal, [acc, cell], ctx, session);
|
|
1632
1793
|
outRow.push((0, values_1.topLeft)(acc));
|
|
1633
1794
|
}
|
|
1634
1795
|
rows.push(outRow);
|
|
@@ -1638,7 +1799,7 @@ function evaluateSCAN(args, ctx, session) {
|
|
|
1638
1799
|
// Scalar input: emit a single-cell array containing the one accumulated
|
|
1639
1800
|
// value. Previously we returned #CALC! here, which was an artefact of the
|
|
1640
1801
|
// array-only implementation path.
|
|
1641
|
-
const result = invokeLambda(lambdaVal, [
|
|
1802
|
+
const result = invokeLambda(lambdaVal, [acc, (0, values_1.topLeft)(arrVal)], ctx, session);
|
|
1642
1803
|
return (0, values_1.rvArray)([[(0, values_1.topLeft)(result)]]);
|
|
1643
1804
|
}
|
|
1644
1805
|
function evaluateMAKEARRAY(args, ctx, session) {
|
|
@@ -1657,6 +1818,14 @@ function evaluateMAKEARRAY(args, ctx, session) {
|
|
|
1657
1818
|
if (!(0, values_1.isLambda)(lambdaVal)) {
|
|
1658
1819
|
return values_1.ERRORS.VALUE;
|
|
1659
1820
|
}
|
|
1821
|
+
// Excel's MAKEARRAY requires a 2-parameter lambda (row, col).
|
|
1822
|
+
// A 0 / 1 / 3+ param lambda is rejected at call time — previously we
|
|
1823
|
+
// invoked with 2 args regardless, which silently extended the lambda's
|
|
1824
|
+
// bindings past its declared parameter list (or left declared params
|
|
1825
|
+
// undefined).
|
|
1826
|
+
if (lambdaVal.params.length !== 2) {
|
|
1827
|
+
return values_1.ERRORS.VALUE;
|
|
1828
|
+
}
|
|
1660
1829
|
// Truncate toward zero and reject non-positive / overflow sizes.
|
|
1661
1830
|
// Without the cell-count cap the engine can silently allocate billions
|
|
1662
1831
|
// of scalars before blowing the heap; matching the broadcast limit
|
|
@@ -1688,9 +1857,21 @@ function evaluateBYROW(args, ctx, session) {
|
|
|
1688
1857
|
if (!(0, values_1.isLambda)(lambdaVal)) {
|
|
1689
1858
|
return values_1.ERRORS.VALUE;
|
|
1690
1859
|
}
|
|
1860
|
+
// BYROW requires a single-parameter lambda (the row). Mismatched arity
|
|
1861
|
+
// is rejected by Excel at call time — our previous impl silently left
|
|
1862
|
+
// extra params undefined.
|
|
1863
|
+
if (lambdaVal.params.length !== 1) {
|
|
1864
|
+
return values_1.ERRORS.VALUE;
|
|
1865
|
+
}
|
|
1691
1866
|
if (arrVal.kind !== 5 /* RVKind.Array */) {
|
|
1692
1867
|
return invokeLambda(lambdaVal, [(0, values_1.rvArray)([[(0, values_1.topLeft)(arrVal)]])], ctx, session);
|
|
1693
1868
|
}
|
|
1869
|
+
// Empty array (height 0) → Excel's BYROW reports `#CALC!` because
|
|
1870
|
+
// there is nothing to iterate over. Previously we returned an empty
|
|
1871
|
+
// array value which downstream arithmetic could not use.
|
|
1872
|
+
if (arrVal.height === 0 || arrVal.width === 0) {
|
|
1873
|
+
return values_1.ERRORS.CALC;
|
|
1874
|
+
}
|
|
1694
1875
|
const rows = [];
|
|
1695
1876
|
for (const row of arrVal.rows) {
|
|
1696
1877
|
const rowArr = (0, values_1.rvArray)([row.map(c => c)]);
|
|
@@ -1707,9 +1888,17 @@ function evaluateBYCOL(args, ctx, session) {
|
|
|
1707
1888
|
if (!(0, values_1.isLambda)(lambdaVal)) {
|
|
1708
1889
|
return values_1.ERRORS.VALUE;
|
|
1709
1890
|
}
|
|
1891
|
+
// BYCOL requires a single-parameter lambda (the column). See BYROW.
|
|
1892
|
+
if (lambdaVal.params.length !== 1) {
|
|
1893
|
+
return values_1.ERRORS.VALUE;
|
|
1894
|
+
}
|
|
1710
1895
|
if (arrVal.kind !== 5 /* RVKind.Array */) {
|
|
1711
1896
|
return invokeLambda(lambdaVal, [(0, values_1.rvArray)([[(0, values_1.topLeft)(arrVal)]])], ctx, session);
|
|
1712
1897
|
}
|
|
1898
|
+
// Empty array → `#CALC!` (see BYROW).
|
|
1899
|
+
if (arrVal.height === 0 || arrVal.width === 0) {
|
|
1900
|
+
return values_1.ERRORS.CALC;
|
|
1901
|
+
}
|
|
1713
1902
|
const numCols = arrVal.width;
|
|
1714
1903
|
const outRow = [];
|
|
1715
1904
|
for (let c = 0; c < numCols; c++) {
|
|
@@ -1772,6 +1961,179 @@ function tryEvaluateRefFunction(name, args, ctx) {
|
|
|
1772
1961
|
// ============================================================================
|
|
1773
1962
|
// Reference-aware: ISREF
|
|
1774
1963
|
// ============================================================================
|
|
1964
|
+
/**
|
|
1965
|
+
* Resolve the top-left cell targeted by an ISFORMULA / FORMULATEXT argument.
|
|
1966
|
+
*
|
|
1967
|
+
* Unlike `resolveCellRefArg` (used by CELL), these two functions must return
|
|
1968
|
+
* `#N/A` for non-reference arguments (arithmetic, literals, ...) rather than
|
|
1969
|
+
* `#VALUE!`. We support:
|
|
1970
|
+
* - purely syntactic reference nodes (CellRef / AreaRef / ColRangeRef /
|
|
1971
|
+
* RowRangeRef / Ref3D) — cheapest path, no evaluation needed
|
|
1972
|
+
* - runtime-produced references (INDIRECT, OFFSET) — we evaluate without
|
|
1973
|
+
* dereferencing and inspect the resulting ReferenceValue
|
|
1974
|
+
*
|
|
1975
|
+
* Anything else — including errors from INDIRECT("xx") — collapses to
|
|
1976
|
+
* `#N/A`, matching Excel's tolerant behaviour for these two functions.
|
|
1977
|
+
*/
|
|
1978
|
+
function resolveFormulaRefArg(arg, ctx, session) {
|
|
1979
|
+
if (!arg) {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
// Syntactic reference forms — extract top-left directly, no evaluation.
|
|
1983
|
+
if (arg.kind === 2 /* BoundExprKind.CellRef */) {
|
|
1984
|
+
return { sheet: arg.sheet, row: arg.row, col: arg.col };
|
|
1985
|
+
}
|
|
1986
|
+
if (arg.kind === 3 /* BoundExprKind.AreaRef */) {
|
|
1987
|
+
return { sheet: arg.sheet, row: arg.top, col: arg.left };
|
|
1988
|
+
}
|
|
1989
|
+
if (arg.kind === 4 /* BoundExprKind.ColRangeRef */) {
|
|
1990
|
+
const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
|
|
1991
|
+
const top = ws?.dimensions?.top ?? 1;
|
|
1992
|
+
return { sheet: arg.sheet, row: top, col: arg.leftCol };
|
|
1993
|
+
}
|
|
1994
|
+
if (arg.kind === 5 /* BoundExprKind.RowRangeRef */) {
|
|
1995
|
+
const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
|
|
1996
|
+
const left = ws?.dimensions?.left ?? 1;
|
|
1997
|
+
return { sheet: arg.sheet, row: arg.topRow, col: left };
|
|
1998
|
+
}
|
|
1999
|
+
if (arg.kind === 6 /* BoundExprKind.Ref3D */) {
|
|
2000
|
+
const first = arg.sheets[0];
|
|
2001
|
+
if (first === undefined) {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
if (arg.inner.kind === 2 /* BoundExprKind.CellRef */) {
|
|
2005
|
+
return { sheet: first, row: arg.inner.row, col: arg.inner.col };
|
|
2006
|
+
}
|
|
2007
|
+
return { sheet: first, row: arg.inner.top, col: arg.inner.left };
|
|
2008
|
+
}
|
|
2009
|
+
// Evaluate without dereferencing — INDIRECT/OFFSET may yield a
|
|
2010
|
+
// ReferenceValue. Errors or non-references collapse to null (→ #N/A).
|
|
2011
|
+
const raw = evaluate(arg, ctx, session);
|
|
2012
|
+
if (raw.kind === 6 /* RVKind.Reference */ && raw.areas.length > 0) {
|
|
2013
|
+
const area = raw.areas[0];
|
|
2014
|
+
return { sheet: area.sheet, row: area.top, col: area.left };
|
|
2015
|
+
}
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
function evaluateISFORMULA(args, ctx, session) {
|
|
2019
|
+
if (args.length !== 1) {
|
|
2020
|
+
return values_1.ERRORS.NA;
|
|
2021
|
+
}
|
|
2022
|
+
const target = resolveFormulaRefArg(args[0], ctx, session);
|
|
2023
|
+
if (!target) {
|
|
2024
|
+
return values_1.ERRORS.NA;
|
|
2025
|
+
}
|
|
2026
|
+
const ws = ctx.snapshot.worksheetsByName.get(target.sheet.toLowerCase());
|
|
2027
|
+
if (!ws) {
|
|
2028
|
+
return values_1.ERRORS.REF;
|
|
2029
|
+
}
|
|
2030
|
+
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(target.row, target.col));
|
|
2031
|
+
return (0, values_1.rvBoolean)(cell !== undefined && cell.formulaKind !== "none");
|
|
2032
|
+
}
|
|
2033
|
+
function evaluateFORMULATEXT(args, ctx, session) {
|
|
2034
|
+
if (args.length !== 1) {
|
|
2035
|
+
return values_1.ERRORS.NA;
|
|
2036
|
+
}
|
|
2037
|
+
const target = resolveFormulaRefArg(args[0], ctx, session);
|
|
2038
|
+
if (!target) {
|
|
2039
|
+
return values_1.ERRORS.NA;
|
|
2040
|
+
}
|
|
2041
|
+
const ws = ctx.snapshot.worksheetsByName.get(target.sheet.toLowerCase());
|
|
2042
|
+
if (!ws) {
|
|
2043
|
+
return values_1.ERRORS.REF;
|
|
2044
|
+
}
|
|
2045
|
+
const cell = ws.cells.get((0, workbook_snapshot_1.snapshotCellKey)(target.row, target.col));
|
|
2046
|
+
if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
|
|
2047
|
+
return (0, values_1.rvString)(`=${cell.formula}`);
|
|
2048
|
+
}
|
|
2049
|
+
return values_1.ERRORS.NA;
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* INDEX reference-aware path.
|
|
2053
|
+
*
|
|
2054
|
+
* Standard INDEX takes `(array, row, col)` and returns a value or
|
|
2055
|
+
* sub-range. When the first argument is a multi-area UnionRef, INDEX
|
|
2056
|
+
* also accepts a fourth `area_num` argument to select which area to
|
|
2057
|
+
* index into — e.g. `INDEX((A1:B2, D4:E5), 1, 1, 2) = D4`.
|
|
2058
|
+
*
|
|
2059
|
+
* We intercept INDEX here (before the normal eager-dereference path)
|
|
2060
|
+
* because dereferencing a multi-area ReferenceValue flattens its
|
|
2061
|
+
* areas into a single stacked array, losing the per-area boundary
|
|
2062
|
+
* needed for `area_num`. For the common single-area case we return
|
|
2063
|
+
* `undefined` so the eager path runs and delegates to `fnINDEX`.
|
|
2064
|
+
*/
|
|
2065
|
+
function tryEvaluateINDEX(args, ctx, session) {
|
|
2066
|
+
if (args.length < 2 || args.length > 4) {
|
|
2067
|
+
return undefined; // Fall through — eager fnINDEX reports arity errors.
|
|
2068
|
+
}
|
|
2069
|
+
// Fast path: 2- and 3-arg INDEX has no area_num, so we can skip the
|
|
2070
|
+
// union-aware logic entirely for the common single-array case. Only
|
|
2071
|
+
// INDEX with an explicit area_num (4 args) OR a multi-area first
|
|
2072
|
+
// operand needs the reference-aware route.
|
|
2073
|
+
const first = evaluate(args[0], ctx, session);
|
|
2074
|
+
if ((0, values_1.isError)(first)) {
|
|
2075
|
+
// Errors from the source expression propagate regardless of arity.
|
|
2076
|
+
return first;
|
|
2077
|
+
}
|
|
2078
|
+
const isMultiArea = first.kind === 6 /* RVKind.Reference */ && first.areas.length > 1;
|
|
2079
|
+
if (!isMultiArea && args.length < 4) {
|
|
2080
|
+
return undefined;
|
|
2081
|
+
}
|
|
2082
|
+
// Single-area reference with an explicit 4th arg still needs
|
|
2083
|
+
// area_num validation — Excel rejects `INDEX(A1:B2, 1, 1, 2)` as
|
|
2084
|
+
// #REF! since the source only has one area.
|
|
2085
|
+
if (first.kind !== 6 /* RVKind.Reference */ || first.areas.length === 0) {
|
|
2086
|
+
// Non-reference first arg (array literal, number, etc.) — only
|
|
2087
|
+
// valid when args.length < 4. Let the eager path handle it.
|
|
2088
|
+
if (args.length < 4) {
|
|
2089
|
+
return undefined;
|
|
2090
|
+
}
|
|
2091
|
+
// With an explicit area_num on a non-reference, Excel returns #REF!.
|
|
2092
|
+
return values_1.ERRORS.REF;
|
|
2093
|
+
}
|
|
2094
|
+
const areas = first.areas;
|
|
2095
|
+
// Resolve `area_num` (1-based). Omitted / Blank → area 1. Truncate
|
|
2096
|
+
// toward zero and bounds-check against the union's cardinality.
|
|
2097
|
+
let areaNum = 1;
|
|
2098
|
+
if (args.length === 4) {
|
|
2099
|
+
const rawArea = dereferenceValue(evaluate(args[3], ctx, session), ctx, session);
|
|
2100
|
+
if ((0, values_1.isError)(rawArea)) {
|
|
2101
|
+
return rawArea;
|
|
2102
|
+
}
|
|
2103
|
+
const s = (0, values_1.topLeft)(rawArea);
|
|
2104
|
+
// Treat a blank / missing 4th argument as the default (area 1) —
|
|
2105
|
+
// matches Excel's tolerance for `INDEX(ref, r, c, )` and avoids the
|
|
2106
|
+
// surprise where a trailing comma would silently produce #REF!
|
|
2107
|
+
// (blank → toNumberRV = 0 → out-of-range).
|
|
2108
|
+
if (s.kind !== 0 /* RVKind.Blank */) {
|
|
2109
|
+
const aRV = (0, values_1.toNumberRV)(s);
|
|
2110
|
+
if ((0, values_1.isError)(aRV)) {
|
|
2111
|
+
return aRV;
|
|
2112
|
+
}
|
|
2113
|
+
areaNum = Math.trunc(aRV.value);
|
|
2114
|
+
if (areaNum < 1 || areaNum > areas.length) {
|
|
2115
|
+
return values_1.ERRORS.REF;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const area = areas[areaNum - 1];
|
|
2120
|
+
// Build a single-area ArrayValue for the selected region, then
|
|
2121
|
+
// delegate to `fnINDEX` using the row/col args. This keeps the
|
|
2122
|
+
// actual indexing logic in one place.
|
|
2123
|
+
const selectedArr = buildRangeArray(ctx, session, area.sheet, area.top, area.left, area.bottom, area.right);
|
|
2124
|
+
const indexArgs = [selectedArr];
|
|
2125
|
+
for (let i = 1; i < Math.min(args.length, 3); i++) {
|
|
2126
|
+
indexArgs.push(dereferenceValue(evaluate(args[i], ctx, session), ctx, session));
|
|
2127
|
+
}
|
|
2128
|
+
// Delegate to the registered INDEX implementation — keeps the actual
|
|
2129
|
+
// indexing logic (fractional truncation, single-col collapse, row=0
|
|
2130
|
+
// / col=0 semantics) in one place rather than reimplementing here.
|
|
2131
|
+
const indexFn = (0, function_registry_1.lookupFunction)("INDEX");
|
|
2132
|
+
if (!indexFn) {
|
|
2133
|
+
return values_1.ERRORS.VALUE;
|
|
2134
|
+
}
|
|
2135
|
+
return indexFn.invoke(indexArgs);
|
|
2136
|
+
}
|
|
1775
2137
|
/**
|
|
1776
2138
|
* ISREF(value) → TRUE if `value` is a reference; FALSE otherwise.
|
|
1777
2139
|
*
|
|
@@ -1915,7 +2277,23 @@ function evaluateCELL(args, ctx, session) {
|
|
|
1915
2277
|
}
|
|
1916
2278
|
switch (info) {
|
|
1917
2279
|
case "address": {
|
|
1918
|
-
|
|
2280
|
+
// Excel qualifies the address with the sheet name when the target
|
|
2281
|
+
// sheet differs from the formula-cell's own sheet (e.g.
|
|
2282
|
+
// `CELL("address", Sheet2!A1)` → `"Sheet2!$A$1"`). Same-sheet
|
|
2283
|
+
// refs stay unqualified. Without the qualifier, callers that
|
|
2284
|
+
// parse the result (INDIRECT(CELL("address", ref))) lose the
|
|
2285
|
+
// sheet context and misread remote cells.
|
|
2286
|
+
const addr = `$${colNumberToLetter(target.col)}$${target.row}`;
|
|
2287
|
+
const sameSheet = ctx.currentSheet.toLowerCase() === target.sheet.toLowerCase();
|
|
2288
|
+
if (sameSheet) {
|
|
2289
|
+
return (0, values_1.rvString)(addr);
|
|
2290
|
+
}
|
|
2291
|
+
// Quote sheet names that need it (spaces, special chars, starts
|
|
2292
|
+
// with digit). The same rule the tokenizer uses when parsing
|
|
2293
|
+
// quoted sheet refs on the way in.
|
|
2294
|
+
const needsQuote = !/^[A-Za-z_][A-Za-z0-9_]*$/.test(target.sheet);
|
|
2295
|
+
const sheetPrefix = needsQuote ? `'${target.sheet.replace(/'/g, "''")}'` : target.sheet;
|
|
2296
|
+
return (0, values_1.rvString)(`${sheetPrefix}!${addr}`);
|
|
1919
2297
|
}
|
|
1920
2298
|
case "row":
|
|
1921
2299
|
return (0, values_1.rvNumber)(target.row);
|
|
@@ -1964,6 +2342,16 @@ function evaluateNameExpr(expr, ctx, session) {
|
|
|
1964
2342
|
const rangeStr = dn.ranges[0];
|
|
1965
2343
|
const parsed = (0, address_utils_1.parseDefinedNameRange)(rangeStr);
|
|
1966
2344
|
if (parsed) {
|
|
2345
|
+
// Validate against Excel's sheet coordinate limits before we pass
|
|
2346
|
+
// the values to `getCellValue` / `buildRangeArray`. An invalid
|
|
2347
|
+
// defined-name string (e.g. a range exceeding column XFD) should
|
|
2348
|
+
// surface as #REF! rather than silently reading BLANK cells.
|
|
2349
|
+
if (parsed.startRow < 1 ||
|
|
2350
|
+
parsed.endRow > 1048576 ||
|
|
2351
|
+
parsed.startCol < 1 ||
|
|
2352
|
+
parsed.endCol > 16384) {
|
|
2353
|
+
return values_1.ERRORS.REF;
|
|
2354
|
+
}
|
|
1967
2355
|
if (parsed.startRow === parsed.endRow && parsed.startCol === parsed.endCol) {
|
|
1968
2356
|
return getCellValue(parsed.sheet, parsed.startRow, parsed.startCol, ctx, session);
|
|
1969
2357
|
}
|
|
@@ -2024,6 +2412,38 @@ function evaluateLambdaExpr(expr, ctx) {
|
|
|
2024
2412
|
return (0, values_1.rvLambda)([...expr.params], expr.body, ctx.localBindings ? new Map(ctx.localBindings) : undefined);
|
|
2025
2413
|
}
|
|
2026
2414
|
// ============================================================================
|
|
2415
|
+
// Union Reference `(A1:B2, D4:E5)` — yields a multi-area ReferenceValue
|
|
2416
|
+
// ============================================================================
|
|
2417
|
+
/**
|
|
2418
|
+
* Evaluate a reference union syntactically formed by `(area1, area2, …)`.
|
|
2419
|
+
*
|
|
2420
|
+
* Each member must resolve to a reference-producing value. The resulting
|
|
2421
|
+
* `ReferenceValue` carries every area in order so that `INDEX(union, r,
|
|
2422
|
+
* c, area_num)` can pick the right one. Any non-reference member
|
|
2423
|
+
* (scalar / array literal / error) short-circuits to `#VALUE!`.
|
|
2424
|
+
*/
|
|
2425
|
+
function evaluateUnionRef(expr, ctx, session) {
|
|
2426
|
+
const areas = [];
|
|
2427
|
+
for (const member of expr.areas) {
|
|
2428
|
+
const val = evaluate(member, ctx, session);
|
|
2429
|
+
if ((0, values_1.isError)(val)) {
|
|
2430
|
+
return val;
|
|
2431
|
+
}
|
|
2432
|
+
if (val.kind !== 6 /* RVKind.Reference */ || val.areas.length === 0) {
|
|
2433
|
+
// Excel rejects non-reference members of a union outright —
|
|
2434
|
+
// `(A1, "text")` is `#VALUE!`, not a silent coerce to 1-cell.
|
|
2435
|
+
return values_1.ERRORS.VALUE;
|
|
2436
|
+
}
|
|
2437
|
+
for (const a of val.areas) {
|
|
2438
|
+
areas.push(a);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (areas.length === 0) {
|
|
2442
|
+
return values_1.ERRORS.VALUE;
|
|
2443
|
+
}
|
|
2444
|
+
return { kind: 6 /* RVKind.Reference */, areas };
|
|
2445
|
+
}
|
|
2446
|
+
// ============================================================================
|
|
2027
2447
|
// Structured Reference (runtime resolution)
|
|
2028
2448
|
// ============================================================================
|
|
2029
2449
|
function evaluateStructuredRef(expr, ctx, session) {
|