@devmm/puredocs-excel-formula 1.0.5 → 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.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[]);
@@ -477,6 +591,15 @@ declare class FormulaParser {
477
591
  * Parses a formula string to an AST, with LRU caching.
478
592
  */
479
593
  declare function parseFormula(formula: string): FormulaNode;
594
+ /**
595
+ * Parses a formula without consulting or populating the shared AST cache.
596
+ *
597
+ * For callers that keep the AST themselves — the recalculation engine holds one
598
+ * per registered formula — the cache is pure overhead: it would evict entries
599
+ * that are still live elsewhere once a workbook has more formulas than its
600
+ * capacity, paying eviction cost and retaining memory for no hits.
601
+ */
602
+ declare function parseFormulaUncached(formula: string): FormulaNode;
480
603
  /**
481
604
  * Evaluates a formula string against a worksheet.
482
605
  * Mirrors FormulaEvaluator.Evaluate() in C#.
@@ -486,6 +609,22 @@ declare function evaluateFormula(formula: string, sheet: WorksheetLike, options?
486
609
  formulaRow?: number;
487
610
  formulaCol?: number;
488
611
  }): FormulaValue;
612
+ /**
613
+ * Evaluates an already-parsed formula. Callers that hold onto an AST — such as
614
+ * the recalculation engine, which parses each formula once when it is
615
+ * registered — should use this so repeated evaluation never depends on the
616
+ * global AST cache still holding the entry.
617
+ */
618
+ declare function evaluateAst(ast: FormulaNode, sheet: WorksheetLike, options?: {
619
+ workbook?: WorkbookLike;
620
+ formulaRow?: number;
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;
627
+ }): FormulaValue;
489
628
  declare function clearAstCache(): void;
490
629
  declare function getAstCacheStats(): {
491
630
  size: number;
@@ -571,6 +710,11 @@ interface CoreCellLike {
571
710
  }
572
711
  interface CoreSheetLike {
573
712
  getCellValue(ref: string): unknown;
713
+ getCellValueAt?(row: number, column: number): unknown;
714
+ readonly usedBounds?: {
715
+ rowCount: number;
716
+ columnCount: number;
717
+ };
574
718
  getCell(ref: string): CoreCellLike;
575
719
  getCellFormula?(ref: string): string | null;
576
720
  getSpillRange?(ref: string): string | null;
@@ -591,6 +735,22 @@ declare function createWorkbookRecalcModel(workbook: CoreWorkbookLike): RecalcMo
591
735
  interface RecalcModel {
592
736
  /** Current value of a cell (raw input value, or last computed value). */
593
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;
594
754
  /** Store a computed formula result. Must NOT clear the cell's formula. */
595
755
  setCellValue(sheet: string, ref: string, value: unknown): void;
596
756
  /**
@@ -636,6 +796,37 @@ declare class RecalcEngine {
636
796
  * were evaluated. The leading '=' of the formula is optional.
637
797
  */
638
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
+ };
639
830
  /**
640
831
  * Removes a formula from a cell and recalculates its dependents (which may now
641
832
  * read a plain input value instead). Returns the recomputed dependent cells.
@@ -651,10 +842,71 @@ declare class RecalcEngine {
651
842
  * updated) and recalculates its dependents.
652
843
  */
653
844
  onCellChanged(sheet: string, ref: string): RecalcResult[];
654
- /** Recalculates every registered formula in dependency order. */
655
- 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[];
656
884
  /** The precedents recorded for a formula cell (mainly for tests/inspection). */
657
885
  getPrecedents(sheet: string, ref: string): readonly PrecedentRef[];
886
+ /**
887
+ * Tells the engine rows were inserted on a sheet. Call AFTER the
888
+ * corresponding `Worksheet.insertRows` (the model must already reflect the
889
+ * edit). The dependency graph is translated in place — registrations,
890
+ * formula text and spill bookkeeping all move — and only volatile formulas
891
+ * are recomputed, because a pure translation changes no values.
892
+ *
893
+ * @example
894
+ * ws.insertRows(3, 2);
895
+ * engine.onRowsInserted('S1', 3, 2);
896
+ */
897
+ onRowsInserted(sheet: string, at: number, count?: number): RecalcResult[];
898
+ /**
899
+ * Tells the engine rows were deleted on a sheet. Call AFTER the
900
+ * corresponding `Worksheet.deleteRows`. Registrations inside the deleted
901
+ * band are dropped; the rest of the graph is translated, and only formulas
902
+ * whose precedents touched the deleted band (plus their dependents and
903
+ * volatiles) are recomputed.
904
+ */
905
+ onRowsDeleted(sheet: string, at: number, count?: number): RecalcResult[];
906
+ /** Column counterpart to {@link onRowsInserted}. */
907
+ onColumnsInserted(sheet: string, at: number, count?: number): RecalcResult[];
908
+ /** Column counterpart to {@link onRowsDeleted}. */
909
+ onColumnsDeleted(sheet: string, at: number, count?: number): RecalcResult[];
658
910
  }
659
911
 
660
912
  declare function registerMath(r: FunctionRegistry): void;
@@ -679,4 +931,4 @@ declare function registerAggregate(r: FunctionRegistry): void;
679
931
 
680
932
  declare function registerArray(r: FunctionRegistry): void;
681
933
 
682
- 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, evaluateFormula, extractReferences, getAstCacheStats, parseFormula, 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 };