@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.cjs +793 -120
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +226 -18
- package/dist/index.d.ts +226 -18
- package/dist/index.js +793 -122
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
/**
|
|
675
|
-
|
|
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
|
-
/**
|
|
675
|
-
|
|
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 };
|