@devmm/puredocs-excel-formula 1.0.6 → 1.0.7

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/index.d.cts CHANGED
@@ -242,13 +242,98 @@ declare namespace FormulaHelper {
242
242
  };
243
243
  /** Flattens all args into FormulaValue array (expanding arrays). */
244
244
  function flattenArgs(args: FormulaNode[], ctx: FormulaContext): FormulaValue[];
245
+ /**
246
+ * A criteria string analysed once, so testing it against N values does not
247
+ * re-parse it N times.
248
+ *
249
+ * `kind` is what lets a caller index instead of scan: `equalsNumber` and
250
+ * `equalsText` are the two shapes whose matching positions can be looked up,
251
+ * and they are the overwhelming majority in real files (`SUMIFS(H:H, W:W, W282)`).
252
+ * Everything else — comparisons, wildcards — only gets the compiled `test`.
253
+ */
254
+ interface CompiledCriteria {
255
+ readonly kind: 'equalsNumber' | 'equalsText' | 'compareNumber' | 'other';
256
+ /** For `equalsNumber` and `compareNumber`: the number compared against. */
257
+ readonly number?: number;
258
+ /** For `equalsText`: the criterion, upper-cased (matching is case-insensitive). */
259
+ readonly text?: string;
260
+ /** For `compareNumber`: which comparison. */
261
+ readonly op?: '>' | '>=' | '<' | '<=';
262
+ readonly test: (value: FormulaValue) => boolean;
263
+ }
264
+ /**
265
+ * Compiles a criteria string into a reusable predicate.
266
+ *
267
+ * Same semantics as {@link matchesCriteria} — that function is now this one
268
+ * applied immediately — but the parsing, the number conversion and any wildcard
269
+ * RegExp happen once instead of once per cell. A SUMIFS filled down 291 rows
270
+ * over a 291-cell range was doing that 84 000 times.
271
+ */
272
+ function compileCriteria(criteria: string): CompiledCriteria;
245
273
  /** Matches a criteria string (e.g. ">5", "=hello", "*partial*") against a value. */
246
274
  function matchesCriteria(value: FormulaValue, criteria: string): boolean;
247
275
  }
248
276
 
