@molecule/api-formula-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/dependencies.d.ts +44 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +110 -0
- package/dist/evaluator.d.ts +29 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +159 -0
- package/dist/functions.d.ts +22 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +774 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +255 -0
- package/dist/references.d.ts +57 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +116 -0
- package/dist/sheet.d.ts +130 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/sheet.js +291 -0
- package/dist/tokenizer.d.ts +32 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +187 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/values.d.ts +80 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +190 -0
- package/package.json +33 -0
package/dist/sheet.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level Sheet engine.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the parser, evaluator, and dependency tracker into a small,
|
|
5
|
+
* stateful container modelled after a single-sheet spreadsheet. All
|
|
6
|
+
* mutation happens through `setValue` / `setFormula` / `clear`; reads
|
|
7
|
+
* happen through `getValue` (returns the most recently computed value)
|
|
8
|
+
* and `getFormula` (returns the formula text, if any).
|
|
9
|
+
*
|
|
10
|
+
* Sheets are *pure data containers* — no I/O, no DB, no async. They
|
|
11
|
+
* are intentionally small so they can be embedded inside a larger
|
|
12
|
+
* resource (e.g. a `Spreadsheet` resource handler) which is
|
|
13
|
+
* responsible for persistence.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { Sheet } from '@molecule/api-formula-engine'
|
|
18
|
+
*
|
|
19
|
+
* const sheet = new Sheet()
|
|
20
|
+
* sheet.setValue('A1', 10)
|
|
21
|
+
* sheet.setValue('A2', 20)
|
|
22
|
+
* sheet.setFormula('A3', '=SUM(A1:A2)')
|
|
23
|
+
* sheet.getValue('A3') // → 30
|
|
24
|
+
*
|
|
25
|
+
* sheet.setValue('A1', 100)
|
|
26
|
+
* sheet.getValue('A3') // → 120 (auto-recomputed)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import type { CellCoord, CellValue, EvaluateOptions, SheetConfig } from './types.js';
|
|
32
|
+
/**
|
|
33
|
+
* In-memory single-sheet engine. References use A1 notation (`A1`,
|
|
34
|
+
* `$A$1`, `BC42`).
|
|
35
|
+
*/
|
|
36
|
+
export declare class Sheet {
|
|
37
|
+
private readonly cells;
|
|
38
|
+
/** dependent-of[key] = set of cells that reference `key`. */
|
|
39
|
+
private readonly dependents;
|
|
40
|
+
private readonly config;
|
|
41
|
+
/**
|
|
42
|
+
* Create a new Sheet with optional dimension limits.
|
|
43
|
+
*
|
|
44
|
+
* @param config - Sheet configuration.
|
|
45
|
+
*/
|
|
46
|
+
constructor(config?: SheetConfig);
|
|
47
|
+
/**
|
|
48
|
+
* Set a literal value at a cell.
|
|
49
|
+
*
|
|
50
|
+
* @param ref - A1-style cell reference (e.g. `A1`, `$B$3`).
|
|
51
|
+
* @param value - Literal value (number, string, boolean, Date, or null).
|
|
52
|
+
* @param options - Evaluation options forwarded to recompute.
|
|
53
|
+
*/
|
|
54
|
+
setValue(ref: string, value: CellValue, options?: EvaluateOptions): void;
|
|
55
|
+
/**
|
|
56
|
+
* Set a formula at a cell. The formula text may include or omit the
|
|
57
|
+
* leading `=`. The formula is parsed eagerly and `#NAME?` /
|
|
58
|
+
* `#VALUE!` errors caused by parser failures are stored as the
|
|
59
|
+
* cell's value.
|
|
60
|
+
*
|
|
61
|
+
* @param ref - A1-style cell reference.
|
|
62
|
+
* @param formula - Formula text (e.g. `=SUM(A1:A10)` or `SUM(A1:A10)`).
|
|
63
|
+
* @param options - Evaluation options forwarded to recompute.
|
|
64
|
+
*/
|
|
65
|
+
setFormula(ref: string, formula: string, options?: EvaluateOptions): void;
|
|
66
|
+
/**
|
|
67
|
+
* Clear a cell (remove value, formula, and dependency edges).
|
|
68
|
+
*
|
|
69
|
+
* @param ref - A1-style cell reference.
|
|
70
|
+
* @param options - Evaluation options forwarded to recompute.
|
|
71
|
+
*/
|
|
72
|
+
clear(ref: string, options?: EvaluateOptions): void;
|
|
73
|
+
/**
|
|
74
|
+
* Read the most recently computed value for a cell.
|
|
75
|
+
*
|
|
76
|
+
* @param ref - A1-style cell reference.
|
|
77
|
+
* @returns Cell value (`null` if the cell is empty).
|
|
78
|
+
*/
|
|
79
|
+
getValue(ref: string): CellValue;
|
|
80
|
+
/**
|
|
81
|
+
* Return the formula text for a cell, or `null` if the cell holds a
|
|
82
|
+
* literal value or is empty.
|
|
83
|
+
*
|
|
84
|
+
* @param ref - A1-style cell reference.
|
|
85
|
+
* @returns Formula text (with leading `=`) or `null`.
|
|
86
|
+
*/
|
|
87
|
+
getFormula(ref: string): string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Recompute every formula cell. Useful after bulk edits or when
|
|
90
|
+
* `now()` semantics change.
|
|
91
|
+
*
|
|
92
|
+
* @param options - Evaluation options.
|
|
93
|
+
*/
|
|
94
|
+
recomputeAll(options?: EvaluateOptions): void;
|
|
95
|
+
/**
|
|
96
|
+
* Evaluate a free-standing formula string against this sheet's data.
|
|
97
|
+
* Does NOT mutate the sheet. Useful for ad-hoc evaluations such as
|
|
98
|
+
* invoice line-item totals or computed-column previews.
|
|
99
|
+
*
|
|
100
|
+
* @param formula - Formula text.
|
|
101
|
+
* @param options - Evaluation options.
|
|
102
|
+
* @returns Evaluated cell value.
|
|
103
|
+
*/
|
|
104
|
+
evaluateFormula(formula: string, options?: EvaluateOptions): CellValue;
|
|
105
|
+
/**
|
|
106
|
+
* Parse an A1-style reference string into a validated CellCoord.
|
|
107
|
+
*/
|
|
108
|
+
private refToCoord;
|
|
109
|
+
/**
|
|
110
|
+
* Write a cell record, rewire dependency edges, and trigger downstream recompute.
|
|
111
|
+
*/
|
|
112
|
+
private assignCell;
|
|
113
|
+
/**
|
|
114
|
+
* Recompute all formula cells that transitively depend on the given key.
|
|
115
|
+
*/
|
|
116
|
+
private recomputeDownstream;
|
|
117
|
+
/**
|
|
118
|
+
* Evaluate (or propagate) the computed value for a single cell key.
|
|
119
|
+
*/
|
|
120
|
+
private evaluateKey;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reformat a cell coordinate as A1-style text. Re-exported for
|
|
124
|
+
* convenience.
|
|
125
|
+
*
|
|
126
|
+
* @param coord - Cell coordinate.
|
|
127
|
+
* @returns A1-style reference text.
|
|
128
|
+
*/
|
|
129
|
+
export declare function refOf(coord: CellCoord): string;
|
|
130
|
+
//# sourceMappingURL=sheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sheet.d.ts","sourceRoot":"","sources":["../src/sheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH,OAAO,KAAK,EAAW,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAqB7F;;;GAGG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,6DAA6D;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;IAC5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAE9C;;;;OAIG;gBACS,MAAM,GAAE,WAAgB;IAIpC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,eAAoB,GAAG,IAAI;IAK5E;;;;;;;;;OASG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,IAAI;IAgB7E;;;;;OAKG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,IAAI;IAavD;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAOhC;;;;;;OAMG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMtC;;;;;OAKG;IACH,YAAY,CAAC,OAAO,GAAE,eAAoB,GAAG,IAAI;IAWjD;;;;;;;;OAQG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,SAAS;IAY1E;;OAEG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,UAAU;IAuClB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA0B3B;;OAEG;IACH,OAAO,CAAC,WAAW;CA0BpB;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAE9C"}
|
package/dist/sheet.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level Sheet engine.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the parser, evaluator, and dependency tracker into a small,
|
|
5
|
+
* stateful container modelled after a single-sheet spreadsheet. All
|
|
6
|
+
* mutation happens through `setValue` / `setFormula` / `clear`; reads
|
|
7
|
+
* happen through `getValue` (returns the most recently computed value)
|
|
8
|
+
* and `getFormula` (returns the formula text, if any).
|
|
9
|
+
*
|
|
10
|
+
* Sheets are *pure data containers* — no I/O, no DB, no async. They
|
|
11
|
+
* are intentionally small so they can be embedded inside a larger
|
|
12
|
+
* resource (e.g. a `Spreadsheet` resource handler) which is
|
|
13
|
+
* responsible for persistence.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { Sheet } from '@molecule/api-formula-engine'
|
|
18
|
+
*
|
|
19
|
+
* const sheet = new Sheet()
|
|
20
|
+
* sheet.setValue('A1', 10)
|
|
21
|
+
* sheet.setValue('A2', 20)
|
|
22
|
+
* sheet.setFormula('A3', '=SUM(A1:A2)')
|
|
23
|
+
* sheet.getValue('A3') // → 30
|
|
24
|
+
*
|
|
25
|
+
* sheet.setValue('A1', 100)
|
|
26
|
+
* sheet.getValue('A3') // → 120 (auto-recomputed)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { collectReferences, topologicalSort } from './dependencies.js';
|
|
32
|
+
import { evaluate } from './evaluator.js';
|
|
33
|
+
import { parseFormula } from './parser.js';
|
|
34
|
+
import { coordKey, formatCellReference, parseCellReference } from './references.js';
|
|
35
|
+
import { isError, makeError } from './values.js';
|
|
36
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
37
|
+
numCols: 16384,
|
|
38
|
+
numRows: 1048576,
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* In-memory single-sheet engine. References use A1 notation (`A1`,
|
|
42
|
+
* `$A$1`, `BC42`).
|
|
43
|
+
*/
|
|
44
|
+
export class Sheet {
|
|
45
|
+
cells = new Map();
|
|
46
|
+
/** dependent-of[key] = set of cells that reference `key`. */
|
|
47
|
+
dependents = new Map();
|
|
48
|
+
config;
|
|
49
|
+
/**
|
|
50
|
+
* Create a new Sheet with optional dimension limits.
|
|
51
|
+
*
|
|
52
|
+
* @param config - Sheet configuration.
|
|
53
|
+
*/
|
|
54
|
+
constructor(config = {}) {
|
|
55
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Set a literal value at a cell.
|
|
59
|
+
*
|
|
60
|
+
* @param ref - A1-style cell reference (e.g. `A1`, `$B$3`).
|
|
61
|
+
* @param value - Literal value (number, string, boolean, Date, or null).
|
|
62
|
+
* @param options - Evaluation options forwarded to recompute.
|
|
63
|
+
*/
|
|
64
|
+
setValue(ref, value, options = {}) {
|
|
65
|
+
const coord = this.refToCoord(ref);
|
|
66
|
+
this.assignCell(coord, value, null, null, options);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Set a formula at a cell. The formula text may include or omit the
|
|
70
|
+
* leading `=`. The formula is parsed eagerly and `#NAME?` /
|
|
71
|
+
* `#VALUE!` errors caused by parser failures are stored as the
|
|
72
|
+
* cell's value.
|
|
73
|
+
*
|
|
74
|
+
* @param ref - A1-style cell reference.
|
|
75
|
+
* @param formula - Formula text (e.g. `=SUM(A1:A10)` or `SUM(A1:A10)`).
|
|
76
|
+
* @param options - Evaluation options forwarded to recompute.
|
|
77
|
+
*/
|
|
78
|
+
setFormula(ref, formula, options = {}) {
|
|
79
|
+
const coord = this.refToCoord(ref);
|
|
80
|
+
let ast = null;
|
|
81
|
+
let parseError = null;
|
|
82
|
+
try {
|
|
83
|
+
ast = parseFormula(formula);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
parseError = makeError('#VALUE!', e.message);
|
|
87
|
+
}
|
|
88
|
+
if (parseError) {
|
|
89
|
+
this.assignCell(coord, parseError, null, formula, options);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.assignCell(coord, null, ast, formula, options);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Clear a cell (remove value, formula, and dependency edges).
|
|
96
|
+
*
|
|
97
|
+
* @param ref - A1-style cell reference.
|
|
98
|
+
* @param options - Evaluation options forwarded to recompute.
|
|
99
|
+
*/
|
|
100
|
+
clear(ref, options = {}) {
|
|
101
|
+
const coord = this.refToCoord(ref);
|
|
102
|
+
const key = coordKey(coord);
|
|
103
|
+
const existing = this.cells.get(key);
|
|
104
|
+
if (existing) {
|
|
105
|
+
for (const dep of existing.precedents) {
|
|
106
|
+
this.dependents.get(dep)?.delete(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.cells.delete(key);
|
|
110
|
+
this.recomputeDownstream(key, options);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Read the most recently computed value for a cell.
|
|
114
|
+
*
|
|
115
|
+
* @param ref - A1-style cell reference.
|
|
116
|
+
* @returns Cell value (`null` if the cell is empty).
|
|
117
|
+
*/
|
|
118
|
+
getValue(ref) {
|
|
119
|
+
const coord = this.refToCoord(ref);
|
|
120
|
+
const key = coordKey(coord);
|
|
121
|
+
const cell = this.cells.get(key);
|
|
122
|
+
return cell ? cell.computed : null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Return the formula text for a cell, or `null` if the cell holds a
|
|
126
|
+
* literal value or is empty.
|
|
127
|
+
*
|
|
128
|
+
* @param ref - A1-style cell reference.
|
|
129
|
+
* @returns Formula text (with leading `=`) or `null`.
|
|
130
|
+
*/
|
|
131
|
+
getFormula(ref) {
|
|
132
|
+
const coord = this.refToCoord(ref);
|
|
133
|
+
const key = coordKey(coord);
|
|
134
|
+
return this.cells.get(key)?.formula ?? null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Recompute every formula cell. Useful after bulk edits or when
|
|
138
|
+
* `now()` semantics change.
|
|
139
|
+
*
|
|
140
|
+
* @param options - Evaluation options.
|
|
141
|
+
*/
|
|
142
|
+
recomputeAll(options = {}) {
|
|
143
|
+
const keys = [...this.cells.keys()];
|
|
144
|
+
const { order, cycle } = topologicalSort(this.dependents, keys);
|
|
145
|
+
for (const k of order)
|
|
146
|
+
this.evaluateKey(k, options, cycle);
|
|
147
|
+
for (const k of cycle) {
|
|
148
|
+
const cell = this.cells.get(k);
|
|
149
|
+
if (!cell)
|
|
150
|
+
continue;
|
|
151
|
+
cell.computed = makeError('#CIRC!');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Evaluate a free-standing formula string against this sheet's data.
|
|
156
|
+
* Does NOT mutate the sheet. Useful for ad-hoc evaluations such as
|
|
157
|
+
* invoice line-item totals or computed-column previews.
|
|
158
|
+
*
|
|
159
|
+
* @param formula - Formula text.
|
|
160
|
+
* @param options - Evaluation options.
|
|
161
|
+
* @returns Evaluated cell value.
|
|
162
|
+
*/
|
|
163
|
+
evaluateFormula(formula, options = {}) {
|
|
164
|
+
let ast;
|
|
165
|
+
try {
|
|
166
|
+
ast = parseFormula(formula);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
return makeError('#VALUE!', e.message);
|
|
170
|
+
}
|
|
171
|
+
return evaluate(ast, (coord) => this.cells.get(coordKey(coord))?.computed ?? null, options);
|
|
172
|
+
}
|
|
173
|
+
// --- Internals ---
|
|
174
|
+
/**
|
|
175
|
+
* Parse an A1-style reference string into a validated CellCoord.
|
|
176
|
+
*/
|
|
177
|
+
refToCoord(ref) {
|
|
178
|
+
const coord = parseCellReference(ref);
|
|
179
|
+
if (!coord)
|
|
180
|
+
throw new Error(`Invalid cell reference: ${ref}`);
|
|
181
|
+
if (coord.col >= this.config.numCols || coord.row >= this.config.numRows) {
|
|
182
|
+
throw new Error(`Reference out of bounds: ${ref}`);
|
|
183
|
+
}
|
|
184
|
+
return coord;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Write a cell record, rewire dependency edges, and trigger downstream recompute.
|
|
188
|
+
*/
|
|
189
|
+
assignCell(coord, value, ast, formula, options) {
|
|
190
|
+
const key = coordKey(coord);
|
|
191
|
+
const existing = this.cells.get(key);
|
|
192
|
+
if (existing) {
|
|
193
|
+
for (const dep of existing.precedents) {
|
|
194
|
+
this.dependents.get(dep)?.delete(key);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const precedents = new Set();
|
|
198
|
+
if (ast) {
|
|
199
|
+
for (const c of collectReferences(ast)) {
|
|
200
|
+
const k = coordKey(c);
|
|
201
|
+
precedents.add(k);
|
|
202
|
+
let set = this.dependents.get(k);
|
|
203
|
+
if (!set) {
|
|
204
|
+
set = new Set();
|
|
205
|
+
this.dependents.set(k, set);
|
|
206
|
+
}
|
|
207
|
+
set.add(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const record = {
|
|
211
|
+
value,
|
|
212
|
+
ast,
|
|
213
|
+
formula,
|
|
214
|
+
precedents,
|
|
215
|
+
computed: value,
|
|
216
|
+
};
|
|
217
|
+
this.cells.set(key, record);
|
|
218
|
+
if (ast)
|
|
219
|
+
this.evaluateKey(key, options, new Set());
|
|
220
|
+
this.recomputeDownstream(key, options);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Recompute all formula cells that transitively depend on the given key.
|
|
224
|
+
*/
|
|
225
|
+
recomputeDownstream(rootKey, options) {
|
|
226
|
+
// Find every transitive dependent of the changed cell.
|
|
227
|
+
const affected = new Set();
|
|
228
|
+
const stack = [rootKey];
|
|
229
|
+
while (stack.length > 0) {
|
|
230
|
+
const k = stack.pop();
|
|
231
|
+
const outs = this.dependents.get(k);
|
|
232
|
+
if (!outs)
|
|
233
|
+
continue;
|
|
234
|
+
for (const t of outs) {
|
|
235
|
+
if (!affected.has(t)) {
|
|
236
|
+
affected.add(t);
|
|
237
|
+
stack.push(t);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (affected.size === 0)
|
|
242
|
+
return;
|
|
243
|
+
const keys = [...affected];
|
|
244
|
+
const { order, cycle } = topologicalSort(this.dependents, keys);
|
|
245
|
+
for (const k of order)
|
|
246
|
+
this.evaluateKey(k, options, cycle);
|
|
247
|
+
for (const k of cycle) {
|
|
248
|
+
const cell = this.cells.get(k);
|
|
249
|
+
if (!cell)
|
|
250
|
+
continue;
|
|
251
|
+
cell.computed = makeError('#CIRC!');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Evaluate (or propagate) the computed value for a single cell key.
|
|
256
|
+
*/
|
|
257
|
+
evaluateKey(key, options, cycleSet) {
|
|
258
|
+
const cell = this.cells.get(key);
|
|
259
|
+
if (!cell)
|
|
260
|
+
return;
|
|
261
|
+
if (cycleSet.has(key)) {
|
|
262
|
+
cell.computed = makeError('#CIRC!');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!cell.ast) {
|
|
266
|
+
cell.computed = cell.value;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const value = evaluate(cell.ast, (coord) => {
|
|
270
|
+
const k = coordKey(coord);
|
|
271
|
+
if (cycleSet.has(k))
|
|
272
|
+
return makeError('#CIRC!');
|
|
273
|
+
const target = this.cells.get(k);
|
|
274
|
+
return target ? target.computed : null;
|
|
275
|
+
}, options);
|
|
276
|
+
cell.computed = value;
|
|
277
|
+
if (isError(value)) {
|
|
278
|
+
cell.value = value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Reformat a cell coordinate as A1-style text. Re-exported for
|
|
284
|
+
* convenience.
|
|
285
|
+
*
|
|
286
|
+
* @param coord - Cell coordinate.
|
|
287
|
+
* @returns A1-style reference text.
|
|
288
|
+
*/
|
|
289
|
+
export function refOf(coord) {
|
|
290
|
+
return formatCellReference(coord);
|
|
291
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formula tokenizer.
|
|
3
|
+
*
|
|
4
|
+
* Lexes a formula string into a flat token stream consumable by the
|
|
5
|
+
* recursive-descent parser. Pure function — no I/O, no globals.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { FormulaErrorCode } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Lexical token kinds.
|
|
12
|
+
*/
|
|
13
|
+
export type TokenKind = 'number' | 'string' | 'boolean' | 'error' | 'identifier' | 'reference' | 'range' | 'op' | 'lparen' | 'rparen' | 'comma';
|
|
14
|
+
/**
|
|
15
|
+
* Lexical token. `value` is the raw source text; `op` carries the
|
|
16
|
+
* normalized operator string for `kind: 'op'` tokens.
|
|
17
|
+
*/
|
|
18
|
+
export interface Token {
|
|
19
|
+
readonly kind: TokenKind;
|
|
20
|
+
readonly value: string;
|
|
21
|
+
readonly position: number;
|
|
22
|
+
readonly errorCode?: FormulaErrorCode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tokenize a formula string. The leading `=` (if present) is stripped
|
|
26
|
+
* by the caller — pass the *body* of the formula here.
|
|
27
|
+
*
|
|
28
|
+
* @param input - Raw formula text (no leading `=`).
|
|
29
|
+
* @returns Flat token list.
|
|
30
|
+
*/
|
|
31
|
+
export declare function tokenize(input: string): Token[];
|
|
32
|
+
//# sourceMappingURL=tokenizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD;;GAEG;AACH,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,OAAO,GACP,YAAY,GACZ,WAAW,GACX,OAAO,GACP,IAAI,GACJ,QAAQ,GACR,QAAQ,GACR,OAAO,CAAA;AAEX;;;GAGG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAA;CACtC;AAeD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,EAAE,CA2J/C"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formula tokenizer.
|
|
3
|
+
*
|
|
4
|
+
* Lexes a formula string into a flat token stream consumable by the
|
|
5
|
+
* recursive-descent parser. Pure function — no I/O, no globals.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
const ERROR_CODES = [
|
|
10
|
+
'#DIV/0!',
|
|
11
|
+
'#VALUE!',
|
|
12
|
+
'#REF!',
|
|
13
|
+
'#NAME?',
|
|
14
|
+
'#NUM!',
|
|
15
|
+
'#N/A',
|
|
16
|
+
'#CIRC!',
|
|
17
|
+
];
|
|
18
|
+
const TWO_CHAR_OPS = new Set(['<=', '>=', '<>']);
|
|
19
|
+
const SINGLE_OPS = new Set(['+', '-', '*', '/', '^', '&', '=', '<', '>', '%']);
|
|
20
|
+
/**
|
|
21
|
+
* Tokenize a formula string. The leading `=` (if present) is stripped
|
|
22
|
+
* by the caller — pass the *body* of the formula here.
|
|
23
|
+
*
|
|
24
|
+
* @param input - Raw formula text (no leading `=`).
|
|
25
|
+
* @returns Flat token list.
|
|
26
|
+
*/
|
|
27
|
+
export function tokenize(input) {
|
|
28
|
+
const tokens = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < input.length) {
|
|
31
|
+
const ch = input[i];
|
|
32
|
+
// Whitespace
|
|
33
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// String literal
|
|
38
|
+
if (ch === '"') {
|
|
39
|
+
const start = i;
|
|
40
|
+
i++;
|
|
41
|
+
let value = '';
|
|
42
|
+
while (i < input.length) {
|
|
43
|
+
const c = input[i];
|
|
44
|
+
if (c === '"') {
|
|
45
|
+
// Escaped quote ""
|
|
46
|
+
if (input[i + 1] === '"') {
|
|
47
|
+
value += '"';
|
|
48
|
+
i += 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
value += c;
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
if (input[i] !== '"') {
|
|
57
|
+
throw new Error(`Unterminated string literal at position ${start}`);
|
|
58
|
+
}
|
|
59
|
+
i++; // closing quote
|
|
60
|
+
tokens.push({ kind: 'string', value, position: start });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Error literal (`#DIV/0!`, etc.)
|
|
64
|
+
if (ch === '#') {
|
|
65
|
+
const start = i;
|
|
66
|
+
let matched = null;
|
|
67
|
+
for (const code of ERROR_CODES) {
|
|
68
|
+
if (input.startsWith(code, i)) {
|
|
69
|
+
matched = code;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (matched) {
|
|
74
|
+
tokens.push({
|
|
75
|
+
kind: 'error',
|
|
76
|
+
value: matched,
|
|
77
|
+
position: start,
|
|
78
|
+
errorCode: matched,
|
|
79
|
+
});
|
|
80
|
+
i += matched.length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unrecognized error code at position ${start}`);
|
|
84
|
+
}
|
|
85
|
+
// Number literal — integer or decimal, optional exponent.
|
|
86
|
+
if (isDigit(ch) || (ch === '.' && isDigit(input[i + 1] ?? ''))) {
|
|
87
|
+
const start = i;
|
|
88
|
+
while (i < input.length && isDigit(input[i]))
|
|
89
|
+
i++;
|
|
90
|
+
if (input[i] === '.') {
|
|
91
|
+
i++;
|
|
92
|
+
while (i < input.length && isDigit(input[i]))
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
if (input[i] === 'e' || input[i] === 'E') {
|
|
96
|
+
i++;
|
|
97
|
+
if (input[i] === '+' || input[i] === '-')
|
|
98
|
+
i++;
|
|
99
|
+
while (i < input.length && isDigit(input[i]))
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
tokens.push({
|
|
103
|
+
kind: 'number',
|
|
104
|
+
value: input.slice(start, i),
|
|
105
|
+
position: start,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Identifier / reference / range / boolean
|
|
110
|
+
if (isLetter(ch) || ch === '$' || ch === '_') {
|
|
111
|
+
const start = i;
|
|
112
|
+
// Consume an identifier-like blob: letters, digits, $, :, _, .
|
|
113
|
+
while (i < input.length &&
|
|
114
|
+
(isLetter(input[i]) ||
|
|
115
|
+
isDigit(input[i]) ||
|
|
116
|
+
input[i] === '$' ||
|
|
117
|
+
input[i] === '_' ||
|
|
118
|
+
input[i] === '.' ||
|
|
119
|
+
input[i] === ':')) {
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
const text = input.slice(start, i);
|
|
123
|
+
if (/^TRUE$/i.test(text)) {
|
|
124
|
+
tokens.push({ kind: 'boolean', value: 'TRUE', position: start });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (/^FALSE$/i.test(text)) {
|
|
128
|
+
tokens.push({ kind: 'boolean', value: 'FALSE', position: start });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Range like A1:B5
|
|
132
|
+
if (/^\$?[A-Za-z]+\$?\d+:\$?[A-Za-z]+\$?\d+$/.test(text)) {
|
|
133
|
+
tokens.push({ kind: 'range', value: text, position: start });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Single reference like A1, $A$1
|
|
137
|
+
if (/^\$?[A-Za-z]+\$?\d+$/.test(text)) {
|
|
138
|
+
tokens.push({ kind: 'reference', value: text, position: start });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Otherwise treat as function/identifier name
|
|
142
|
+
tokens.push({ kind: 'identifier', value: text, position: start });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Two-char operators
|
|
146
|
+
if (i + 1 < input.length && TWO_CHAR_OPS.has(input.slice(i, i + 2))) {
|
|
147
|
+
tokens.push({ kind: 'op', value: input.slice(i, i + 2), position: i });
|
|
148
|
+
i += 2;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Single-char operators
|
|
152
|
+
if (SINGLE_OPS.has(ch)) {
|
|
153
|
+
tokens.push({ kind: 'op', value: ch, position: i });
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch === '(') {
|
|
158
|
+
tokens.push({ kind: 'lparen', value: '(', position: i });
|
|
159
|
+
i++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === ')') {
|
|
163
|
+
tokens.push({ kind: 'rparen', value: ')', position: i });
|
|
164
|
+
i++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === ',') {
|
|
168
|
+
tokens.push({ kind: 'comma', value: ',', position: i });
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`Unexpected character '${ch}' at position ${i}`);
|
|
173
|
+
}
|
|
174
|
+
return tokens;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Returns true if the character is an ASCII decimal digit (0–9).
|
|
178
|
+
*/
|
|
179
|
+
function isDigit(ch) {
|
|
180
|
+
return ch >= '0' && ch <= '9';
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Returns true if the character is an ASCII letter (A–Z or a–z).
|
|
184
|
+
*/
|
|
185
|
+
function isLetter(ch) {
|
|
186
|
+
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
|
|
187
|
+
}
|