@devmm/puredocs-excel-formula 1.0.6 → 1.0.8

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