277
+ /**
278
+ * Memo for range reads: `(sheet, rectangle) → array value`.
279
+ *
280
+ * Why this exists. A range reference is resolved by materialising an ArrayValue
281
+ * and reading every cell in the rectangle. The dominant pattern in real
282
+ * workbooks is many formulas reading the *same* range — a SUMIFS filled down
283
+ * 291 rows reads its two 291-cell ranges 291 times each — so the same rectangle
284
+ * is rebuilt over and over. Memoising it turns O(formulas × range) into
285
+ * O(distinct ranges × range) for the read side, which is the difference between
286
+ * a recalculation that finishes and one that does not.
287
+ *
288
+ * Why invalidation is per-rectangle rather than per-sheet. During a recalc pass
289
+ * the engine writes a computed value after every formula, so a sheet-wide
290
+ * generation counter would invalidate the whole cache thousands of times per
291
+ * pass and never hit. Instead each cached rectangle is filed into a coarse tile
292
+ * grid (same shape as {@link DependencyIndex}), and a write only drops the
293
+ * rectangles that actually contain the written cell — writes to a column of
294
+ * results leave the input ranges cached.
295
+ *
296
+ * Safety. Cached values are shared, not copied, so a reader must not mutate the
297
+ * ArrayValue it receives. Every built-in function builds its own result array;
298
+ * none writes into an argument.
299
+ */
300
+
301
+ declare class RangeCache {
302
+ #private;
303
+ hits: number;
304
+ misses: number;
305
+ constructor(maxCells?: number);
306
+ get size(): number;
307
+ get cachedCells(): number;
308
+ static key(sheet: string, r0: number, c0: number, r1: number, c1: number): string;
309
+ get(sheet: string, r0: number, c0: number, r1: number, c1: number): FormulaValue | undefined;
310
+ /** Memoises a rectangle's value. `cells` is its area, for the budget. */
311
+ set(sheet: string, r0: number, c0: number, r1: number, c1: number, value: FormulaValue, cells: number): void;
312
+ /** Drops every cached rectangle containing the given cell. */
313
+ invalidateCell(sheet: string, row: number, col: number): void;
314
+ clear(): void;
315
+ }
316
+
249
317
  interface WorksheetLike {
250
318
  getCellValue(reference: string): unknown;
251
319
  name: string;
320
+ /**
321
+ * Optional: value of a cell by 1-based row/column. Range reads use this when
322
+ * present, which saves building — and re-parsing — one reference string per
323
+ * cell. Implementations that resolve values lazily inside getCellValue should
324
+ * leave it undefined to keep that path.
325
+ */
326
+ getCellValueAt?(row: number, column: number): unknown;
327
+ /**
328
+ * Optional: the populated extent of the sheet — the highest 1-based row and
329
+ * column that hold a cell (0 when empty). Used to bound whole-column and
330
+ * whole-row references (`A:A`, `1:1`), which Excel evaluates over the used
331
+ * range rather than all 1,048,576 rows.
332
+ */
333
+ readonly usedBounds?: {
334
+ rowCount: number;
335
+ columnCount: number;
336
+ };
252
337
  /** Optional: whether a 1-based row is hidden (used by SUBTOTAL/AGGREGATE). */
253
338
  isRowHidden?(row: number): boolean;
254
339
  /** Optional: formula text of a cell (used by SUBTOTAL to skip nested calls). */
@@ -277,6 +362,13 @@ declare class FormulaContext {
277
362
  formulaCol: number;
278
363
  /** Optional workbook reference for cross-sheet references. */
279
364
  workbook?: WorkbookLike;
365
+ /**
366
+ * Optional memo for range reads, supplied by whoever owns the write side —
367
+ * the recalculation engine, which invalidates it as it writes results. Left
368
+ * undefined for one-shot evaluation, where a stale entry could otherwise
369
+ * outlive a caller's direct edit to the model.
370
+ */
371
+ rangeCache?: RangeCache;
280
372
  /**
281
373
  * Optional named-range resolver, installed by the evaluator when the workbook
282
374
  * exposes getDefinedName. Given a name, returns its evaluated value. Left
@@ -301,8 +393,26 @@ declare class FormulaContext {
301
393
  get worksheet(): WorksheetLike;
302
394
  getCellValue(cellReference: string): FormulaValue;
303
395
  getRangeValues(startRef: string, endRef: string): FormulaValue;
396
+ /**
397
+ * Resolves a rectangle already known in coordinates. Split out from
398
+ * {@link getRangeValues} because whole-column references and the range memo
399
+ * both work in numbers, and formatting them back into `A1` strings only to
400
+ * re-parse them is measurable at range scale.
401
+ */
402
+ getRangeValuesByIndex(startRow: number, startCol: number, endRow: number, endCol: number): FormulaValue;
403
+ /**
404
+ * Resolves a whole-column (`A:A`) or whole-row (`1:1`) reference, bounded by
405
+ * the sheet's used range the way Excel does. Without a bound this would
406
+ * materialise a million-cell array for a sheet with twelve rows in it.
407
+ *
408
+ * A sheet that cannot report its extent yields `#REF!` rather than a guess:
409
+ * silently reading nothing would make SUM(A:A) return 0 on a full column.
410
+ */
411
+ getFullRangeValues(startCol: number | null, endCol: number | null, startRow: number | null, endRow: number | null): FormulaValue;
304
412
  getSheetCellValue(sheetName: string, cellReference: string): FormulaValue;
305
413
  getSheetRangeValues(sheetName: string, startRef: string, endRef: string): FormulaValue;
414
+ /** Whole-column/whole-row reference qualified by a sheet name. */
415
+ getSheetFullRangeValues(sheetName: string, startCol: number | null, endCol: number | null, startRow: number | null, endRow: number | null): FormulaValue;
306
416
  resolveNamedRange(name: string): FormulaValue;
307
417
  /**
308
418
  * Resolves a spill-range reference (A1#) to the array spilling from the anchor
@@ -383,6 +493,25 @@ declare class SheetRangeReferenceNode extends FormulaNode {
383
493
  constructor(sheetName: string, startRef: string, endRef: string);
384
494
  evaluate(ctx: FormulaContext): FormulaValue;
385
495
  }
496
+ /**
497
+ * A whole-column (`A:A`, `$B:$D`) or whole-row (`1:1`, `3:7`) reference,
498
+ * optionally sheet-qualified (`Sheet1!A:A`).
499
+ *
500
+ * Exactly one dimension is bounded: a column range fixes the columns and leaves
501
+ * the rows open, a row range the reverse. The open dimension is resolved against
502
+ * the sheet's used range at evaluation time rather than baked in as 1..1048576 —
503
+ * expanding it eagerly would turn `SUM(A:A)` on a twelve-row sheet into a
504
+ * million-cell read, and Excel's own answer is the used range too.
505
+ */
506
+ declare class FullRangeReferenceNode extends FormulaNode {
507
+ readonly startCol: number | null;
508
+ readonly endCol: number | null;
509
+ readonly startRow: number | null;
510
+ readonly endRow: number | null;
511
+ readonly sheetName?: string | undefined;
512
+ constructor(startCol: number | null, endCol: number | null, startRow: number | null, endRow: number | null, sheetName?: string | undefined);
513
+ evaluate(ctx: FormulaContext): FormulaValue;
514
+ }
386
515
  declare class NamedRangeNode extends FormulaNode {
387
516
  readonly name: string;
388
517
  constructor(name: string);
@@ -452,21 +581,6 @@ declare class CallNode extends FormulaNode {
452
581
  evaluateTail(ctx: FormulaContext): FormulaValue | TailCall;
453
582
  }
454
583
 
455
- /**
456
- * Recursive descent parser converting formula tokens into an AST.
457
- * Port of TVE.PureDocs.Excel.Formulas.FormulaParser (C#)
458
- *
459
- * Operator precedence (low → high):
460
- * Comparison: =, <>, <, <=, >, >=
461
- * Concatenation: &
462
- * Addition/Subtraction: +, -
463
- * Multiplication/Division: *, /
464
- * Unary: +, -, @ (prefix)
465
- * Power: ^ (right-associative)
466
- * Percent: % (postfix)
467
- * Primary: literals, refs, functions, parentheses
468
- */
469
-
470
584
  declare class FormulaParser {
471
585
  #private;
472
586
  constructor(tokens: Token[]);
@@ -505,6 +619,11 @@ declare function evaluateAst(ast: FormulaNode, sheet: WorksheetLike, options?: {
505
619
  workbook?: WorkbookLike;
506
620
  formulaRow?: number;
507
621
  formulaCol?: number;
622
+ /**
623
+ * Range memo shared across evaluations. Only pass one if you invalidate it
624
+ * on every write to the model — {@link RecalcEngine} does.
625
+ */
626
+ rangeCache?: RangeCache;
508
627
  }): FormulaValue;
509
628
  declare function clearAstCache(): void;
510
629
  declare function getAstCacheStats(): {
@@ -591,6 +710,11 @@ interface CoreCellLike {
591
710
  }
592
711
  interface CoreSheetLike {
593
712
  getCellValue(ref: string): unknown;
713
+ getCellValueAt?(row: number, column: number): unknown;
714
+ readonly usedBounds?: {
715
+ rowCount: number;
716
+ columnCount: number;
717
+ };
594
718
  getCell(ref: string): CoreCellLike;
595
719
  getCellFormula?(ref: string): string | null;
596
720
  getSpillRange?(ref: string): string | null;
@@ -611,6 +735,22 @@ declare function createWorkbookRecalcModel(workbook: CoreWorkbookLike): RecalcMo
611
735
  interface RecalcModel {
612
736
  /** Current value of a cell (raw input value, or last computed value). */
613
737
  getCellValue(sheet: string, ref: string): unknown;
738
+ /**
739
+ * Optional: value of a cell by 1-based row/column. Range reads prefer this —
740
+ * it saves building and re-parsing a reference string per cell, which at
741
+ * range scale is tens of millions of strings per recalculation. Must return
742
+ * the same value `getCellValue` would for that coordinate.
743
+ */
744
+ getCellValueAt?(sheet: string, row: number, column: number): unknown;
745
+ /**
746
+ * Optional: the sheet's populated extent, used to bound whole-column (`A:A`)
747
+ * and whole-row (`1:1`) references. Without it those references are `#REF!`,
748
+ * since guessing a bound would silently change what SUM(A:A) adds up.
749
+ */
750
+ getUsedBounds?(sheet: string): {
751
+ rowCount: number;
752
+ columnCount: number;
753
+ } | undefined;
614
754
  /** Store a computed formula result. Must NOT clear the cell's formula. */
615
755
  setCellValue(sheet: string, ref: string, value: unknown): void;
616
756
  /**
@@ -656,6 +796,37 @@ declare class RecalcEngine {
656
796
  * were evaluated. The leading '=' of the formula is optional.
657
797
  */
658
798
  setCellFormula(sheet: string, ref: string, formula: string): RecalcResult[];
799
+ /**
800
+ * Indexes many formulas **without evaluating anything**.
801
+ *
802
+ * This is the path for loading a workbook. `setCellFormula` is built for an
803
+ * edit: it recalculates the new cell and everything downstream of it, which is
804
+ * right for one keystroke and quadratic for a file — registering N formulas one
805
+ * by one evaluates the whole workbook N times. Here the formulas are only
806
+ * parsed and filed into the dependency index; call {@link recalcAll} once
807
+ * afterwards, or nothing at all if the cached values from the file will do.
808
+ *
809
+ * The model is deliberately **not** written to. These formulas are assumed to
810
+ * be the ones the model already holds (they came from the file), and persisting
811
+ * them again would clear each cell's cached value — leaving every formula cell
812
+ * blank until a full recalculation had run. Use `setCellFormula` for formulas
813
+ * the user is actually introducing.
814
+ *
815
+ * A formula that fails to parse is reported rather than thrown: one unsupported
816
+ * construct in a large workbook should not abort the load.
817
+ */
818
+ registerBulk(entries: Iterable<{
819
+ sheet: string;
820
+ ref: string;
821
+ formula: string;
822
+ }>): {
823
+ registered: number;
824
+ failed: Array<{
825
+ sheet: string;
826
+ ref: string;
827
+ error: string;
828
+ }>;
829
+ };
659
830
  /**
660
831
  * Removes a formula from a cell and recalculates its dependents (which may now
661
832
  * read a plain input value instead). Returns the recomputed dependent cells.
@@ -671,8 +842,45 @@ declare class RecalcEngine {
671
842
  * updated) and recalculates its dependents.
672
843
  */
673
844
  onCellChanged(sheet: string, ref: string): RecalcResult[];
674
- /** Recalculates every registered formula in dependency order. */
675
- recalcAll(): RecalcResult[];
845
+ /**
846
+ * Recalculates every registered formula.
847
+ *
848
+ * By default the order is derived here, with a topological sort over the
849
+ * dependency index. `order` — typically `workbook.calcChain` — supplies one
850
+ * instead, so the sort is not needed.
851
+ *
852
+ * **A supplied order is verified, not trusted.** Evaluating in a stale order
853
+ * makes a formula read a precedent that has not been computed yet, and the
854
+ * result is a plausible wrong number with nothing to distinguish it from a
855
+ * right one. In a calculation engine that is the worst failure available, so
856
+ * this checks as it goes: after each formula is written, the dependency index
857
+ * says who reads it, and any reader that was already evaluated is an inversion.
858
+ * On the first inversion the ordered attempt is abandoned and the whole
859
+ * recalculation is redone through the sorted path, whose result is returned;
860
+ * `onOrderConflict`, if given, is told which pair conflicted.
861
+ *
862
+ * The verification costs about what the sort it replaces costs, so `order` is
863
+ * worth roughly nothing now (measured at ~9% of a full recalculation on a
864
+ * 2 000-formula workbook, after the dependency index stopped being the
865
+ * bottleneck). It is kept because reading an order Excel already computed is a
866
+ * reasonable thing to want; prefer plain `recalcAll()` unless you have measured
867
+ * a reason not to.
868
+ *
869
+ * Registered formulas the order does not mention are evaluated afterwards
870
+ * through the sorted path, and a dynamic array that spills still triggers the
871
+ * usual fixpoint, so neither silently misses cells.
872
+ */
873
+ recalcAll(options?: {
874
+ order?: ReadonlyArray<{
875
+ sheet: string;
876
+ ref: string;
877
+ }>;
878
+ /** Called when the supplied order turned out to be invalid and was discarded. */
879
+ onOrderConflict?: (conflict: {
880
+ dependent: string;
881
+ precedent: string;
882
+ }) => void;
883
+ }): RecalcResult[];
676
884
  /** The precedents recorded for a formula cell (mainly for tests/inspection). */
677
885
  getPrecedents(sheet: string, ref: string): readonly PrecedentRef[];
678
886
  /**
@@ -723,4 +931,4 @@ declare function registerAggregate(r: FunctionRegistry): void;
723
931
 
724
932
  declare function registerArray(r: FunctionRegistry): void;
725
933
 
726
- export { ArrayValue, BinaryOpNode, BinaryOperator, BlankNode, BooleanNode, CallNode, CellReferenceNode, ErrorNode, type ExtractResult, FormulaContext, FormulaError, FormulaException, type FormulaFunction, FormulaHelper, FormulaLexer, FormulaNode, FormulaParser, FormulaValue, type FormulaValueKind, FunctionCallNode, type FunctionDef, FunctionRegistry, ImplicitIntersectionNode, type LambdaLike, LruCache, type NameResolver, NamedRangeNode, NumberNode, type PrecedentRef, RangeReferenceNode, RecalcEngine, type RecalcModel, type RecalcResult, SheetCellReferenceNode, SheetRangeReferenceNode, SpillReferenceNode, StringNode, type Token, TokenType, UnaryOpNode, UnaryOperator, type WorkbookLike, type WorksheetLike, clearAstCache, createWorkbookRecalcModel, evaluateAst, evaluateFormula, extractReferences, getAstCacheStats, parseFormula, parseFormulaUncached, refContains, registerAggregate, registerArray, registerDate, registerFinance, registerInfo, registerLogical, registerLookup, registerMath, registerMeta, registerStatistical, registerText, setAstCacheCapacity };
934
+ export { ArrayValue, BinaryOpNode, BinaryOperator, BlankNode, BooleanNode, CallNode, CellReferenceNode, ErrorNode, type ExtractResult, FormulaContext, FormulaError, FormulaException, type FormulaFunction, FormulaHelper, FormulaLexer, FormulaNode, FormulaParser, FormulaValue, type FormulaValueKind, FullRangeReferenceNode, FunctionCallNode, type FunctionDef, FunctionRegistry, ImplicitIntersectionNode, type LambdaLike, LruCache, type NameResolver, NamedRangeNode, NumberNode, type PrecedentRef, RangeCache, RangeReferenceNode, RecalcEngine, type RecalcModel, type RecalcResult, SheetCellReferenceNode, SheetRangeReferenceNode, SpillReferenceNode, StringNode, type Token, TokenType, UnaryOpNode, UnaryOperator, type WorkbookLike, type WorksheetLike, clearAstCache, createWorkbookRecalcModel, evaluateAst, evaluateFormula, extractReferences, getAstCacheStats, parseFormula, parseFormulaUncached, refContains, registerAggregate, registerArray, registerDate, registerFinance, registerInfo, registerLogical, registerLookup, registerMath, registerMeta, registerStatistical, registerText, setAstCacheCapacity };
package/dist/index.d.ts CHANGED
@@ -242,13 +242,98 @@ declare namespace FormulaHelper {
242
242
  };
243
243
  /** Flattens all args into FormulaValue array (expanding arrays). */
244
244
  function flattenArgs(args: FormulaNode[], ctx: FormulaContext): FormulaValue[];
245
+ /**
246
+ * A criteria string analysed once, so testing it against N values does not
247
+ * re-parse it N times.
248
+ *
249
+ * `kind` is what lets a caller index instead of scan: `equalsNumber` and
250
+ * `equalsText` are the two shapes whose matching positions can be looked up,
251
+ * and they are the overwhelming majority in real files (`SUMIFS(H:H, W:W, W282)`).
252
+ * Everything else — comparisons, wildcards — only gets the compiled `test`.
253
+ */
254
+ interface CompiledCriteria {
255
+ readonly kind: 'equalsNumber' | 'equalsText' | 'compareNumber' | 'other';
256
+ /** For `equalsNumber` and `compareNumber`: the number compared against. */
257
+ readonly number?: number;
258
+ /** For `equalsText`: the criterion, upper-cased (matching is case-insensitive). */
259
+ readonly text?: string;
260
+ /** For `compareNumber`: which comparison. */
261
+ readonly op?: '>' | '>=' | '<' | '<=';
262
+ readonly test: (value: FormulaValue) => boolean;
263
+ }
264
+ /**
265
+ * Compiles a criteria string into a reusable predicate.
266
+ *
267
+ * Same semantics as {@link matchesCriteria} — that function is now this one
268
+ * applied immediately — but the parsing, the number conversion and any wildcard
269
+ * RegExp happen once instead of once per cell. A SUMIFS filled down 291 rows
270
+ * over a 291-cell range was doing that 84 000 times.
271
+ */
272
+ function compileCriteria(criteria: string): CompiledCriteria;
245
273
  /** Matches a criteria string (e.g. ">5", "=hello", "*partial*") against a value. */
246
274
  function matchesCriteria(value: FormulaValue, criteria: string): boolean;
247
275
  }
248
276
 
277
+ /**
278
+ * Memo for range reads: `(sheet, rectangle) → array value`.
279
+ *
280
+ * Why this exists. A range reference is resolved by materialising an ArrayValue
281
+ * and reading every cell in the rectangle. The dominant pattern in real
282
+ * workbooks is many formulas reading the *same* range — a SUMIFS filled down
283
+ * 291 rows reads its two 291-cell ranges 291 times each — so the same rectangle
284
+ * is rebuilt over and over. Memoising it turns O(formulas × range) into
285
+ * O(distinct ranges × range) for the read side, which is the difference between
286
+ * a recalculation that finishes and one that does not.
287
+ *
288
+ * Why invalidation is per-rectangle rather than per-sheet. During a recalc pass
289
+ * the engine writes a computed value after every formula, so a sheet-wide
290
+ * generation counter would invalidate the whole cache thousands of times per
291
+ * pass and never hit. Instead each cached rectangle is filed into a coarse tile
292
+ * grid (same shape as {@link DependencyIndex}), and a write only drops the
293
+ * rectangles that actually contain the written cell — writes to a column of
294
+ * results leave the input ranges cached.
295
+ *
296
+ * Safety. Cached values are shared, not copied, so a reader must not mutate the
297
+ * ArrayValue it receives. Every built-in function builds its own result array;
298
+ * none writes into an argument.
299
+ */
300
+
301
+ declare class RangeCache {
302
+ #private;
303
+ hits: number;
304
+ misses: number;
305
+ constructor(maxCells?: number);
306
+ get size(): number;
307
+ get cachedCells(): number;
308
+ static key(sheet: string, r0: number, c0: number, r1: number, c1: number): string;
309
+ get(sheet: string, r0: number, c0: number, r1: number, c1: number): FormulaValue | undefined;
310
+ /** Memoises a rectangle's value. `cells` is its area, for the budget. */
311
+ set(sheet: string, r0: number, c0: number, r1: number, c1: number, value: FormulaValue, cells: number): void;
312
+ /** Drops every cached rectangle containing the given cell. */
313
+ invalidateCell(sheet: string, row: number, col: number): void;
314
+ clear(): void;
315
+ }
316
+
249
317
  interface WorksheetLike {
250
318
  getCellValue(reference: string): unknown;
251
319
  name: string;
320
+ /**
321
+ * Optional: value of a cell by 1-based row/column. Range reads use this when
322
+ * present, which saves building — and re-parsing — one reference string per
323
+ * cell. Implementations that resolve values lazily inside getCellValue should
324
+ * leave it undefined to keep that path.
325
+ */
326
+ getCellValueAt?(row: number, column: number): unknown;
327
+ /**
328
+ * Optional: the populated extent of the sheet — the highest 1-based row and
329
+ * column that hold a cell (0 when empty). Used to bound whole-column and
330
+ * whole-row references (`A:A`, `1:1`), which Excel evaluates over the used
331
+ * range rather than all 1,048,576 rows.
332
+ */
333
+ readonly usedBounds?: {
334
+ rowCount: number;
335
+ columnCount: number;
336
+ };
252
337
  /** Optional: whether a 1-based row is hidden (used by SUBTOTAL/AGGREGATE). */
253
338
  isRowHidden?(row: number): boolean;
254
339
  /** Optional: formula text of a cell (used by SUBTOTAL to skip nested calls). */
@@ -277,6 +362,13 @@ declare class FormulaContext {
277
362
  formulaCol: number;
278
363
  /** Optional workbook reference for cross-sheet references. */
279
364
  workbook?: WorkbookLike;
365
+ /**
366
+ * Optional memo for range reads, supplied by whoever owns the write side —
367
+ * the recalculation engine, which invalidates it as it writes results. Left
368
+ * undefined for one-shot evaluation, where a stale entry could otherwise
369
+ * outlive a caller's direct edit to the model.
370
+ */
371
+ rangeCache?: RangeCache;
280
372
  /**
281
373
  * Optional named-range resolver, installed by the evaluator when the workbook
282
374
  * exposes getDefinedName. Given a name, returns its evaluated value. Left
@@ -301,8 +393,26 @@ declare class FormulaContext {
301
393
  get worksheet(): WorksheetLike;
302
394
  getCellValue(cellReference: string): FormulaValue;
303
395
  getRangeValues(startRef: string, endRef: string): FormulaValue;
396
+ /**
397
+ * Resolves a rectangle already known in coordinates. Split out from
398
+ * {@link getRangeValues} because whole-column references and the range memo
399
+ * both work in numbers, and formatting them back into `A1` strings only to
400
+ * re-parse them is measurable at range scale.
401
+ */
402
+ getRangeValuesByIndex(startRow: number, startCol: number, endRow: number, endCol: number): FormulaValue;
403
+ /**
404
+ * Resolves a whole-column (`A:A`) or whole-row (`1:1`) reference, bounded by
405
+ * the sheet's used range the way Excel does. Without a bound this would
406
+ * materialise a million-cell array for a sheet with twelve rows in it.
407
+ *
408
+ * A sheet that cannot report its extent yields `#REF!` rather than a guess:
409
+ * silently reading nothing would make SUM(A:A) return 0 on a full column.
410
+ */
411
+ getFullRangeValues(startCol: number | null, endCol: number | null, startRow: number | null, endRow: number | null): FormulaValue;
304
412
  getSheetCellValue(sheetName: string, cellReference: string): FormulaValue;
305
413
  getSheetRangeValues(sheetName: string, startRef: string, endRef: string): FormulaValue;
414
+ /** Whole-column/whole-row reference qualified by a sheet name. */
415
+ getSheetFullRangeValues(sheetName: string, startCol: number | null, endCol: number | null, startRow: number | null, endRow: number | null): FormulaValue;
306
416
  resolveNamedRange(name: string): FormulaValue;
307
417
  /**
308
418
  * Resolves a spill-range reference (A1#) to the array spilling from the anchor
@@ -383,6 +493,25 @@ declare class SheetRangeReferenceNode extends FormulaNode {
383
493
  constructor(sheetName: string, startRef: string, endRef: string);
384
494
  evaluate(ctx: FormulaContext): FormulaValue;
385
495
  }
496
+ /**
497
+ * A whole-column (`A:A`, `$B:$D`) or whole-row (`1:1`, `3:7`) reference,
498
+ * optionally sheet-qualified (`Sheet1!A:A`).
499
+ *
500
+ * Exactly one dimension is bounded: a column range fixes the columns and leaves
501
+ * the rows open, a row range the reverse. The open dimension is resolved against
502
+ * the sheet's used range at evaluation time rather than baked in as 1..1048576 —
503
+ * expanding it eagerly would turn `SUM(A:A)` on a twelve-row sheet into a
504
+ * million-cell read, and Excel's own answer is the used range too.
505
+ */
506
+ declare class FullRangeReferenceNode extends FormulaNode {
507
+ readonly startCol: number | null;
508
+ readonly endCol: number | null;
509
+ readonly startRow: number | null;
510
+ readonly endRow: number | null;
511
+ readonly sheetName?: string | undefined;
512
+ constructor(startCol: number | null, endCol: number | null, startRow: number | null, endRow: number | null, sheetName?: string | undefined);
513
+ evaluate(ctx: FormulaContext): FormulaValue;
514
+ }
386
515
  declare class NamedRangeNode extends FormulaNode {
387
516
  readonly name: string;
388
517
  constructor(name: string);
@@ -452,21 +581,6 @@ declare class CallNode extends FormulaNode {
452
581
  evaluateTail(ctx: FormulaContext): FormulaValue | TailCall;
453
582
  }
454
583
 
455
- /**
456
- * Recursive descent parser converting formula tokens into an AST.
457
- * Port of TVE.PureDocs.Excel.Formulas.FormulaParser (C#)
458
- *
459
- * Operator precedence (low → high):
460
- * Comparison: =, <>, <, <=, >, >=
461
- * Concatenation: &
462
- * Addition/Subtraction: +, -
463
- * Multiplication/Division: *, /
464
- * Unary: +, -, @ (prefix)
465
- * Power: ^ (right-associative)
466
- * Percent: % (postfix)
467
- * Primary: literals, refs, functions, parentheses
468
- */
469
-
470
584
  declare class FormulaParser {
471
585
  #private;
472
586
  constructor(tokens: Token[]);
@@ -505,6 +619,11 @@ declare function evaluateAst(ast: FormulaNode, sheet: WorksheetLike, options?: {
505
619
  workbook?: WorkbookLike;
506
620
  formulaRow?: number;
507
621
  formulaCol?: number;
622
+ /**
623
+ * Range memo shared across evaluations. Only pass one if you invalidate it
624
+ * on every write to the model — {@link RecalcEngine} does.
625
+ */
626
+ rangeCache?: RangeCache;
508
627
  }): FormulaValue;
509
628
  declare function clearAstCache(): void;
510
629
  declare function getAstCacheStats(): {
@@ -591,6 +710,11 @@ interface CoreCellLike {
591
710
  }
592
711
  interface CoreSheetLike {
593
712
  getCellValue(ref: string): unknown;
713
+ getCellValueAt?(row: number, column: number): unknown;
714
+ readonly usedBounds?: {
715
+ rowCount: number;
716
+ columnCount: number;
717
+ };
594
718
  getCell(ref: string): CoreCellLike;
595
719
  getCellFormula?(ref: string): string | null;
596
720
  getSpillRange?(ref: string): string | null;
@@ -611,6 +735,22 @@ declare function createWorkbookRecalcModel(workbook: CoreWorkbookLike): RecalcMo
611
735
  interface RecalcModel {
612
736
  /** Current value of a cell (raw input value, or last computed value). */
613
737
  getCellValue(sheet: string, ref: string): unknown;
738
+ /**
739
+ * Optional: value of a cell by 1-based row/column. Range reads prefer this —
740
+ * it saves building and re-parsing a reference string per cell, which at
741
+ * range scale is tens of millions of strings per recalculation. Must return
742
+ * the same value `getCellValue` would for that coordinate.
743
+ */
744
+ getCellValueAt?(sheet: string, row: number, column: number): unknown;
745
+ /**
746
+ * Optional: the sheet's populated extent, used to bound whole-column (`A:A`)
747
+ * and whole-row (`1:1`) references. Without it those references are `#REF!`,
748
+ * since guessing a bound would silently change what SUM(A:A) adds up.
749
+ */
750
+ getUsedBounds?(sheet: string): {
751
+ rowCount: number;
752
+ columnCount: number;
753
+ } | undefined;
614
754
  /** Store a computed formula result. Must NOT clear the cell's formula. */
615
755
  setCellValue(sheet: string, ref: string, value: unknown): void;
616
756
  /**
@@ -656,6 +796,37 @@ declare class RecalcEngine {
656
796
  * were evaluated. The leading '=' of the formula is optional.
657
797
  */
658
798
  setCellFormula(sheet: string, ref: string, formula: string): RecalcResult[];
799
+ /**
800
+ * Indexes many formulas **without evaluating anything**.
801
+ *
802
+ * This is the path for loading a workbook. `setCellFormula` is built for an
803
+ * edit: it recalculates the new cell and everything downstream of it, which is
804
+ * right for one keystroke and quadratic for a file — registering N formulas one
805
+ * by one evaluates the whole workbook N times. Here the formulas are only
806
+ * parsed and filed into the dependency index; call {@link recalcAll} once
807
+ * afterwards, or nothing at all if the cached values from the file will do.
808
+ *
809
+ * The model is deliberately **not** written to. These formulas are assumed to
810
+ * be the ones the model already holds (they came from the file), and persisting
811
+ * them again would clear each cell's cached value — leaving every formula cell
812
+ * blank until a full recalculation had run. Use `setCellFormula` for formulas
813
+ * the user is actually introducing.
814
+ *
815
+ * A formula that fails to parse is reported rather than thrown: one unsupported
816
+ * construct in a large workbook should not abort the load.
817
+ */
818
+ registerBulk(entries: Iterable<{
819
+ sheet: string;
820
+ ref: string;
821
+ formula: string;
822
+ }>): {
823
+ registered: number;
824
+ failed: Array<{
825
+ sheet: string;
826
+ ref: string;
827
+ error: string;
828
+ }>;
829
+ };
659
830
  /**
660
831
  * Removes a formula from a cell and recalculates its dependents (which may now
661
832
  * read a plain input value instead). Returns the recomputed dependent cells.
@@ -671,8 +842,45 @@ declare class RecalcEngine {
671
842
  * updated) and recalculates its dependents.
672
843
  */
673
844
  onCellChanged(sheet: string, ref: string): RecalcResult[];
674
- /** Recalculates every registered formula in dependency order. */
675
- recalcAll(): RecalcResult[];
845
+ /**
846
+ * Recalculates every registered formula.
847
+ *
848
+ * By default the order is derived here, with a topological sort over the
849
+ * dependency index. `order` — typically `workbook.calcChain` — supplies one
850
+ * instead, so the sort is not needed.
851
+ *
852
+ * **A supplied order is verified, not trusted.** Evaluating in a stale order
853
+ * makes a formula read a precedent that has not been computed yet, and the
854
+ * result is a plausible wrong number with nothing to distinguish it from a
855
+ * right one. In a calculation engine that is the worst failure available, so
856
+ * this checks as it goes: after each formula is written, the dependency index
857
+ * says who reads it, and any reader that was already evaluated is an inversion.
858
+ * On the first inversion the ordered attempt is abandoned and the whole
859
+ * recalculation is redone through the sorted path, whose result is returned;
860
+ * `onOrderConflict`, if given, is told which pair conflicted.
861
+ *
862
+ * The verification costs about what the sort it replaces costs, so `order` is
863
+ * worth roughly nothing now (measured at ~9% of a full recalculation on a
864
+ * 2 000-formula workbook, after the dependency index stopped being the
865
+ * bottleneck). It is kept because reading an order Excel already computed is a
866
+ * reasonable thing to want; prefer plain `recalcAll()` unless you have measured
867
+ * a reason not to.
868
+ *
869
+ * Registered formulas the order does not mention are evaluated afterwards
870
+ * through the sorted path, and a dynamic array that spills still triggers the
871
+ * usual fixpoint, so neither silently misses cells.
872
+ */
873
+ recalcAll(options?: {
874
+ order?: ReadonlyArray<{
875
+ sheet: string;
876
+ ref: string;
877
+ }>;
878
+ /** Called when the supplied order turned out to be invalid and was discarded. */
879
+ onOrderConflict?: (conflict: {
880
+ dependent: string;
881
+ precedent: string;
882
+ }) => void;
883
+ }): RecalcResult[];
676
884
  /** The precedents recorded for a formula cell (mainly for tests/inspection). */
677
885
  getPrecedents(sheet: string, ref: string): readonly PrecedentRef[];
678
886
  /**
@@ -723,4 +931,4 @@ declare function registerAggregate(r: FunctionRegistry): void;
723
931
 
724
932
  declare function registerArray(r: FunctionRegistry): void;
725
933
 
726
- export { ArrayValue, BinaryOpNode, BinaryOperator, BlankNode, BooleanNode, CallNode, CellReferenceNode, ErrorNode, type ExtractResult, FormulaContext, FormulaError, FormulaException, type FormulaFunction, FormulaHelper, FormulaLexer, FormulaNode, FormulaParser, FormulaValue, type FormulaValueKind, FunctionCallNode, type FunctionDef, FunctionRegistry, ImplicitIntersectionNode, type LambdaLike, LruCache, type NameResolver, NamedRangeNode, NumberNode, type PrecedentRef, RangeReferenceNode, RecalcEngine, type RecalcModel, type RecalcResult, SheetCellReferenceNode, SheetRangeReferenceNode, SpillReferenceNode, StringNode, type Token, TokenType, UnaryOpNode, UnaryOperator, type WorkbookLike, type WorksheetLike, clearAstCache, createWorkbookRecalcModel, evaluateAst, evaluateFormula, extractReferences, getAstCacheStats, parseFormula, parseFormulaUncached, refContains, registerAggregate, registerArray, registerDate, registerFinance, registerInfo, registerLogical, registerLookup, registerMath, registerMeta, registerStatistical, registerText, setAstCacheCapacity };
934
+ export { ArrayValue, BinaryOpNode, BinaryOperator, BlankNode, BooleanNode, CallNode, CellReferenceNode, ErrorNode, type ExtractResult, FormulaContext, FormulaError, FormulaException, type FormulaFunction, FormulaHelper, FormulaLexer, FormulaNode, FormulaParser, FormulaValue, type FormulaValueKind, FullRangeReferenceNode, FunctionCallNode, type FunctionDef, FunctionRegistry, ImplicitIntersectionNode, type LambdaLike, LruCache, type NameResolver, NamedRangeNode, NumberNode, type PrecedentRef, RangeCache, RangeReferenceNode, RecalcEngine, type RecalcModel, type RecalcResult, SheetCellReferenceNode, SheetRangeReferenceNode, SpillReferenceNode, StringNode, type Token, TokenType, UnaryOpNode, UnaryOperator, type WorkbookLike, type WorksheetLike, clearAstCache, createWorkbookRecalcModel, evaluateAst, evaluateFormula, extractReferences, getAstCacheStats, parseFormula, parseFormulaUncached, refContains, registerAggregate, registerArray, registerDate, registerFinance, registerInfo, registerLogical, registerLookup, registerMath, registerMeta, registerStatistical, registerText, setAstCacheCapacity };