@devmm/puredocs-excel-formula 1.1.1 → 1.1.3
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 +1580 -1569
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +1581 -1570
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import { toOADate, fromOADate, columnLetter, parseCellRef, cellRefFromRowCol, parseRangeRef, shiftIndex, shiftFormulaRefs, shiftSpan
|
|
1
|
+
import { toOADate, EXCEL_MAX_ROWS, columnNumber, EXCEL_MAX_COLUMNS, fromOADate, columnLetter, parseCellRef, cellRefFromRowCol, parseRangeRef, shiftIndex, shiftFormulaRefs, shiftSpan } from '@devmm/puredocs-excel';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
throw TypeError(msg);
|
|
5
|
-
};
|
|
6
|
-
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
7
|
-
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
8
|
-
var __privateAdd = (obj, member, value2) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value2);
|
|
9
|
-
var __privateSet = (obj, member, value2, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value2), value2);
|
|
10
|
-
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
11
|
-
var __privateWrapper = (obj, member, setter, getter) => ({
|
|
12
|
-
set _(value2) {
|
|
13
|
-
__privateSet(obj, member, value2);
|
|
14
|
-
},
|
|
15
|
-
get _() {
|
|
16
|
-
return __privateGet(obj, member, getter);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
3
|
+
// src/formula-value.ts
|
|
19
4
|
var FormulaError = /* @__PURE__ */ ((FormulaError2) => {
|
|
20
5
|
FormulaError2[FormulaError2["None"] = 0] = "None";
|
|
21
6
|
FormulaError2[FormulaError2["Null"] = 1] = "Null";
|
|
@@ -41,19 +26,36 @@ var TailCall = class {
|
|
|
41
26
|
this.lambda = lambda;
|
|
42
27
|
this.args = args;
|
|
43
28
|
}
|
|
29
|
+
lambda;
|
|
30
|
+
args;
|
|
44
31
|
};
|
|
45
|
-
var
|
|
46
|
-
|
|
32
|
+
var FormulaValue = class _FormulaValue {
|
|
33
|
+
kind;
|
|
34
|
+
/** Numeric payload (also used for boolean: 1=true, 0=false). */
|
|
35
|
+
#num;
|
|
36
|
+
/** String, ArrayValue, or LambdaLike payload. */
|
|
37
|
+
#obj;
|
|
38
|
+
errorCode;
|
|
47
39
|
constructor(kind, num5 = 0, obj, errorCode = 0 /* None */) {
|
|
48
|
-
/** Numeric payload (also used for boolean: 1=true, 0=false). */
|
|
49
|
-
__privateAdd(this, _num);
|
|
50
|
-
/** String, ArrayValue, or LambdaLike payload. */
|
|
51
|
-
__privateAdd(this, _obj);
|
|
52
40
|
this.kind = kind;
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
this.#num = num5;
|
|
42
|
+
this.#obj = obj;
|
|
55
43
|
this.errorCode = errorCode;
|
|
56
44
|
}
|
|
45
|
+
// ── Cached singletons ────────────────────────────────────────────────────
|
|
46
|
+
static blank = new _FormulaValue("blank");
|
|
47
|
+
static zero = new _FormulaValue("number", 0);
|
|
48
|
+
static one = new _FormulaValue("number", 1);
|
|
49
|
+
static true_ = new _FormulaValue("boolean", 1);
|
|
50
|
+
static false_ = new _FormulaValue("boolean", 0);
|
|
51
|
+
static emptyString = new _FormulaValue("text", 0, "");
|
|
52
|
+
static errorDiv0 = new _FormulaValue("error", 0, void 0, 2 /* Div0 */);
|
|
53
|
+
static errorValue = new _FormulaValue("error", 0, void 0, 3 /* Value */);
|
|
54
|
+
static errorRef = new _FormulaValue("error", 0, void 0, 4 /* Ref */);
|
|
55
|
+
static errorName = new _FormulaValue("error", 0, void 0, 5 /* Name */);
|
|
56
|
+
static errorNA = new _FormulaValue("error", 0, void 0, 7 /* NA */);
|
|
57
|
+
static errorNum = new _FormulaValue("error", 0, void 0, 6 /* Num */);
|
|
58
|
+
static errorNull = new _FormulaValue("error", 0, void 0, 1 /* Null */);
|
|
57
59
|
// ── Static factories ─────────────────────────────────────────────────────
|
|
58
60
|
static number(v) {
|
|
59
61
|
if (v === 0) return _FormulaValue.zero;
|
|
@@ -120,19 +122,19 @@ var _FormulaValue = class _FormulaValue {
|
|
|
120
122
|
}
|
|
121
123
|
// ── Value accessors ──────────────────────────────────────────────────────
|
|
122
124
|
get numberValue() {
|
|
123
|
-
return
|
|
125
|
+
return this.#num;
|
|
124
126
|
}
|
|
125
127
|
get textValue() {
|
|
126
|
-
return
|
|
128
|
+
return this.#obj ?? "";
|
|
127
129
|
}
|
|
128
130
|
get booleanValue() {
|
|
129
|
-
return
|
|
131
|
+
return this.#num !== 0;
|
|
130
132
|
}
|
|
131
133
|
get arrayVal() {
|
|
132
|
-
return
|
|
134
|
+
return this.#obj ?? ArrayValue.empty;
|
|
133
135
|
}
|
|
134
136
|
get lambdaVal() {
|
|
135
|
-
return
|
|
137
|
+
return this.#obj;
|
|
136
138
|
}
|
|
137
139
|
// ── Coercion (Excel semantics, no exceptions) ─────────────────────────────
|
|
138
140
|
coerceToNumber() {
|
|
@@ -142,7 +144,7 @@ var _FormulaValue = class _FormulaValue {
|
|
|
142
144
|
case "blank":
|
|
143
145
|
return _FormulaValue.zero;
|
|
144
146
|
case "boolean":
|
|
145
|
-
return _FormulaValue.number(
|
|
147
|
+
return _FormulaValue.number(this.#num);
|
|
146
148
|
case "error":
|
|
147
149
|
return this;
|
|
148
150
|
case "text": {
|
|
@@ -156,11 +158,11 @@ var _FormulaValue = class _FormulaValue {
|
|
|
156
158
|
tryAsDouble() {
|
|
157
159
|
switch (this.kind) {
|
|
158
160
|
case "number":
|
|
159
|
-
return { ok: true, value:
|
|
161
|
+
return { ok: true, value: this.#num };
|
|
160
162
|
case "blank":
|
|
161
163
|
return { ok: true, value: 0 };
|
|
162
164
|
case "boolean":
|
|
163
|
-
return { ok: true, value:
|
|
165
|
+
return { ok: true, value: this.#num };
|
|
164
166
|
case "text": {
|
|
165
167
|
const n = parseFloat(this.textValue);
|
|
166
168
|
return isNaN(n) ? { ok: false } : { ok: true, value: n };
|
|
@@ -174,7 +176,7 @@ var _FormulaValue = class _FormulaValue {
|
|
|
174
176
|
case "boolean":
|
|
175
177
|
return this;
|
|
176
178
|
case "number":
|
|
177
|
-
return _FormulaValue.boolean(
|
|
179
|
+
return _FormulaValue.boolean(this.#num !== 0);
|
|
178
180
|
case "blank":
|
|
179
181
|
return _FormulaValue.false_;
|
|
180
182
|
case "error":
|
|
@@ -194,9 +196,9 @@ var _FormulaValue = class _FormulaValue {
|
|
|
194
196
|
case "text":
|
|
195
197
|
return this.textValue;
|
|
196
198
|
case "number":
|
|
197
|
-
return String(
|
|
199
|
+
return String(this.#num);
|
|
198
200
|
case "boolean":
|
|
199
|
-
return
|
|
201
|
+
return this.#num !== 0 ? "TRUE" : "FALSE";
|
|
200
202
|
case "blank":
|
|
201
203
|
return "";
|
|
202
204
|
case "error":
|
|
@@ -220,9 +222,9 @@ var _FormulaValue = class _FormulaValue {
|
|
|
220
222
|
if (a.kind !== b.kind) return false;
|
|
221
223
|
switch (a.kind) {
|
|
222
224
|
case "number":
|
|
223
|
-
return Math.abs(
|
|
225
|
+
return Math.abs(a.#num - b.#num) < 1e-10;
|
|
224
226
|
case "boolean":
|
|
225
|
-
return
|
|
227
|
+
return a.#num === b.#num;
|
|
226
228
|
case "text":
|
|
227
229
|
return a.textValue.toUpperCase() === b.textValue.toUpperCase();
|
|
228
230
|
default:
|
|
@@ -240,7 +242,7 @@ var _FormulaValue = class _FormulaValue {
|
|
|
240
242
|
case 1:
|
|
241
243
|
return a.textValue.toUpperCase() < b.textValue.toUpperCase() ? -1 : a.textValue.toUpperCase() > b.textValue.toUpperCase() ? 1 : 0;
|
|
242
244
|
case 2:
|
|
243
|
-
return
|
|
245
|
+
return a.#num - b.#num;
|
|
244
246
|
default:
|
|
245
247
|
return 0;
|
|
246
248
|
}
|
|
@@ -289,7 +291,7 @@ var _FormulaValue = class _FormulaValue {
|
|
|
289
291
|
toObject() {
|
|
290
292
|
switch (this.kind) {
|
|
291
293
|
case "number":
|
|
292
|
-
return
|
|
294
|
+
return this.#num;
|
|
293
295
|
case "text":
|
|
294
296
|
return this.textValue;
|
|
295
297
|
case "boolean":
|
|
@@ -310,59 +312,44 @@ var _FormulaValue = class _FormulaValue {
|
|
|
310
312
|
return this.asText();
|
|
311
313
|
}
|
|
312
314
|
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
_FormulaValue.one = new _FormulaValue("number", 1);
|
|
319
|
-
_FormulaValue.true_ = new _FormulaValue("boolean", 1);
|
|
320
|
-
_FormulaValue.false_ = new _FormulaValue("boolean", 0);
|
|
321
|
-
_FormulaValue.emptyString = new _FormulaValue("text", 0, "");
|
|
322
|
-
_FormulaValue.errorDiv0 = new _FormulaValue("error", 0, void 0, 2 /* Div0 */);
|
|
323
|
-
_FormulaValue.errorValue = new _FormulaValue("error", 0, void 0, 3 /* Value */);
|
|
324
|
-
_FormulaValue.errorRef = new _FormulaValue("error", 0, void 0, 4 /* Ref */);
|
|
325
|
-
_FormulaValue.errorName = new _FormulaValue("error", 0, void 0, 5 /* Name */);
|
|
326
|
-
_FormulaValue.errorNA = new _FormulaValue("error", 0, void 0, 7 /* NA */);
|
|
327
|
-
_FormulaValue.errorNum = new _FormulaValue("error", 0, void 0, 6 /* Num */);
|
|
328
|
-
_FormulaValue.errorNull = new _FormulaValue("error", 0, void 0, 1 /* Null */);
|
|
329
|
-
var FormulaValue = _FormulaValue;
|
|
330
|
-
var _data;
|
|
331
|
-
var _ArrayValue = class _ArrayValue {
|
|
315
|
+
var ArrayValue = class _ArrayValue {
|
|
316
|
+
static empty = new _ArrayValue(0, 0);
|
|
317
|
+
#data;
|
|
318
|
+
rows;
|
|
319
|
+
columns;
|
|
332
320
|
constructor(rows2, columns2) {
|
|
333
|
-
__privateAdd(this, _data);
|
|
334
321
|
this.rows = rows2;
|
|
335
322
|
this.columns = columns2;
|
|
336
|
-
|
|
323
|
+
this.#data = new Array(rows2 * columns2).fill(FormulaValue.blank);
|
|
337
324
|
}
|
|
338
325
|
get length() {
|
|
339
|
-
return
|
|
326
|
+
return this.#data.length;
|
|
340
327
|
}
|
|
341
328
|
get(row2, col) {
|
|
342
329
|
if (row2 < 0 || row2 >= this.rows || col < 0 || col >= this.columns) {
|
|
343
330
|
return FormulaValue.errorRef;
|
|
344
331
|
}
|
|
345
|
-
return
|
|
332
|
+
return this.#data[row2 * this.columns + col];
|
|
346
333
|
}
|
|
347
334
|
set(row2, col, value2) {
|
|
348
335
|
if (row2 >= 0 && row2 < this.rows && col >= 0 && col < this.columns) {
|
|
349
|
-
|
|
336
|
+
this.#data[row2 * this.columns + col] = value2;
|
|
350
337
|
}
|
|
351
338
|
}
|
|
352
339
|
getFlat(index) {
|
|
353
|
-
return index >= 0 && index <
|
|
340
|
+
return index >= 0 && index < this.#data.length ? this.#data[index] : FormulaValue.errorRef;
|
|
354
341
|
}
|
|
355
342
|
setFlat(index, value2) {
|
|
356
|
-
if (index >= 0 && index <
|
|
357
|
-
|
|
343
|
+
if (index >= 0 && index < this.#data.length) {
|
|
344
|
+
this.#data[index] = value2;
|
|
358
345
|
}
|
|
359
346
|
}
|
|
360
347
|
/** Iterate all values in row-major order. */
|
|
361
348
|
values() {
|
|
362
|
-
return
|
|
349
|
+
return this.#data;
|
|
363
350
|
}
|
|
364
351
|
toObjectArray() {
|
|
365
|
-
return
|
|
352
|
+
return this.#data.map((v) => v.toObject());
|
|
366
353
|
}
|
|
367
354
|
/** Create from a flat array (single column). */
|
|
368
355
|
static fromFlat(values) {
|
|
@@ -420,9 +407,6 @@ var _ArrayValue = class _ArrayValue {
|
|
|
420
407
|
return result;
|
|
421
408
|
}
|
|
422
409
|
};
|
|
423
|
-
_data = new WeakMap();
|
|
424
|
-
_ArrayValue.empty = new _ArrayValue(0, 0);
|
|
425
|
-
var ArrayValue = _ArrayValue;
|
|
426
410
|
|
|
427
411
|
// src/formula-token.ts
|
|
428
412
|
var TokenType = /* @__PURE__ */ ((TokenType2) => {
|
|
@@ -484,262 +468,257 @@ var KNOWN_ERRORS = /* @__PURE__ */ new Set([
|
|
|
484
468
|
"#CALC!",
|
|
485
469
|
"#SPILL!"
|
|
486
470
|
]);
|
|
487
|
-
var _formula, _pos, _FormulaLexer_instances, readNextToken_fn, readString_fn, readErrorLiteral_fn, readQuotedSheetRef_fn, readNumber_fn, readIdentifier_fn, stripReservedPrefixes_fn, readCellRefChars_fn, readOperator_fn, skipWhitespace_fn;
|
|
488
471
|
var FormulaLexer = class {
|
|
472
|
+
#formula;
|
|
473
|
+
#pos = 0;
|
|
489
474
|
constructor(formula) {
|
|
490
|
-
__privateAdd(this, _FormulaLexer_instances);
|
|
491
|
-
__privateAdd(this, _formula);
|
|
492
|
-
__privateAdd(this, _pos, 0);
|
|
493
475
|
if (!formula) throw new FormulaException("Formula cannot be empty.");
|
|
494
|
-
|
|
476
|
+
this.#formula = formula;
|
|
495
477
|
}
|
|
496
478
|
tokenize() {
|
|
497
|
-
|
|
479
|
+
this.#pos = 0;
|
|
498
480
|
const tokens = [];
|
|
499
|
-
while (
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
const token =
|
|
481
|
+
while (this.#pos < this.#formula.length) {
|
|
482
|
+
this.#skipWhitespace();
|
|
483
|
+
if (this.#pos >= this.#formula.length) break;
|
|
484
|
+
const token = this.#readNextToken();
|
|
503
485
|
if (token) tokens.push(token);
|
|
504
486
|
}
|
|
505
|
-
tokens.push(makeToken(31 /* EOF */, "",
|
|
487
|
+
tokens.push(makeToken(31 /* EOF */, "", this.#pos));
|
|
506
488
|
return tokens;
|
|
507
489
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (__privateGet(this, _formula)[__privateGet(this, _pos)] === '"') {
|
|
533
|
-
value2 += '"';
|
|
534
|
-
__privateWrapper(this, _pos)._++;
|
|
535
|
-
} else return makeToken(1 /* String */, value2, start);
|
|
536
|
-
} else {
|
|
537
|
-
value2 += ch;
|
|
538
|
-
__privateWrapper(this, _pos)._++;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
throw new FormulaException(`Unterminated string at position ${start}.`);
|
|
542
|
-
};
|
|
543
|
-
readErrorLiteral_fn = function() {
|
|
544
|
-
const start = __privateGet(this, _pos);
|
|
545
|
-
let raw = "#";
|
|
546
|
-
__privateWrapper(this, _pos)._++;
|
|
547
|
-
while (__privateGet(this, _pos) < __privateGet(this, _formula).length) {
|
|
548
|
-
const ch = __privateGet(this, _formula)[__privateGet(this, _pos)];
|
|
549
|
-
if (/[a-zA-Z0-9/]/.test(ch) || ch === "!" || ch === "?") {
|
|
550
|
-
raw += ch;
|
|
551
|
-
__privateWrapper(this, _pos)._++;
|
|
552
|
-
if (ch === "!" || ch === "?") break;
|
|
553
|
-
} else break;
|
|
554
|
-
}
|
|
555
|
-
const upper2 = raw.toUpperCase();
|
|
556
|
-
if (!KNOWN_ERRORS.has(upper2)) {
|
|
557
|
-
throw new FormulaException(`Unknown error literal "${raw}" at position ${start}.`);
|
|
558
|
-
}
|
|
559
|
-
return makeToken(30 /* ErrorLiteral */, upper2, start);
|
|
560
|
-
};
|
|
561
|
-
readQuotedSheetRef_fn = function() {
|
|
562
|
-
const start = __privateWrapper(this, _pos)._++;
|
|
563
|
-
let sheetName = "";
|
|
564
|
-
while (__privateGet(this, _pos) < __privateGet(this, _formula).length) {
|
|
565
|
-
const ch = __privateGet(this, _formula)[__privateGet(this, _pos)];
|
|
566
|
-
if (ch === "'") {
|
|
567
|
-
__privateWrapper(this, _pos)._++;
|
|
568
|
-
if (__privateGet(this, _formula)[__privateGet(this, _pos)] === "'") {
|
|
569
|
-
sheetName += "'";
|
|
570
|
-
__privateWrapper(this, _pos)._++;
|
|
490
|
+
// ── Private tokenisers ─────────────────────────────────────────────────────
|
|
491
|
+
#readNextToken() {
|
|
492
|
+
const c = this.#formula[this.#pos];
|
|
493
|
+
if (c === '"') return this.#readString();
|
|
494
|
+
if (c === "#") return this.#readErrorLiteral();
|
|
495
|
+
if (c === "'") return this.#readQuotedSheetRef();
|
|
496
|
+
if (c === "." && this.#pos + 1 < this.#formula.length && /\d/.test(this.#formula[this.#pos + 1])) {
|
|
497
|
+
return this.#readNumber();
|
|
498
|
+
}
|
|
499
|
+
if (/\d/.test(c)) return this.#readNumber();
|
|
500
|
+
if (/[a-zA-Z_$]/.test(c)) return this.#readIdentifier();
|
|
501
|
+
return this.#readOperator();
|
|
502
|
+
}
|
|
503
|
+
#readString() {
|
|
504
|
+
const start = this.#pos++;
|
|
505
|
+
let value2 = "";
|
|
506
|
+
while (this.#pos < this.#formula.length) {
|
|
507
|
+
const ch = this.#formula[this.#pos];
|
|
508
|
+
if (ch === '"') {
|
|
509
|
+
this.#pos++;
|
|
510
|
+
if (this.#formula[this.#pos] === '"') {
|
|
511
|
+
value2 += '"';
|
|
512
|
+
this.#pos++;
|
|
513
|
+
} else return makeToken(1 /* String */, value2, start);
|
|
571
514
|
} else {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
__privateWrapper(this, _pos)._++;
|
|
576
|
-
const cellRef = __privateMethod(this, _FormulaLexer_instances, readCellRefChars_fn).call(this);
|
|
577
|
-
return makeToken(22 /* SheetReference */, `${sheetName}!${cellRef}`, start);
|
|
515
|
+
value2 += ch;
|
|
516
|
+
this.#pos++;
|
|
578
517
|
}
|
|
579
|
-
} else {
|
|
580
|
-
sheetName += ch;
|
|
581
|
-
__privateWrapper(this, _pos)._++;
|
|
582
518
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
519
|
+
throw new FormulaException(`Unterminated string at position ${start}.`);
|
|
520
|
+
}
|
|
521
|
+
#readErrorLiteral() {
|
|
522
|
+
const start = this.#pos;
|
|
523
|
+
let raw = "#";
|
|
524
|
+
this.#pos++;
|
|
525
|
+
while (this.#pos < this.#formula.length) {
|
|
526
|
+
const ch = this.#formula[this.#pos];
|
|
527
|
+
if (/[a-zA-Z0-9/]/.test(ch) || ch === "!" || ch === "?") {
|
|
528
|
+
raw += ch;
|
|
529
|
+
this.#pos++;
|
|
530
|
+
if (ch === "!" || ch === "?") break;
|
|
531
|
+
} else break;
|
|
532
|
+
}
|
|
533
|
+
const upper2 = raw.toUpperCase();
|
|
534
|
+
if (!KNOWN_ERRORS.has(upper2)) {
|
|
535
|
+
throw new FormulaException(`Unknown error literal "${raw}" at position ${start}.`);
|
|
536
|
+
}
|
|
537
|
+
return makeToken(30 /* ErrorLiteral */, upper2, start);
|
|
538
|
+
}
|
|
539
|
+
#readQuotedSheetRef() {
|
|
540
|
+
const start = this.#pos++;
|
|
541
|
+
let sheetName = "";
|
|
542
|
+
while (this.#pos < this.#formula.length) {
|
|
543
|
+
const ch = this.#formula[this.#pos];
|
|
544
|
+
if (ch === "'") {
|
|
545
|
+
this.#pos++;
|
|
546
|
+
if (this.#formula[this.#pos] === "'") {
|
|
547
|
+
sheetName += "'";
|
|
548
|
+
this.#pos++;
|
|
549
|
+
} else {
|
|
550
|
+
if (this.#formula[this.#pos] !== "!") {
|
|
551
|
+
throw new FormulaException(`Expected '!' after quoted sheet name at position ${start}.`);
|
|
552
|
+
}
|
|
553
|
+
this.#pos++;
|
|
554
|
+
const cellRef = this.#readCellRefChars();
|
|
555
|
+
return makeToken(22 /* SheetReference */, `${sheetName}!${cellRef}`, start);
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
sheetName += ch;
|
|
559
|
+
this.#pos++;
|
|
606
560
|
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return makeToken(
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
561
|
+
}
|
|
562
|
+
throw new FormulaException(`Unterminated quoted sheet name at position ${start}.`);
|
|
563
|
+
}
|
|
564
|
+
#readNumber() {
|
|
565
|
+
const start = this.#pos;
|
|
566
|
+
let raw = "";
|
|
567
|
+
let hasDot = false;
|
|
568
|
+
let hasE = false;
|
|
569
|
+
while (this.#pos < this.#formula.length) {
|
|
570
|
+
const ch = this.#formula[this.#pos];
|
|
571
|
+
if (/\d/.test(ch)) {
|
|
572
|
+
raw += ch;
|
|
573
|
+
this.#pos++;
|
|
574
|
+
} else if (ch === "." && !hasDot && !hasE) {
|
|
575
|
+
hasDot = true;
|
|
576
|
+
raw += ch;
|
|
577
|
+
this.#pos++;
|
|
578
|
+
} else if ((ch === "e" || ch === "E") && !hasE && raw.length > 0) {
|
|
579
|
+
hasE = true;
|
|
580
|
+
raw += ch;
|
|
581
|
+
this.#pos++;
|
|
582
|
+
if (this.#formula[this.#pos] === "+" || this.#formula[this.#pos] === "-") {
|
|
583
|
+
raw += this.#formula[this.#pos++];
|
|
584
|
+
}
|
|
585
|
+
} else break;
|
|
586
|
+
}
|
|
587
|
+
return makeToken(0 /* Number */, raw, start);
|
|
588
|
+
}
|
|
589
|
+
#readIdentifier() {
|
|
590
|
+
const start = this.#pos;
|
|
591
|
+
let raw = "";
|
|
592
|
+
while (this.#pos < this.#formula.length) {
|
|
593
|
+
const ch = this.#formula[this.#pos];
|
|
594
|
+
if (/[a-zA-Z0-9_$.]/.test(ch)) {
|
|
595
|
+
raw += ch;
|
|
596
|
+
this.#pos++;
|
|
597
|
+
} else break;
|
|
598
|
+
}
|
|
599
|
+
let upper2 = raw.toUpperCase();
|
|
600
|
+
raw = this.#stripReservedPrefixes(raw, upper2);
|
|
601
|
+
if (raw.length !== upper2.length) upper2 = upper2.slice(upper2.length - raw.length);
|
|
602
|
+
if (upper2 === "TRUE") return makeToken(2 /* Boolean */, "TRUE", start);
|
|
603
|
+
if (upper2 === "FALSE") return makeToken(2 /* Boolean */, "FALSE", start);
|
|
604
|
+
if (this.#formula[this.#pos] === "!") {
|
|
605
|
+
this.#pos++;
|
|
606
|
+
const cellRef = this.#readCellRefChars();
|
|
607
|
+
return makeToken(22 /* SheetReference */, `${raw}!${cellRef}`, start);
|
|
608
|
+
}
|
|
609
|
+
this.#skipWhitespace();
|
|
610
|
+
if (this.#formula[this.#pos] === "(") {
|
|
611
|
+
return makeToken(5 /* Function */, upper2, start);
|
|
612
|
+
}
|
|
613
|
+
const stripped = raw.replace(/\$/g, "");
|
|
614
|
+
if (isCellRef(stripped)) {
|
|
615
|
+
if (this.#formula[this.#pos] === "#") {
|
|
616
|
+
this.#pos++;
|
|
617
|
+
return makeToken(24 /* SpillReference */, stripped, start);
|
|
663
618
|
}
|
|
619
|
+
return makeToken(3 /* CellReference */, stripped, start);
|
|
664
620
|
}
|
|
665
|
-
|
|
621
|
+
return makeToken(23 /* NamedRange */, raw, start);
|
|
666
622
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
case ")":
|
|
687
|
-
return makeToken(7 /* RightParen */, ")", start);
|
|
688
|
-
case ",":
|
|
689
|
-
return makeToken(8 /* Comma */, ",", start);
|
|
690
|
-
case ":":
|
|
691
|
-
return makeToken(4 /* Colon */, ":", start);
|
|
692
|
-
case "+":
|
|
693
|
-
return makeToken(9 /* Plus */, "+", start);
|
|
694
|
-
case "-":
|
|
695
|
-
return makeToken(10 /* Minus */, "-", start);
|
|
696
|
-
case "*":
|
|
697
|
-
return makeToken(11 /* Multiply */, "*", start);
|
|
698
|
-
case "/":
|
|
699
|
-
return makeToken(12 /* Divide */, "/", start);
|
|
700
|
-
case "^":
|
|
701
|
-
return makeToken(13 /* Power */, "^", start);
|
|
702
|
-
case "%":
|
|
703
|
-
return makeToken(14 /* Percent */, "%", start);
|
|
704
|
-
case "&":
|
|
705
|
-
return makeToken(15 /* Ampersand */, "&", start);
|
|
706
|
-
case "=":
|
|
707
|
-
return makeToken(16 /* Equal */, "=", start);
|
|
708
|
-
case "@":
|
|
709
|
-
return makeToken(26 /* AtSign */, "@", start);
|
|
710
|
-
case "!":
|
|
711
|
-
return makeToken(25 /* Exclamation */, "!", start);
|
|
712
|
-
case "{":
|
|
713
|
-
return makeToken(27 /* LeftBrace */, "{", start);
|
|
714
|
-
case "}":
|
|
715
|
-
return makeToken(28 /* RightBrace */, "}", start);
|
|
716
|
-
// Inside an array constant ';' separates rows. Excel always stores ',' as
|
|
717
|
-
// the argument separator regardless of locale, so ';' has no other role.
|
|
718
|
-
case ";":
|
|
719
|
-
return makeToken(29 /* Semicolon */, ";", start);
|
|
720
|
-
case "<":
|
|
721
|
-
if (__privateGet(this, _formula)[__privateGet(this, _pos)] === ">") {
|
|
722
|
-
__privateWrapper(this, _pos)._++;
|
|
723
|
-
return makeToken(17 /* NotEqual */, "<>", start);
|
|
724
|
-
}
|
|
725
|
-
if (__privateGet(this, _formula)[__privateGet(this, _pos)] === "=") {
|
|
726
|
-
__privateWrapper(this, _pos)._++;
|
|
727
|
-
return makeToken(19 /* LessThanOrEqual */, "<=", start);
|
|
728
|
-
}
|
|
729
|
-
return makeToken(18 /* LessThan */, "<", start);
|
|
730
|
-
case ">":
|
|
731
|
-
if (__privateGet(this, _formula)[__privateGet(this, _pos)] === "=") {
|
|
732
|
-
__privateWrapper(this, _pos)._++;
|
|
733
|
-
return makeToken(21 /* GreaterThanOrEqual */, ">=", start);
|
|
623
|
+
/**
|
|
624
|
+
* Drops Excel's storage-level name decorations. They carry no semantics — the
|
|
625
|
+
* writer stamps them so an older reader fails loudly instead of silently
|
|
626
|
+
* mis-evaluating — and they *stack*: `_xlfn._xlws.FILTER` wears two. Stripping
|
|
627
|
+
* one prefix once, as this used to, leaves `_XLWS.FILTER` unresolvable.
|
|
628
|
+
*
|
|
629
|
+
* Every identifier in every formula passes through here, so the common case is
|
|
630
|
+
* gated on a single char-code test: a name not starting with '_' returns
|
|
631
|
+
* untouched without so much as a comparison against the prefix table.
|
|
632
|
+
*/
|
|
633
|
+
#stripReservedPrefixes(raw, upper2) {
|
|
634
|
+
if (raw.charCodeAt(0) !== UNDERSCORE) return raw;
|
|
635
|
+
let cut = 0;
|
|
636
|
+
outer: for (; ; ) {
|
|
637
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
638
|
+
if (upper2.startsWith(prefix, cut)) {
|
|
639
|
+
cut += prefix.length;
|
|
640
|
+
continue outer;
|
|
641
|
+
}
|
|
734
642
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
return cut > 0 && cut < raw.length ? raw.slice(cut) : raw;
|
|
646
|
+
}
|
|
647
|
+
#readCellRefChars() {
|
|
648
|
+
let ref = "";
|
|
649
|
+
while (this.#pos < this.#formula.length) {
|
|
650
|
+
const ch = this.#formula[this.#pos];
|
|
651
|
+
if (/[a-zA-Z0-9$]/.test(ch)) {
|
|
652
|
+
ref += ch;
|
|
653
|
+
this.#pos++;
|
|
654
|
+
} else break;
|
|
655
|
+
}
|
|
656
|
+
return ref.replace(/\$/g, "");
|
|
657
|
+
}
|
|
658
|
+
#readOperator() {
|
|
659
|
+
const start = this.#pos;
|
|
660
|
+
const ch = this.#formula[this.#pos++];
|
|
661
|
+
switch (ch) {
|
|
662
|
+
case "(":
|
|
663
|
+
return makeToken(6 /* LeftParen */, "(", start);
|
|
664
|
+
case ")":
|
|
665
|
+
return makeToken(7 /* RightParen */, ")", start);
|
|
666
|
+
case ",":
|
|
667
|
+
return makeToken(8 /* Comma */, ",", start);
|
|
668
|
+
case ":":
|
|
669
|
+
return makeToken(4 /* Colon */, ":", start);
|
|
670
|
+
case "+":
|
|
671
|
+
return makeToken(9 /* Plus */, "+", start);
|
|
672
|
+
case "-":
|
|
673
|
+
return makeToken(10 /* Minus */, "-", start);
|
|
674
|
+
case "*":
|
|
675
|
+
return makeToken(11 /* Multiply */, "*", start);
|
|
676
|
+
case "/":
|
|
677
|
+
return makeToken(12 /* Divide */, "/", start);
|
|
678
|
+
case "^":
|
|
679
|
+
return makeToken(13 /* Power */, "^", start);
|
|
680
|
+
case "%":
|
|
681
|
+
return makeToken(14 /* Percent */, "%", start);
|
|
682
|
+
case "&":
|
|
683
|
+
return makeToken(15 /* Ampersand */, "&", start);
|
|
684
|
+
case "=":
|
|
685
|
+
return makeToken(16 /* Equal */, "=", start);
|
|
686
|
+
case "@":
|
|
687
|
+
return makeToken(26 /* AtSign */, "@", start);
|
|
688
|
+
case "!":
|
|
689
|
+
return makeToken(25 /* Exclamation */, "!", start);
|
|
690
|
+
case "{":
|
|
691
|
+
return makeToken(27 /* LeftBrace */, "{", start);
|
|
692
|
+
case "}":
|
|
693
|
+
return makeToken(28 /* RightBrace */, "}", start);
|
|
694
|
+
// Inside an array constant ';' separates rows. Excel always stores ',' as
|
|
695
|
+
// the argument separator regardless of locale, so ';' has no other role.
|
|
696
|
+
case ";":
|
|
697
|
+
return makeToken(29 /* Semicolon */, ";", start);
|
|
698
|
+
case "<":
|
|
699
|
+
if (this.#formula[this.#pos] === ">") {
|
|
700
|
+
this.#pos++;
|
|
701
|
+
return makeToken(17 /* NotEqual */, "<>", start);
|
|
702
|
+
}
|
|
703
|
+
if (this.#formula[this.#pos] === "=") {
|
|
704
|
+
this.#pos++;
|
|
705
|
+
return makeToken(19 /* LessThanOrEqual */, "<=", start);
|
|
706
|
+
}
|
|
707
|
+
return makeToken(18 /* LessThan */, "<", start);
|
|
708
|
+
case ">":
|
|
709
|
+
if (this.#formula[this.#pos] === "=") {
|
|
710
|
+
this.#pos++;
|
|
711
|
+
return makeToken(21 /* GreaterThanOrEqual */, ">=", start);
|
|
712
|
+
}
|
|
713
|
+
return makeToken(20 /* GreaterThan */, ">", start);
|
|
714
|
+
default:
|
|
715
|
+
throw new FormulaException(`Unexpected character '${ch}' at position ${start}.`);
|
|
716
|
+
}
|
|
738
717
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
718
|
+
#skipWhitespace() {
|
|
719
|
+
while (this.#pos < this.#formula.length && /\s/.test(this.#formula[this.#pos])) {
|
|
720
|
+
this.#pos++;
|
|
721
|
+
}
|
|
743
722
|
}
|
|
744
723
|
};
|
|
745
724
|
function isCellRef(s) {
|
|
@@ -768,6 +747,7 @@ var NumberNode = class extends FormulaNode {
|
|
|
768
747
|
super();
|
|
769
748
|
this.value = value2;
|
|
770
749
|
}
|
|
750
|
+
value;
|
|
771
751
|
evaluate() {
|
|
772
752
|
return FormulaValue.number(this.value);
|
|
773
753
|
}
|
|
@@ -777,6 +757,7 @@ var StringNode = class extends FormulaNode {
|
|
|
777
757
|
super();
|
|
778
758
|
this.value = value2;
|
|
779
759
|
}
|
|
760
|
+
value;
|
|
780
761
|
evaluate() {
|
|
781
762
|
return FormulaValue.text(this.value);
|
|
782
763
|
}
|
|
@@ -786,6 +767,7 @@ var BooleanNode = class extends FormulaNode {
|
|
|
786
767
|
super();
|
|
787
768
|
this.value = value2;
|
|
788
769
|
}
|
|
770
|
+
value;
|
|
789
771
|
evaluate() {
|
|
790
772
|
return FormulaValue.boolean(this.value);
|
|
791
773
|
}
|
|
@@ -795,29 +777,30 @@ var ErrorNode = class extends FormulaNode {
|
|
|
795
777
|
super();
|
|
796
778
|
this.errorValue = errorValue;
|
|
797
779
|
}
|
|
780
|
+
errorValue;
|
|
798
781
|
evaluate() {
|
|
799
782
|
return this.errorValue;
|
|
800
783
|
}
|
|
801
784
|
};
|
|
802
|
-
var _value;
|
|
803
785
|
var ArrayLiteralNode = class extends FormulaNode {
|
|
804
786
|
constructor(array) {
|
|
805
787
|
super();
|
|
806
788
|
this.array = array;
|
|
807
|
-
|
|
808
|
-
__privateSet(this, _value, FormulaValue.array(array));
|
|
789
|
+
this.#value = FormulaValue.array(array);
|
|
809
790
|
}
|
|
791
|
+
array;
|
|
792
|
+
#value;
|
|
810
793
|
evaluate() {
|
|
811
|
-
return
|
|
794
|
+
return this.#value;
|
|
812
795
|
}
|
|
813
796
|
};
|
|
814
|
-
_value = new WeakMap();
|
|
815
797
|
var ValueNode = class extends FormulaNode {
|
|
816
798
|
/** Mutable so one wrapper can be re-pointed across a lifted call's cells. */
|
|
817
799
|
constructor(value2) {
|
|
818
800
|
super();
|
|
819
801
|
this.value = value2;
|
|
820
802
|
}
|
|
803
|
+
value;
|
|
821
804
|
evaluate() {
|
|
822
805
|
return this.value;
|
|
823
806
|
}
|
|
@@ -832,6 +815,7 @@ var CellReferenceNode = class extends FormulaNode {
|
|
|
832
815
|
super();
|
|
833
816
|
this.reference = reference;
|
|
834
817
|
}
|
|
818
|
+
reference;
|
|
835
819
|
evaluate(ctx) {
|
|
836
820
|
return ctx.getCellValue(this.reference);
|
|
837
821
|
}
|
|
@@ -842,6 +826,8 @@ var RangeReferenceNode = class extends FormulaNode {
|
|
|
842
826
|
this.startRef = startRef;
|
|
843
827
|
this.endRef = endRef;
|
|
844
828
|
}
|
|
829
|
+
startRef;
|
|
830
|
+
endRef;
|
|
845
831
|
evaluate(ctx) {
|
|
846
832
|
return ctx.getRangeValues(this.startRef, this.endRef);
|
|
847
833
|
}
|
|
@@ -852,6 +838,8 @@ var SheetCellReferenceNode = class extends FormulaNode {
|
|
|
852
838
|
this.sheetName = sheetName;
|
|
853
839
|
this.cellReference = cellReference;
|
|
854
840
|
}
|
|
841
|
+
sheetName;
|
|
842
|
+
cellReference;
|
|
855
843
|
evaluate(ctx) {
|
|
856
844
|
return ctx.getSheetCellValue(this.sheetName, this.cellReference);
|
|
857
845
|
}
|
|
@@ -863,6 +851,9 @@ var SheetRangeReferenceNode = class extends FormulaNode {
|
|
|
863
851
|
this.startRef = startRef;
|
|
864
852
|
this.endRef = endRef;
|
|
865
853
|
}
|
|
854
|
+
sheetName;
|
|
855
|
+
startRef;
|
|
856
|
+
endRef;
|
|
866
857
|
evaluate(ctx) {
|
|
867
858
|
return ctx.getSheetRangeValues(this.sheetName, this.startRef, this.endRef);
|
|
868
859
|
}
|
|
@@ -876,6 +867,11 @@ var FullRangeReferenceNode = class extends FormulaNode {
|
|
|
876
867
|
this.endRow = endRow;
|
|
877
868
|
this.sheetName = sheetName;
|
|
878
869
|
}
|
|
870
|
+
startCol;
|
|
871
|
+
endCol;
|
|
872
|
+
startRow;
|
|
873
|
+
endRow;
|
|
874
|
+
sheetName;
|
|
879
875
|
evaluate(ctx) {
|
|
880
876
|
return this.sheetName === void 0 ? ctx.getFullRangeValues(this.startCol, this.endCol, this.startRow, this.endRow) : ctx.getSheetFullRangeValues(this.sheetName, this.startCol, this.endCol, this.startRow, this.endRow);
|
|
881
877
|
}
|
|
@@ -885,6 +881,7 @@ var NamedRangeNode = class extends FormulaNode {
|
|
|
885
881
|
super();
|
|
886
882
|
this.name = name;
|
|
887
883
|
}
|
|
884
|
+
name;
|
|
888
885
|
evaluate(ctx) {
|
|
889
886
|
return ctx.resolveNamedRange(this.name);
|
|
890
887
|
}
|
|
@@ -894,6 +891,7 @@ var SpillReferenceNode = class extends FormulaNode {
|
|
|
894
891
|
super();
|
|
895
892
|
this.anchorRef = anchorRef;
|
|
896
893
|
}
|
|
894
|
+
anchorRef;
|
|
897
895
|
evaluate(ctx) {
|
|
898
896
|
return ctx.resolveSpillRange(this.anchorRef);
|
|
899
897
|
}
|
|
@@ -919,15 +917,16 @@ var UnaryOperator = /* @__PURE__ */ ((UnaryOperator2) => {
|
|
|
919
917
|
UnaryOperator2[UnaryOperator2["Percent"] = 2] = "Percent";
|
|
920
918
|
return UnaryOperator2;
|
|
921
919
|
})(UnaryOperator || {});
|
|
922
|
-
var _BinaryOpNode_instances, evalScalar_fn;
|
|
923
920
|
var BinaryOpNode = class extends FormulaNode {
|
|
924
921
|
constructor(left2, op, right2) {
|
|
925
922
|
super();
|
|
926
923
|
this.left = left2;
|
|
927
924
|
this.op = op;
|
|
928
925
|
this.right = right2;
|
|
929
|
-
__privateAdd(this, _BinaryOpNode_instances);
|
|
930
926
|
}
|
|
927
|
+
left;
|
|
928
|
+
op;
|
|
929
|
+
right;
|
|
931
930
|
evaluate(ctx) {
|
|
932
931
|
const l = this.left.evaluate(ctx);
|
|
933
932
|
const r = this.right.evaluate(ctx);
|
|
@@ -935,46 +934,45 @@ var BinaryOpNode = class extends FormulaNode {
|
|
|
935
934
|
const la = l.isArray ? l.arrayVal : ArrayValue.fromScalar(l);
|
|
936
935
|
const ra = r.isArray ? r.arrayVal : ArrayValue.fromScalar(r);
|
|
937
936
|
return FormulaValue.array(
|
|
938
|
-
ArrayValue.broadcast(la, ra, (a, b) =>
|
|
937
|
+
ArrayValue.broadcast(la, ra, (a, b) => this.#evalScalar(a, b))
|
|
939
938
|
);
|
|
940
939
|
}
|
|
941
|
-
return
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
return FormulaValue.errorValue;
|
|
940
|
+
return this.#evalScalar(l, r);
|
|
941
|
+
}
|
|
942
|
+
#evalScalar(l, r) {
|
|
943
|
+
const op = this.op;
|
|
944
|
+
if (op === 6 /* Equal */) return FormulaValue.boolean(FormulaValue.areEqual(l, r));
|
|
945
|
+
if (op === 7 /* NotEqual */) return FormulaValue.boolean(!FormulaValue.areEqual(l, r));
|
|
946
|
+
if (op === 8 /* LessThan */) return FormulaValue.boolean(FormulaValue.compare(l, r) < 0);
|
|
947
|
+
if (op === 9 /* LessThanOrEqual */) return FormulaValue.boolean(FormulaValue.compare(l, r) <= 0);
|
|
948
|
+
if (op === 10 /* GreaterThan */) return FormulaValue.boolean(FormulaValue.compare(l, r) > 0);
|
|
949
|
+
if (op === 11 /* GreaterThanOrEqual */) return FormulaValue.boolean(FormulaValue.compare(l, r) >= 0);
|
|
950
|
+
if (op === 5 /* Concatenate */) {
|
|
951
|
+
if (l.isError) return l;
|
|
952
|
+
if (r.isError) return r;
|
|
953
|
+
return FormulaValue.text(l.asText() + r.asText());
|
|
954
|
+
}
|
|
955
|
+
const ln2 = l.coerceToNumber();
|
|
956
|
+
if (ln2.isError) return ln2;
|
|
957
|
+
const rn = r.coerceToNumber();
|
|
958
|
+
if (rn.isError) return rn;
|
|
959
|
+
const lv = ln2.numberValue;
|
|
960
|
+
const rv = rn.numberValue;
|
|
961
|
+
switch (op) {
|
|
962
|
+
case 0 /* Add */:
|
|
963
|
+
return FormulaValue.number(lv + rv);
|
|
964
|
+
case 1 /* Subtract */:
|
|
965
|
+
return FormulaValue.number(lv - rv);
|
|
966
|
+
case 2 /* Multiply */:
|
|
967
|
+
return FormulaValue.number(lv * rv);
|
|
968
|
+
case 3 /* Divide */:
|
|
969
|
+
if (rv === 0) return FormulaValue.errorDiv0;
|
|
970
|
+
return FormulaValue.number(lv / rv);
|
|
971
|
+
case 4 /* Power */:
|
|
972
|
+
return FormulaValue.number(Math.pow(lv, rv));
|
|
973
|
+
default:
|
|
974
|
+
return FormulaValue.errorValue;
|
|
975
|
+
}
|
|
978
976
|
}
|
|
979
977
|
};
|
|
980
978
|
var UnaryOpNode = class extends FormulaNode {
|
|
@@ -983,6 +981,8 @@ var UnaryOpNode = class extends FormulaNode {
|
|
|
983
981
|
this.op = op;
|
|
984
982
|
this.operand = operand;
|
|
985
983
|
}
|
|
984
|
+
op;
|
|
985
|
+
operand;
|
|
986
986
|
evaluate(ctx) {
|
|
987
987
|
const val = this.operand.evaluate(ctx);
|
|
988
988
|
if (this.op === 1 /* Plus */) {
|
|
@@ -1006,6 +1006,7 @@ var ImplicitIntersectionNode = class extends FormulaNode {
|
|
|
1006
1006
|
super();
|
|
1007
1007
|
this.inner = inner;
|
|
1008
1008
|
}
|
|
1009
|
+
inner;
|
|
1009
1010
|
evaluate(ctx) {
|
|
1010
1011
|
const val = this.inner.evaluate(ctx);
|
|
1011
1012
|
if (!val.isArray) return val;
|
|
@@ -1018,6 +1019,8 @@ var FunctionCallNode = class extends FormulaNode {
|
|
|
1018
1019
|
this.functionName = functionName;
|
|
1019
1020
|
this.args = args;
|
|
1020
1021
|
}
|
|
1022
|
+
functionName;
|
|
1023
|
+
args;
|
|
1021
1024
|
evaluate(ctx) {
|
|
1022
1025
|
return ctx.evaluateFunction(this.functionName, this.args);
|
|
1023
1026
|
}
|
|
@@ -1084,6 +1087,8 @@ var CallNode = class extends FormulaNode {
|
|
|
1084
1087
|
this.callee = callee;
|
|
1085
1088
|
this.args = args;
|
|
1086
1089
|
}
|
|
1090
|
+
callee;
|
|
1091
|
+
args;
|
|
1087
1092
|
evaluate(ctx) {
|
|
1088
1093
|
const fn = this.callee.evaluate(ctx);
|
|
1089
1094
|
if (fn.isError) return fn;
|
|
@@ -1099,317 +1104,313 @@ var CallNode = class extends FormulaNode {
|
|
|
1099
1104
|
};
|
|
1100
1105
|
|
|
1101
1106
|
// src/formula-parser.ts
|
|
1102
|
-
var _tokens, _pos2, _FormulaParser_instances, current_get, advance_fn, expect_fn, parseComparison_fn, parseConcatenation_fn, parseAddSub_fn, parseMulDiv_fn, parseUnary_fn, parsePower_fn, parsePercent_fn, parsePostfix_fn, parsePrimary_fn, tryParseFullRange_fn, parseArrayLiteral_fn, parseArrayElement_fn, parseFunction_fn, parseArgList_fn;
|
|
1103
1107
|
var FormulaParser = class {
|
|
1108
|
+
#tokens;
|
|
1109
|
+
#pos = 0;
|
|
1104
1110
|
constructor(tokens) {
|
|
1105
|
-
|
|
1106
|
-
__privateAdd(this, _tokens);
|
|
1107
|
-
__privateAdd(this, _pos2, 0);
|
|
1108
|
-
__privateSet(this, _tokens, tokens);
|
|
1111
|
+
this.#tokens = tokens;
|
|
1109
1112
|
}
|
|
1110
1113
|
parse() {
|
|
1111
|
-
|
|
1112
|
-
const node =
|
|
1113
|
-
if (
|
|
1114
|
+
this.#pos = 0;
|
|
1115
|
+
const node = this.#parseComparison();
|
|
1116
|
+
if (this.#current.type !== 31 /* EOF */) {
|
|
1114
1117
|
throw new FormulaException(
|
|
1115
|
-
`Unexpected token '${
|
|
1118
|
+
`Unexpected token '${this.#current.value}' at position ${this.#current.position}.`
|
|
1116
1119
|
);
|
|
1117
1120
|
}
|
|
1118
1121
|
return node;
|
|
1119
1122
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
_FormulaParser_instances = new WeakSet();
|
|
1124
|
-
current_get = function() {
|
|
1125
|
-
return __privateGet(this, _tokens)[__privateGet(this, _pos2)] ?? { type: 31 /* EOF */, value: "", position: -1 };
|
|
1126
|
-
};
|
|
1127
|
-
advance_fn = function() {
|
|
1128
|
-
const t = __privateGet(this, _FormulaParser_instances, current_get);
|
|
1129
|
-
__privateWrapper(this, _pos2)._++;
|
|
1130
|
-
return t;
|
|
1131
|
-
};
|
|
1132
|
-
expect_fn = function(type) {
|
|
1133
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type !== type) {
|
|
1134
|
-
throw new FormulaException(
|
|
1135
|
-
`Expected token type ${type} but got ${__privateGet(this, _FormulaParser_instances, current_get).type} ('${__privateGet(this, _FormulaParser_instances, current_get).value}') at position ${__privateGet(this, _FormulaParser_instances, current_get).position}.`
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
return __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1139
|
-
};
|
|
1140
|
-
// ── Precedence levels ──────────────────────────────────────────────────────
|
|
1141
|
-
parseComparison_fn = function() {
|
|
1142
|
-
let left2 = __privateMethod(this, _FormulaParser_instances, parseConcatenation_fn).call(this);
|
|
1143
|
-
while (true) {
|
|
1144
|
-
const op = comparisonOp(__privateGet(this, _FormulaParser_instances, current_get).type);
|
|
1145
|
-
if (op === null) break;
|
|
1146
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1147
|
-
left2 = new BinaryOpNode(left2, op, __privateMethod(this, _FormulaParser_instances, parseConcatenation_fn).call(this));
|
|
1148
|
-
}
|
|
1149
|
-
return left2;
|
|
1150
|
-
};
|
|
1151
|
-
parseConcatenation_fn = function() {
|
|
1152
|
-
let left2 = __privateMethod(this, _FormulaParser_instances, parseAddSub_fn).call(this);
|
|
1153
|
-
while (__privateGet(this, _FormulaParser_instances, current_get).type === 15 /* Ampersand */) {
|
|
1154
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1155
|
-
left2 = new BinaryOpNode(left2, 5 /* Concatenate */, __privateMethod(this, _FormulaParser_instances, parseAddSub_fn).call(this));
|
|
1156
|
-
}
|
|
1157
|
-
return left2;
|
|
1158
|
-
};
|
|
1159
|
-
parseAddSub_fn = function() {
|
|
1160
|
-
let left2 = __privateMethod(this, _FormulaParser_instances, parseMulDiv_fn).call(this);
|
|
1161
|
-
while (true) {
|
|
1162
|
-
const t = __privateGet(this, _FormulaParser_instances, current_get).type;
|
|
1163
|
-
if (t !== 9 /* Plus */ && t !== 10 /* Minus */) break;
|
|
1164
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1165
|
-
const op = t === 9 /* Plus */ ? 0 /* Add */ : 1 /* Subtract */;
|
|
1166
|
-
left2 = new BinaryOpNode(left2, op, __privateMethod(this, _FormulaParser_instances, parseMulDiv_fn).call(this));
|
|
1167
|
-
}
|
|
1168
|
-
return left2;
|
|
1169
|
-
};
|
|
1170
|
-
parseMulDiv_fn = function() {
|
|
1171
|
-
let left2 = __privateMethod(this, _FormulaParser_instances, parseUnary_fn).call(this);
|
|
1172
|
-
while (true) {
|
|
1173
|
-
const t = __privateGet(this, _FormulaParser_instances, current_get).type;
|
|
1174
|
-
if (t !== 11 /* Multiply */ && t !== 12 /* Divide */) break;
|
|
1175
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1176
|
-
const op = t === 11 /* Multiply */ ? 2 /* Multiply */ : 3 /* Divide */;
|
|
1177
|
-
left2 = new BinaryOpNode(left2, op, __privateMethod(this, _FormulaParser_instances, parseUnary_fn).call(this));
|
|
1178
|
-
}
|
|
1179
|
-
return left2;
|
|
1180
|
-
};
|
|
1181
|
-
parseUnary_fn = function() {
|
|
1182
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 10 /* Minus */) {
|
|
1183
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1184
|
-
return new UnaryOpNode(0 /* Negate */, __privateMethod(this, _FormulaParser_instances, parsePower_fn).call(this));
|
|
1123
|
+
// ── Token helpers ──────────────────────────────────────────────────────────
|
|
1124
|
+
get #current() {
|
|
1125
|
+
return this.#tokens[this.#pos] ?? { type: 31 /* EOF */, value: "", position: -1 };
|
|
1185
1126
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1127
|
+
#advance() {
|
|
1128
|
+
const t = this.#current;
|
|
1129
|
+
this.#pos++;
|
|
1130
|
+
return t;
|
|
1189
1131
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1132
|
+
#expect(type) {
|
|
1133
|
+
if (this.#current.type !== type) {
|
|
1134
|
+
throw new FormulaException(
|
|
1135
|
+
`Expected token type ${type} but got ${this.#current.type} ('${this.#current.value}') at position ${this.#current.position}.`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
return this.#advance();
|
|
1139
|
+
}
|
|
1140
|
+
// ── Precedence levels ──────────────────────────────────────────────────────
|
|
1141
|
+
#parseComparison() {
|
|
1142
|
+
let left2 = this.#parseConcatenation();
|
|
1143
|
+
while (true) {
|
|
1144
|
+
const op = comparisonOp(this.#current.type);
|
|
1145
|
+
if (op === null) break;
|
|
1146
|
+
this.#advance();
|
|
1147
|
+
left2 = new BinaryOpNode(left2, op, this.#parseConcatenation());
|
|
1148
|
+
}
|
|
1149
|
+
return left2;
|
|
1150
|
+
}
|
|
1151
|
+
#parseConcatenation() {
|
|
1152
|
+
let left2 = this.#parseAddSub();
|
|
1153
|
+
while (this.#current.type === 15 /* Ampersand */) {
|
|
1154
|
+
this.#advance();
|
|
1155
|
+
left2 = new BinaryOpNode(left2, 5 /* Concatenate */, this.#parseAddSub());
|
|
1156
|
+
}
|
|
1157
|
+
return left2;
|
|
1158
|
+
}
|
|
1159
|
+
#parseAddSub() {
|
|
1160
|
+
let left2 = this.#parseMulDiv();
|
|
1161
|
+
while (true) {
|
|
1162
|
+
const t = this.#current.type;
|
|
1163
|
+
if (t !== 9 /* Plus */ && t !== 10 /* Minus */) break;
|
|
1164
|
+
this.#advance();
|
|
1165
|
+
const op = t === 9 /* Plus */ ? 0 /* Add */ : 1 /* Subtract */;
|
|
1166
|
+
left2 = new BinaryOpNode(left2, op, this.#parseMulDiv());
|
|
1167
|
+
}
|
|
1168
|
+
return left2;
|
|
1169
|
+
}
|
|
1170
|
+
#parseMulDiv() {
|
|
1171
|
+
let left2 = this.#parseUnary();
|
|
1172
|
+
while (true) {
|
|
1173
|
+
const t = this.#current.type;
|
|
1174
|
+
if (t !== 11 /* Multiply */ && t !== 12 /* Divide */) break;
|
|
1175
|
+
this.#advance();
|
|
1176
|
+
const op = t === 11 /* Multiply */ ? 2 /* Multiply */ : 3 /* Divide */;
|
|
1177
|
+
left2 = new BinaryOpNode(left2, op, this.#parseUnary());
|
|
1178
|
+
}
|
|
1179
|
+
return left2;
|
|
1180
|
+
}
|
|
1181
|
+
#parseUnary() {
|
|
1182
|
+
if (this.#current.type === 10 /* Minus */) {
|
|
1183
|
+
this.#advance();
|
|
1184
|
+
return new UnaryOpNode(0 /* Negate */, this.#parsePower());
|
|
1185
|
+
}
|
|
1186
|
+
if (this.#current.type === 9 /* Plus */) {
|
|
1187
|
+
this.#advance();
|
|
1188
|
+
return new UnaryOpNode(1 /* Plus */, this.#parsePower());
|
|
1189
|
+
}
|
|
1190
|
+
if (this.#current.type === 26 /* AtSign */) {
|
|
1191
|
+
this.#advance();
|
|
1192
|
+
return new ImplicitIntersectionNode(this.#parsePower());
|
|
1193
|
+
}
|
|
1194
|
+
return this.#parsePower();
|
|
1195
|
+
}
|
|
1196
|
+
#parsePower() {
|
|
1197
|
+
const left2 = this.#parsePercent();
|
|
1198
|
+
if (this.#current.type === 13 /* Power */) {
|
|
1199
|
+
this.#advance();
|
|
1200
|
+
return new BinaryOpNode(left2, 4 /* Power */, this.#parseUnary());
|
|
1201
|
+
}
|
|
1202
|
+
return left2;
|
|
1203
|
+
}
|
|
1204
|
+
#parsePercent() {
|
|
1205
|
+
let node = this.#parsePostfix();
|
|
1206
|
+
while (this.#current.type === 14 /* Percent */) {
|
|
1207
|
+
this.#advance();
|
|
1208
|
+
node = new UnaryOpNode(2 /* Percent */, node);
|
|
1209
|
+
}
|
|
1210
|
+
return node;
|
|
1201
1211
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1212
|
+
/** Handles postfix call syntax on an expression, e.g. LAMBDA(x,x+1)(5). */
|
|
1213
|
+
#parsePostfix() {
|
|
1214
|
+
let node = this.#parsePrimary();
|
|
1215
|
+
while (this.#current.type === 6 /* LeftParen */) {
|
|
1216
|
+
this.#advance();
|
|
1217
|
+
node = new CallNode(node, this.#parseArgList());
|
|
1218
|
+
}
|
|
1219
|
+
return node;
|
|
1209
1220
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1221
|
+
#parsePrimary() {
|
|
1222
|
+
const token = this.#current;
|
|
1223
|
+
switch (token.type) {
|
|
1224
|
+
case 0 /* Number */: {
|
|
1225
|
+
this.#advance();
|
|
1226
|
+
if (this.#current.type === 4 /* Colon */) {
|
|
1227
|
+
const full = this.#tryParseFullRange(token.value);
|
|
1228
|
+
if (full) return full;
|
|
1229
|
+
}
|
|
1230
|
+
const n = parseFloat(token.value);
|
|
1231
|
+
if (isNaN(n)) throw new FormulaException(`Invalid number: ${token.value}`);
|
|
1232
|
+
return new NumberNode(n);
|
|
1233
|
+
}
|
|
1234
|
+
case 1 /* String */:
|
|
1235
|
+
this.#advance();
|
|
1236
|
+
return new StringNode(token.value);
|
|
1237
|
+
case 2 /* Boolean */:
|
|
1238
|
+
this.#advance();
|
|
1239
|
+
return new BooleanNode(token.value === "TRUE");
|
|
1240
|
+
case 30 /* ErrorLiteral */:
|
|
1241
|
+
this.#advance();
|
|
1242
|
+
return new ErrorNode(FormulaValue.error(FormulaValue.errorFromString(token.value)));
|
|
1243
|
+
case 3 /* CellReference */: {
|
|
1244
|
+
this.#advance();
|
|
1245
|
+
if (this.#current.type === 4 /* Colon */) {
|
|
1246
|
+
this.#advance();
|
|
1247
|
+
const end = this.#expect(3 /* CellReference */);
|
|
1248
|
+
return new RangeReferenceNode(token.value, end.value);
|
|
1249
|
+
}
|
|
1250
|
+
return new CellReferenceNode(token.value);
|
|
1251
|
+
}
|
|
1252
|
+
case 22 /* SheetReference */: {
|
|
1253
|
+
this.#advance();
|
|
1254
|
+
const parts = token.value.split("!");
|
|
1255
|
+
const sheetName = parts[0];
|
|
1256
|
+
const cellRef = parts[1] ?? "";
|
|
1257
|
+
if (this.#current.type === 4 /* Colon */) {
|
|
1258
|
+
const full = this.#tryParseFullRange(cellRef, sheetName);
|
|
1259
|
+
if (full) return full;
|
|
1260
|
+
this.#advance();
|
|
1261
|
+
const end = this.#expect(3 /* CellReference */);
|
|
1262
|
+
return new SheetRangeReferenceNode(sheetName, cellRef, end.value);
|
|
1263
|
+
}
|
|
1264
|
+
return new SheetCellReferenceNode(sheetName, cellRef);
|
|
1229
1265
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
case 2 /* Boolean */:
|
|
1238
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1239
|
-
return new BooleanNode(token.value === "TRUE");
|
|
1240
|
-
case 30 /* ErrorLiteral */:
|
|
1241
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1242
|
-
return new ErrorNode(FormulaValue.error(FormulaValue.errorFromString(token.value)));
|
|
1243
|
-
case 3 /* CellReference */: {
|
|
1244
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1245
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
|
|
1246
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1247
|
-
const end = __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 3 /* CellReference */);
|
|
1248
|
-
return new RangeReferenceNode(token.value, end.value);
|
|
1266
|
+
case 23 /* NamedRange */: {
|
|
1267
|
+
this.#advance();
|
|
1268
|
+
if (this.#current.type === 4 /* Colon */) {
|
|
1269
|
+
const full = this.#tryParseFullRange(token.value);
|
|
1270
|
+
if (full) return full;
|
|
1271
|
+
}
|
|
1272
|
+
return new NamedRangeNode(token.value);
|
|
1249
1273
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
return new SheetRangeReferenceNode(sheetName, cellRef, end.value);
|
|
1274
|
+
case 24 /* SpillReference */:
|
|
1275
|
+
this.#advance();
|
|
1276
|
+
return new SpillReferenceNode(token.value);
|
|
1277
|
+
case 5 /* Function */:
|
|
1278
|
+
return this.#parseFunction();
|
|
1279
|
+
case 27 /* LeftBrace */:
|
|
1280
|
+
return this.#parseArrayLiteral();
|
|
1281
|
+
case 6 /* LeftParen */: {
|
|
1282
|
+
this.#advance();
|
|
1283
|
+
const expr = this.#parseComparison();
|
|
1284
|
+
this.#expect(7 /* RightParen */);
|
|
1285
|
+
return expr;
|
|
1263
1286
|
}
|
|
1264
|
-
|
|
1287
|
+
default:
|
|
1288
|
+
throw new FormulaException(
|
|
1289
|
+
`Unexpected token '${token.value}' (type ${token.type}) at position ${token.position}.`
|
|
1290
|
+
);
|
|
1265
1291
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Completes a whole-column or whole-row reference whose first half has already
|
|
1295
|
+
* been consumed and whose ':' is the current token. Returns null — consuming
|
|
1296
|
+
* nothing — when the two halves are not a matching pair of columns or rows, so
|
|
1297
|
+
* the caller can fall back to its normal interpretation.
|
|
1298
|
+
*
|
|
1299
|
+
* Both halves must be the same kind: `A:A` and `1:5` are ranges, `A:1` is not
|
|
1300
|
+
* a reference at all in Excel and must stay an error rather than becoming a
|
|
1301
|
+
* silently different region.
|
|
1302
|
+
*/
|
|
1303
|
+
#tryParseFullRange(startText, sheetName) {
|
|
1304
|
+
const endToken = this.#tokens[this.#pos + 1];
|
|
1305
|
+
if (!endToken) return null;
|
|
1306
|
+
const start = classifyBound(startText);
|
|
1307
|
+
const end = classifyBound(endToken.value);
|
|
1308
|
+
if (!start || !end || start.kind !== end.kind) return null;
|
|
1309
|
+
this.#advance();
|
|
1310
|
+
this.#advance();
|
|
1311
|
+
const lo = Math.min(start.index, end.index);
|
|
1312
|
+
const hi = Math.max(start.index, end.index);
|
|
1313
|
+
return start.kind === "col" ? new FullRangeReferenceNode(lo, hi, null, null, sheetName) : new FullRangeReferenceNode(null, null, lo, hi, sheetName);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Parses an array constant: `{1,2;3,4}` — ',' separates columns, ';' rows.
|
|
1317
|
+
*
|
|
1318
|
+
* Excel permits only literals inside one (no references, no calls, not even
|
|
1319
|
+
* arithmetic beyond a leading sign), so the value is fully determined here and
|
|
1320
|
+
* is materialised once into the node instead of being rebuilt on every eval.
|
|
1321
|
+
*
|
|
1322
|
+
* A ragged constant is rejected rather than padded: Excel refuses to accept
|
|
1323
|
+
* one at entry, so a file cannot legitimately contain it, and silently filling
|
|
1324
|
+
* the gaps would invent data the author never wrote.
|
|
1325
|
+
*/
|
|
1326
|
+
#parseArrayLiteral() {
|
|
1327
|
+
const open = this.#advance();
|
|
1328
|
+
const rows2 = [];
|
|
1329
|
+
let row2 = [];
|
|
1330
|
+
for (; ; ) {
|
|
1331
|
+
row2.push(this.#parseArrayElement());
|
|
1332
|
+
if (this.#current.type === 8 /* Comma */) {
|
|
1333
|
+
this.#advance();
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (this.#current.type === 29 /* Semicolon */) {
|
|
1337
|
+
this.#advance();
|
|
1338
|
+
rows2.push(row2);
|
|
1339
|
+
row2 = [];
|
|
1340
|
+
continue;
|
|
1271
1341
|
}
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
case 24 /* SpillReference */:
|
|
1275
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1276
|
-
return new SpillReferenceNode(token.value);
|
|
1277
|
-
case 5 /* Function */:
|
|
1278
|
-
return __privateMethod(this, _FormulaParser_instances, parseFunction_fn).call(this);
|
|
1279
|
-
case 27 /* LeftBrace */:
|
|
1280
|
-
return __privateMethod(this, _FormulaParser_instances, parseArrayLiteral_fn).call(this);
|
|
1281
|
-
case 6 /* LeftParen */: {
|
|
1282
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1283
|
-
const expr = __privateMethod(this, _FormulaParser_instances, parseComparison_fn).call(this);
|
|
1284
|
-
__privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 7 /* RightParen */);
|
|
1285
|
-
return expr;
|
|
1342
|
+
break;
|
|
1286
1343
|
}
|
|
1287
|
-
|
|
1344
|
+
rows2.push(row2);
|
|
1345
|
+
this.#expect(28 /* RightBrace */);
|
|
1346
|
+
const cols = rows2[0].length;
|
|
1347
|
+
if (rows2.some((r) => r.length !== cols)) {
|
|
1288
1348
|
throw new FormulaException(
|
|
1289
|
-
`
|
|
1349
|
+
`Array constant at position ${open.position} has rows of differing lengths.`
|
|
1290
1350
|
);
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
*/
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
* the gaps would invent data the author never wrote.
|
|
1325
|
-
*/
|
|
1326
|
-
parseArrayLiteral_fn = function() {
|
|
1327
|
-
const open = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1328
|
-
const rows2 = [];
|
|
1329
|
-
let row2 = [];
|
|
1330
|
-
for (; ; ) {
|
|
1331
|
-
row2.push(__privateMethod(this, _FormulaParser_instances, parseArrayElement_fn).call(this));
|
|
1332
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 8 /* Comma */) {
|
|
1333
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 29 /* Semicolon */) {
|
|
1337
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1338
|
-
rows2.push(row2);
|
|
1339
|
-
row2 = [];
|
|
1340
|
-
continue;
|
|
1341
|
-
}
|
|
1342
|
-
break;
|
|
1343
|
-
}
|
|
1344
|
-
rows2.push(row2);
|
|
1345
|
-
__privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 28 /* RightBrace */);
|
|
1346
|
-
const cols = rows2[0].length;
|
|
1347
|
-
if (rows2.some((r) => r.length !== cols)) {
|
|
1351
|
+
}
|
|
1352
|
+
const array = new ArrayValue(rows2.length, cols);
|
|
1353
|
+
for (let r = 0; r < rows2.length; r++) {
|
|
1354
|
+
for (let c = 0; c < cols; c++) array.set(r, c, rows2[r][c]);
|
|
1355
|
+
}
|
|
1356
|
+
return new ArrayLiteralNode(array);
|
|
1357
|
+
}
|
|
1358
|
+
/** One element of an array constant: a literal, optionally signed. */
|
|
1359
|
+
#parseArrayElement() {
|
|
1360
|
+
let negate = false;
|
|
1361
|
+
let signed = false;
|
|
1362
|
+
while (this.#current.type === 10 /* Minus */ || this.#current.type === 9 /* Plus */) {
|
|
1363
|
+
if (this.#current.type === 10 /* Minus */) negate = !negate;
|
|
1364
|
+
signed = true;
|
|
1365
|
+
this.#advance();
|
|
1366
|
+
}
|
|
1367
|
+
const token = this.#advance();
|
|
1368
|
+
switch (token.type) {
|
|
1369
|
+
case 0 /* Number */: {
|
|
1370
|
+
const n = parseFloat(token.value);
|
|
1371
|
+
if (isNaN(n)) throw new FormulaException(`Invalid number: ${token.value}`);
|
|
1372
|
+
return FormulaValue.number(negate ? -n : n);
|
|
1373
|
+
}
|
|
1374
|
+
case 1 /* String */:
|
|
1375
|
+
if (signed) break;
|
|
1376
|
+
return FormulaValue.text(token.value);
|
|
1377
|
+
case 2 /* Boolean */:
|
|
1378
|
+
if (signed) break;
|
|
1379
|
+
return FormulaValue.boolean(token.value === "TRUE");
|
|
1380
|
+
case 30 /* ErrorLiteral */:
|
|
1381
|
+
if (signed) break;
|
|
1382
|
+
return FormulaValue.error(FormulaValue.errorFromString(token.value));
|
|
1383
|
+
}
|
|
1348
1384
|
throw new FormulaException(
|
|
1349
|
-
`Array
|
|
1385
|
+
`Array constants accept only literal values; got '${token.value}' at position ${token.position}.`
|
|
1350
1386
|
);
|
|
1351
1387
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1388
|
+
#parseFunction() {
|
|
1389
|
+
const nameToken = this.#advance();
|
|
1390
|
+
this.#expect(6 /* LeftParen */);
|
|
1391
|
+
return new FunctionCallNode(nameToken.value.toUpperCase(), this.#parseArgList());
|
|
1355
1392
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
let signed = false;
|
|
1362
|
-
while (__privateGet(this, _FormulaParser_instances, current_get).type === 10 /* Minus */ || __privateGet(this, _FormulaParser_instances, current_get).type === 9 /* Plus */) {
|
|
1363
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 10 /* Minus */) negate = !negate;
|
|
1364
|
-
signed = true;
|
|
1365
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1366
|
-
}
|
|
1367
|
-
const token = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1368
|
-
switch (token.type) {
|
|
1369
|
-
case 0 /* Number */: {
|
|
1370
|
-
const n = parseFloat(token.value);
|
|
1371
|
-
if (isNaN(n)) throw new FormulaException(`Invalid number: ${token.value}`);
|
|
1372
|
-
return FormulaValue.number(negate ? -n : n);
|
|
1373
|
-
}
|
|
1374
|
-
case 1 /* String */:
|
|
1375
|
-
if (signed) break;
|
|
1376
|
-
return FormulaValue.text(token.value);
|
|
1377
|
-
case 2 /* Boolean */:
|
|
1378
|
-
if (signed) break;
|
|
1379
|
-
return FormulaValue.boolean(token.value === "TRUE");
|
|
1380
|
-
case 30 /* ErrorLiteral */:
|
|
1381
|
-
if (signed) break;
|
|
1382
|
-
return FormulaValue.error(FormulaValue.errorFromString(token.value));
|
|
1383
|
-
}
|
|
1384
|
-
throw new FormulaException(
|
|
1385
|
-
`Array constants accept only literal values; got '${token.value}' at position ${token.position}.`
|
|
1386
|
-
);
|
|
1387
|
-
};
|
|
1388
|
-
parseFunction_fn = function() {
|
|
1389
|
-
const nameToken = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1390
|
-
__privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 6 /* LeftParen */);
|
|
1391
|
-
return new FunctionCallNode(nameToken.value.toUpperCase(), __privateMethod(this, _FormulaParser_instances, parseArgList_fn).call(this));
|
|
1392
|
-
};
|
|
1393
|
-
/** Parses a comma-separated argument list up to and including the ')'. */
|
|
1394
|
-
parseArgList_fn = function() {
|
|
1395
|
-
const args = [];
|
|
1396
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type !== 7 /* RightParen */) {
|
|
1397
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 8 /* Comma */) {
|
|
1398
|
-
args.push(new BlankNode());
|
|
1399
|
-
} else {
|
|
1400
|
-
args.push(__privateMethod(this, _FormulaParser_instances, parseComparison_fn).call(this));
|
|
1401
|
-
}
|
|
1402
|
-
while (__privateGet(this, _FormulaParser_instances, current_get).type === 8 /* Comma */) {
|
|
1403
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1404
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 8 /* Comma */ || __privateGet(this, _FormulaParser_instances, current_get).type === 7 /* RightParen */) {
|
|
1393
|
+
/** Parses a comma-separated argument list up to and including the ')'. */
|
|
1394
|
+
#parseArgList() {
|
|
1395
|
+
const args = [];
|
|
1396
|
+
if (this.#current.type !== 7 /* RightParen */) {
|
|
1397
|
+
if (this.#current.type === 8 /* Comma */) {
|
|
1405
1398
|
args.push(new BlankNode());
|
|
1406
1399
|
} else {
|
|
1407
|
-
args.push(
|
|
1400
|
+
args.push(this.#parseComparison());
|
|
1401
|
+
}
|
|
1402
|
+
while (this.#current.type === 8 /* Comma */) {
|
|
1403
|
+
this.#advance();
|
|
1404
|
+
if (this.#current.type === 8 /* Comma */ || this.#current.type === 7 /* RightParen */) {
|
|
1405
|
+
args.push(new BlankNode());
|
|
1406
|
+
} else {
|
|
1407
|
+
args.push(this.#parseComparison());
|
|
1408
|
+
}
|
|
1408
1409
|
}
|
|
1409
1410
|
}
|
|
1411
|
+
this.#expect(7 /* RightParen */);
|
|
1412
|
+
return args;
|
|
1410
1413
|
}
|
|
1411
|
-
__privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 7 /* RightParen */);
|
|
1412
|
-
return args;
|
|
1413
1414
|
};
|
|
1414
1415
|
function classifyBound(text) {
|
|
1415
1416
|
const bare = text.replace(/\$/g, "");
|
|
@@ -2815,6 +2816,8 @@ function registerLookup(r) {
|
|
|
2815
2816
|
r.register("COLUMNS", columns, 1, 1);
|
|
2816
2817
|
r.register("OFFSET", offset, 3, 5, true);
|
|
2817
2818
|
r.register("INDIRECT", indirect, 1, 2, true);
|
|
2819
|
+
r.register("ANCHORARRAY", anchorArray, 1, 1);
|
|
2820
|
+
r.register("SINGLE", single, 1, 1);
|
|
2818
2821
|
}
|
|
2819
2822
|
function num2(a, c, i) {
|
|
2820
2823
|
const r = FormulaHelper.evalDouble(a[i], c);
|
|
@@ -2838,8 +2841,9 @@ function vlookup(a, c) {
|
|
|
2838
2841
|
const tbl = tableVal.arrayVal;
|
|
2839
2842
|
if (colIdx < 0 || colIdx >= tbl.columns) return FormulaValue.errorRef;
|
|
2840
2843
|
if (exactMatch) {
|
|
2844
|
+
const pattern = lookupPattern(lookupVal);
|
|
2841
2845
|
for (let r = 0; r < tbl.rows; r++) {
|
|
2842
|
-
if (
|
|
2846
|
+
if (matchesLookup(tbl.get(r, 0), lookupVal, pattern)) return tbl.get(r, colIdx);
|
|
2843
2847
|
}
|
|
2844
2848
|
return FormulaValue.errorNA;
|
|
2845
2849
|
}
|
|
@@ -2868,8 +2872,9 @@ function hlookup(a, c) {
|
|
|
2868
2872
|
const tbl = tableVal.arrayVal;
|
|
2869
2873
|
if (rowIdx < 0 || rowIdx >= tbl.rows) return FormulaValue.errorRef;
|
|
2870
2874
|
if (exactMatch) {
|
|
2875
|
+
const pattern = lookupPattern(lookupVal);
|
|
2871
2876
|
for (let col = 0; col < tbl.columns; col++) {
|
|
2872
|
-
if (
|
|
2877
|
+
if (matchesLookup(tbl.get(0, col), lookupVal, pattern)) return tbl.get(rowIdx, col);
|
|
2873
2878
|
}
|
|
2874
2879
|
return FormulaValue.errorNA;
|
|
2875
2880
|
}
|
|
@@ -2880,18 +2885,13 @@ function hlookup(a, c) {
|
|
|
2880
2885
|
}
|
|
2881
2886
|
return best >= 0 ? tbl.get(rowIdx, best) : FormulaValue.errorNA;
|
|
2882
2887
|
}
|
|
2883
|
-
function
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
} else if (ch === "*") out += ".*";
|
|
2891
|
-
else if (ch === "?") out += ".";
|
|
2892
|
-
else out += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2893
|
-
}
|
|
2894
|
-
return new RegExp(`^${out}$`, "i");
|
|
2888
|
+
function lookupPattern(lookupVal) {
|
|
2889
|
+
if (!lookupVal.isText) return null;
|
|
2890
|
+
const text = lookupVal.textValue;
|
|
2891
|
+
return FormulaHelper.hasWildcardSyntax(text) ? FormulaHelper.wildcardToRegex(text) : null;
|
|
2892
|
+
}
|
|
2893
|
+
function matchesLookup(candidate, lookupVal, pattern) {
|
|
2894
|
+
return pattern === null ? FormulaValue.areEqual(candidate, lookupVal) : !candidate.isBlank && pattern.test(candidate.asText());
|
|
2895
2895
|
}
|
|
2896
2896
|
function findMatch(n, get, lookupVal, matchMode, searchMode) {
|
|
2897
2897
|
if ((searchMode === 2 || searchMode === -2) && matchMode !== 2) {
|
|
@@ -2915,8 +2915,11 @@ function findMatch(n, get, lookupVal, matchMode, searchMode) {
|
|
|
2915
2915
|
const reverse = searchMode === -1;
|
|
2916
2916
|
const order = reverse ? Array.from({ length: n }, (_, k) => n - 1 - k) : Array.from({ length: n }, (_, k) => k);
|
|
2917
2917
|
if (matchMode === 2) {
|
|
2918
|
-
const re =
|
|
2919
|
-
for (const i of order)
|
|
2918
|
+
const re = FormulaHelper.wildcardToRegex(lookupVal.asText());
|
|
2919
|
+
for (const i of order) {
|
|
2920
|
+
const v = get(i);
|
|
2921
|
+
if (!v.isBlank && re.test(v.asText())) return i;
|
|
2922
|
+
}
|
|
2920
2923
|
return -1;
|
|
2921
2924
|
}
|
|
2922
2925
|
if (matchMode === 0) {
|
|
@@ -3026,13 +3029,15 @@ function match(a, c) {
|
|
|
3026
3029
|
matchType = Math.round(r.n);
|
|
3027
3030
|
}
|
|
3028
3031
|
if (!arrVal.isArray) {
|
|
3029
|
-
|
|
3032
|
+
const single2 = matchType === 0 ? matchesLookup(arrVal, lookupVal, lookupPattern(lookupVal)) : FormulaValue.areEqual(arrVal, lookupVal);
|
|
3033
|
+
return single2 ? FormulaValue.one : FormulaValue.errorNA;
|
|
3030
3034
|
}
|
|
3031
3035
|
const arr = arrVal.arrayVal;
|
|
3032
3036
|
const len2 = arr.length;
|
|
3033
3037
|
if (matchType === 0) {
|
|
3038
|
+
const pattern = lookupPattern(lookupVal);
|
|
3034
3039
|
for (let i = 0; i < len2; i++) {
|
|
3035
|
-
if (
|
|
3040
|
+
if (matchesLookup(arr.getFlat(i), lookupVal, pattern)) return FormulaValue.number(i + 1);
|
|
3036
3041
|
}
|
|
3037
3042
|
return FormulaValue.errorNA;
|
|
3038
3043
|
}
|
|
@@ -3262,6 +3267,15 @@ function offset(a, c) {
|
|
|
3262
3267
|
cellRefFromRowCol(newRow + height - 1, newCol + width - 1)
|
|
3263
3268
|
);
|
|
3264
3269
|
}
|
|
3270
|
+
function anchorArray(a, c) {
|
|
3271
|
+
const arg2 = a[0];
|
|
3272
|
+
if (arg2 instanceof CellReferenceNode) return c.resolveSpillRange(arg2.reference);
|
|
3273
|
+
if (arg2 instanceof SheetCellReferenceNode) return c.resolveSheetSpillRange(arg2.sheetName, arg2.cellReference);
|
|
3274
|
+
return FormulaValue.errorRef;
|
|
3275
|
+
}
|
|
3276
|
+
function single(a, c) {
|
|
3277
|
+
return new ImplicitIntersectionNode(a[0]).evaluate(c);
|
|
3278
|
+
}
|
|
3265
3279
|
|
|
3266
3280
|
// src/functions/statistical-functions.ts
|
|
3267
3281
|
function registerStatistical(r) {
|
|
@@ -4542,24 +4556,20 @@ function toRow(a, c) {
|
|
|
4542
4556
|
|
|
4543
4557
|
// src/function-registry.ts
|
|
4544
4558
|
var MAX_LIFTED_CELLS = 1048576;
|
|
4545
|
-
var
|
|
4546
|
-
|
|
4547
|
-
constructor() {
|
|
4548
|
-
__privateAdd(this, _FunctionRegistry_instances);
|
|
4549
|
-
__privateAdd(this, _functions, /* @__PURE__ */ new Map());
|
|
4550
|
-
}
|
|
4559
|
+
var FunctionRegistry = class _FunctionRegistry {
|
|
4560
|
+
static #default;
|
|
4551
4561
|
/** Lazily-created default registry with all built-in functions registered. */
|
|
4552
4562
|
static get default() {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
__privateMethod(_a = __privateGet(_FunctionRegistry, _default), _FunctionRegistry_instances, registerBuiltIns_fn).call(_a);
|
|
4563
|
+
if (!_FunctionRegistry.#default) {
|
|
4564
|
+
_FunctionRegistry.#default = new _FunctionRegistry();
|
|
4565
|
+
_FunctionRegistry.#default.#registerBuiltIns();
|
|
4557
4566
|
}
|
|
4558
|
-
return
|
|
4567
|
+
return _FunctionRegistry.#default;
|
|
4559
4568
|
}
|
|
4569
|
+
#functions = /* @__PURE__ */ new Map();
|
|
4560
4570
|
/** Registers a function. Name is normalised to UPPERCASE. */
|
|
4561
4571
|
register(name, handler, minArgs = 0, maxArgs = -1, isVolatile = false) {
|
|
4562
|
-
|
|
4572
|
+
this.#functions.set(name.toUpperCase(), {
|
|
4563
4573
|
name: name.toUpperCase(),
|
|
4564
4574
|
minArgs,
|
|
4565
4575
|
maxArgs,
|
|
@@ -4585,7 +4595,7 @@ var _FunctionRegistry = class _FunctionRegistry {
|
|
|
4585
4595
|
* `register`, or lifting would map it over the array it is meant to reduce.
|
|
4586
4596
|
*/
|
|
4587
4597
|
registerElementwise(name, handler, minArgs = 0, maxArgs = -1) {
|
|
4588
|
-
|
|
4598
|
+
this.#functions.set(name.toUpperCase(), {
|
|
4589
4599
|
name: name.toUpperCase(),
|
|
4590
4600
|
minArgs,
|
|
4591
4601
|
maxArgs,
|
|
@@ -4596,81 +4606,84 @@ var _FunctionRegistry = class _FunctionRegistry {
|
|
|
4596
4606
|
}
|
|
4597
4607
|
/** Executes a function by name with argument validation. */
|
|
4598
4608
|
execute(name, args, ctx) {
|
|
4599
|
-
|
|
4600
|
-
const def = __privateGet(this, _functions).get(name.toUpperCase());
|
|
4609
|
+
const def = this.#functions.get(name.toUpperCase());
|
|
4601
4610
|
if (!def) return FormulaValue.errorName;
|
|
4602
4611
|
if (args.length < def.minArgs) return FormulaValue.errorValue;
|
|
4603
4612
|
if (def.maxArgs >= 0 && args.length > def.maxArgs) return FormulaValue.errorValue;
|
|
4604
4613
|
try {
|
|
4605
|
-
return def.elementwise ?
|
|
4614
|
+
return def.elementwise ? _FunctionRegistry.#executeElementwise(def, args, ctx) : def.handler(args, ctx);
|
|
4606
4615
|
} catch {
|
|
4607
4616
|
return FormulaValue.errorValue;
|
|
4608
4617
|
}
|
|
4609
4618
|
}
|
|
4619
|
+
/**
|
|
4620
|
+
* Runs a scalar handler, mapping it over any array arguments.
|
|
4621
|
+
*
|
|
4622
|
+
* Arguments are evaluated exactly once and the handler is re-driven through
|
|
4623
|
+
* {@link ValueNode}s, so a nested call in an argument is not recomputed per
|
|
4624
|
+
* output cell — the alternative (letting the handler re-evaluate the original
|
|
4625
|
+
* nodes) would multiply the cost of `LEFT(expensive(), 3)` by the array size.
|
|
4626
|
+
* The all-scalar case — the overwhelming majority of calls — costs one array of
|
|
4627
|
+
* wrappers and takes the same path, so there is no second code path to keep in
|
|
4628
|
+
* step.
|
|
4629
|
+
*/
|
|
4630
|
+
static #executeElementwise(def, args, ctx) {
|
|
4631
|
+
const values = args.map((a) => a.evaluate(ctx));
|
|
4632
|
+
let rows2 = 1, cols = 1;
|
|
4633
|
+
for (const v of values) {
|
|
4634
|
+
if (!v.isArray) continue;
|
|
4635
|
+
const a = v.arrayVal;
|
|
4636
|
+
if (a.rows > rows2) rows2 = a.rows;
|
|
4637
|
+
if (a.columns > cols) cols = a.columns;
|
|
4638
|
+
}
|
|
4639
|
+
if (rows2 === 1 && cols === 1) {
|
|
4640
|
+
return def.handler(values.map((v) => new ValueNode(v.isArray ? v.arrayVal.get(0, 0) : v)), ctx);
|
|
4641
|
+
}
|
|
4642
|
+
if (rows2 * cols > MAX_LIFTED_CELLS) return FormulaValue.errorValue;
|
|
4643
|
+
for (const v of values) {
|
|
4644
|
+
if (!v.isArray) continue;
|
|
4645
|
+
const a = v.arrayVal;
|
|
4646
|
+
if (a.rows !== 1 && a.rows !== rows2 || a.columns !== 1 && a.columns !== cols) {
|
|
4647
|
+
return FormulaValue.errorValue;
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
const nodes = values.map((v) => new ValueNode(v));
|
|
4651
|
+
const out = new ArrayValue(rows2, cols);
|
|
4652
|
+
for (let r = 0; r < rows2; r++) {
|
|
4653
|
+
for (let c = 0; c < cols; c++) {
|
|
4654
|
+
for (let i = 0; i < values.length; i++) {
|
|
4655
|
+
const v = values[i];
|
|
4656
|
+
if (!v.isArray) continue;
|
|
4657
|
+
const a = v.arrayVal;
|
|
4658
|
+
nodes[i].value = a.get(a.rows === 1 ? 0 : r, a.columns === 1 ? 0 : c);
|
|
4659
|
+
}
|
|
4660
|
+
out.set(r, c, def.handler(nodes, ctx));
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
return FormulaValue.array(out);
|
|
4664
|
+
}
|
|
4610
4665
|
/** Returns true if the named function is volatile (NOW, RAND, etc.). */
|
|
4611
4666
|
isVolatile(name) {
|
|
4612
|
-
return
|
|
4667
|
+
return this.#functions.get(name.toUpperCase())?.isVolatile ?? false;
|
|
4613
4668
|
}
|
|
4614
4669
|
/** Returns true if the function is registered. */
|
|
4615
4670
|
has(name) {
|
|
4616
|
-
return
|
|
4617
|
-
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
}
|
|
4631
|
-
if (rows2 === 1 && cols === 1) {
|
|
4632
|
-
return def.handler(values.map((v) => new ValueNode(v.isArray ? v.arrayVal.get(0, 0) : v)), ctx);
|
|
4633
|
-
}
|
|
4634
|
-
if (rows2 * cols > MAX_LIFTED_CELLS) return FormulaValue.errorValue;
|
|
4635
|
-
for (const v of values) {
|
|
4636
|
-
if (!v.isArray) continue;
|
|
4637
|
-
const a = v.arrayVal;
|
|
4638
|
-
if (a.rows !== 1 && a.rows !== rows2 || a.columns !== 1 && a.columns !== cols) {
|
|
4639
|
-
return FormulaValue.errorValue;
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
const nodes = values.map((v) => new ValueNode(v));
|
|
4643
|
-
const out = new ArrayValue(rows2, cols);
|
|
4644
|
-
for (let r = 0; r < rows2; r++) {
|
|
4645
|
-
for (let c = 0; c < cols; c++) {
|
|
4646
|
-
for (let i = 0; i < values.length; i++) {
|
|
4647
|
-
const v = values[i];
|
|
4648
|
-
if (!v.isArray) continue;
|
|
4649
|
-
const a = v.arrayVal;
|
|
4650
|
-
nodes[i].value = a.get(a.rows === 1 ? 0 : r, a.columns === 1 ? 0 : c);
|
|
4651
|
-
}
|
|
4652
|
-
out.set(r, c, def.handler(nodes, ctx));
|
|
4653
|
-
}
|
|
4671
|
+
return this.#functions.has(name.toUpperCase());
|
|
4672
|
+
}
|
|
4673
|
+
#registerBuiltIns() {
|
|
4674
|
+
registerMath(this);
|
|
4675
|
+
registerText(this);
|
|
4676
|
+
registerLogical(this);
|
|
4677
|
+
registerDate(this);
|
|
4678
|
+
registerLookup(this);
|
|
4679
|
+
registerStatistical(this);
|
|
4680
|
+
registerInfo(this);
|
|
4681
|
+
registerFinance(this);
|
|
4682
|
+
registerMeta(this);
|
|
4683
|
+
registerAggregate(this);
|
|
4684
|
+
registerArray(this);
|
|
4654
4685
|
}
|
|
4655
|
-
return FormulaValue.array(out);
|
|
4656
|
-
};
|
|
4657
|
-
_FunctionRegistry_instances = new WeakSet();
|
|
4658
|
-
registerBuiltIns_fn = function() {
|
|
4659
|
-
registerMath(this);
|
|
4660
|
-
registerText(this);
|
|
4661
|
-
registerLogical(this);
|
|
4662
|
-
registerDate(this);
|
|
4663
|
-
registerLookup(this);
|
|
4664
|
-
registerStatistical(this);
|
|
4665
|
-
registerInfo(this);
|
|
4666
|
-
registerFinance(this);
|
|
4667
|
-
registerMeta(this);
|
|
4668
|
-
registerAggregate(this);
|
|
4669
|
-
registerArray(this);
|
|
4670
4686
|
};
|
|
4671
|
-
__privateAdd(_FunctionRegistry, _FunctionRegistry_static);
|
|
4672
|
-
__privateAdd(_FunctionRegistry, _default);
|
|
4673
|
-
var FunctionRegistry = _FunctionRegistry;
|
|
4674
4687
|
var FormulaHelper;
|
|
4675
4688
|
((FormulaHelper2) => {
|
|
4676
4689
|
function evalDouble(node, ctx) {
|
|
@@ -4743,7 +4756,7 @@ var FormulaHelper;
|
|
|
4743
4756
|
const test = (v) => wanted(FormulaValue.compare(v, rhsValue));
|
|
4744
4757
|
return rhsNum !== null ? { kind: "compareNumber", number: rhsNum, op, test } : { kind: "other", test };
|
|
4745
4758
|
}
|
|
4746
|
-
if (
|
|
4759
|
+
if (hasWildcardSyntax(trimmed)) {
|
|
4747
4760
|
const pattern = wildcardToRegex(trimmed);
|
|
4748
4761
|
return { kind: "other", test: (v) => pattern.test(v.asText()) };
|
|
4749
4762
|
}
|
|
@@ -4775,53 +4788,106 @@ var FormulaHelper;
|
|
|
4775
4788
|
return compileCriteria(criteria).test(value2);
|
|
4776
4789
|
}
|
|
4777
4790
|
FormulaHelper2.matchesCriteria = matchesCriteria;
|
|
4778
|
-
function
|
|
4779
|
-
|
|
4780
|
-
|
|
4791
|
+
function hasWildcardSyntax(pattern) {
|
|
4792
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
4793
|
+
const ch = pattern.charCodeAt(i);
|
|
4794
|
+
if (ch === 42 || ch === 63 || ch === 126) return true;
|
|
4795
|
+
}
|
|
4796
|
+
return false;
|
|
4781
4797
|
}
|
|
4798
|
+
FormulaHelper2.hasWildcardSyntax = hasWildcardSyntax;
|
|
4799
|
+
function wildcardToRegex(pattern) {
|
|
4800
|
+
let out = "";
|
|
4801
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
4802
|
+
const ch = pattern[i];
|
|
4803
|
+
const next = pattern[i + 1];
|
|
4804
|
+
if (ch === "~" && (next === "*" || next === "?" || next === "~")) {
|
|
4805
|
+
out += escapeRegex(next);
|
|
4806
|
+
i++;
|
|
4807
|
+
} else if (ch === "*") out += ".*";
|
|
4808
|
+
else if (ch === "?") out += ".";
|
|
4809
|
+
else out += escapeRegex(ch);
|
|
4810
|
+
}
|
|
4811
|
+
return new RegExp(`^${out}$`, "i");
|
|
4812
|
+
}
|
|
4813
|
+
FormulaHelper2.wildcardToRegex = wildcardToRegex;
|
|
4814
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4782
4815
|
})(FormulaHelper || (FormulaHelper = {}));
|
|
4783
4816
|
|
|
4784
4817
|
// src/formula-context.ts
|
|
4785
|
-
var
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4818
|
+
var FormulaContext = class _FormulaContext {
|
|
4819
|
+
#sheet;
|
|
4820
|
+
#functions;
|
|
4821
|
+
#evaluating;
|
|
4822
|
+
/** Row of the formula cell (for @ implicit intersection). */
|
|
4823
|
+
formulaRow;
|
|
4824
|
+
/** Column of the formula cell (for @ implicit intersection). */
|
|
4825
|
+
formulaCol;
|
|
4826
|
+
/** Optional workbook reference for cross-sheet references. */
|
|
4827
|
+
workbook;
|
|
4828
|
+
/**
|
|
4829
|
+
* Optional memo for range reads, supplied by whoever owns the write side —
|
|
4830
|
+
* the recalculation engine, which invalidates it as it writes results. Left
|
|
4831
|
+
* undefined for one-shot evaluation, where a stale entry could otherwise
|
|
4832
|
+
* outlive a caller's direct edit to the model.
|
|
4833
|
+
*/
|
|
4834
|
+
rangeCache;
|
|
4835
|
+
/**
|
|
4836
|
+
* Optional named-range resolver, installed by the evaluator when the workbook
|
|
4837
|
+
* exposes getDefinedName. Given a name, returns its evaluated value. Left
|
|
4838
|
+
* undefined when no resolver is available, in which case named ranges are
|
|
4839
|
+
* #NAME? (unchanged legacy behaviour).
|
|
4840
|
+
*/
|
|
4841
|
+
nameResolver;
|
|
4842
|
+
/**
|
|
4843
|
+
* Shared recursion budget for LAMBDA calls, threaded across all contexts in a
|
|
4844
|
+
* single evaluation. Guards non-tail recursion (which grows the JS stack);
|
|
4845
|
+
* tail recursion is trampolined and does not count against it.
|
|
4846
|
+
*/
|
|
4847
|
+
recursionGuard;
|
|
4848
|
+
/**
|
|
4849
|
+
* Lexical scope stack for LET/LAMBDA local names. Each frame maps an
|
|
4850
|
+
* upper-cased local name to its value. Names resolve innermost-first and take
|
|
4851
|
+
* precedence over workbook defined names.
|
|
4852
|
+
*/
|
|
4853
|
+
#scopes = [];
|
|
4804
4854
|
/** Pushes a new binding frame (used by LET/LAMBDA). */
|
|
4805
4855
|
pushScope(bindings) {
|
|
4806
|
-
|
|
4856
|
+
this.#scopes.push(bindings);
|
|
4807
4857
|
}
|
|
4808
4858
|
/** Pops the innermost binding frame. */
|
|
4809
4859
|
popScope() {
|
|
4810
|
-
|
|
4860
|
+
this.#scopes.pop();
|
|
4861
|
+
}
|
|
4862
|
+
#lookupScope(name) {
|
|
4863
|
+
if (this.#scopes.length === 0) return void 0;
|
|
4864
|
+
const key = name.toUpperCase();
|
|
4865
|
+
for (let i = this.#scopes.length - 1; i >= 0; i--) {
|
|
4866
|
+
const v = this.#scopes[i].get(key);
|
|
4867
|
+
if (v !== void 0) return v;
|
|
4868
|
+
}
|
|
4869
|
+
return void 0;
|
|
4870
|
+
}
|
|
4871
|
+
constructor(sheet, formulaRow = 0, formulaCol = 0, evaluating, registry) {
|
|
4872
|
+
this.#sheet = sheet;
|
|
4873
|
+
this.formulaRow = formulaRow;
|
|
4874
|
+
this.formulaCol = formulaCol;
|
|
4875
|
+
this.#evaluating = evaluating ?? /* @__PURE__ */ new Set();
|
|
4876
|
+
this.#functions = registry ?? FunctionRegistry.default;
|
|
4811
4877
|
}
|
|
4812
4878
|
get worksheet() {
|
|
4813
|
-
return
|
|
4879
|
+
return this.#sheet;
|
|
4814
4880
|
}
|
|
4815
4881
|
// ── Cell/Range resolution ─────────────────────────────────────────────────
|
|
4816
4882
|
getCellValue(cellReference) {
|
|
4817
4883
|
const norm = cellReference.replace(/\$/g, "").toUpperCase();
|
|
4818
|
-
if (
|
|
4819
|
-
|
|
4884
|
+
if (this.#evaluating.has(norm)) return FormulaValue.errorRef;
|
|
4885
|
+
this.#evaluating.add(norm);
|
|
4820
4886
|
try {
|
|
4821
|
-
const raw =
|
|
4887
|
+
const raw = this.#sheet.getCellValue(cellReference);
|
|
4822
4888
|
return FormulaValue.fromObjectSync(raw);
|
|
4823
4889
|
} finally {
|
|
4824
|
-
|
|
4890
|
+
this.#evaluating.delete(norm);
|
|
4825
4891
|
}
|
|
4826
4892
|
}
|
|
4827
4893
|
getRangeValues(startRef, endRef) {
|
|
@@ -4839,17 +4905,17 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4839
4905
|
const rows2 = endRow - startRow + 1;
|
|
4840
4906
|
const cols = endCol - startCol + 1;
|
|
4841
4907
|
const cache = rows2 * cols > 1 ? this.rangeCache : void 0;
|
|
4842
|
-
const sheetKey = cache ?
|
|
4908
|
+
const sheetKey = cache ? this.#sheet.name.toUpperCase() : "";
|
|
4843
4909
|
if (cache) {
|
|
4844
4910
|
const hit = cache.get(sheetKey, startRow, startCol, endRow, endCol);
|
|
4845
4911
|
if (hit !== void 0) return hit;
|
|
4846
4912
|
}
|
|
4847
4913
|
const arr = new ArrayValue(rows2, cols);
|
|
4848
|
-
const at =
|
|
4914
|
+
const at = this.#sheet.getCellValueAt;
|
|
4849
4915
|
if (at) {
|
|
4850
4916
|
for (let r = 0; r < rows2; r++) {
|
|
4851
4917
|
for (let c = 0; c < cols; c++) {
|
|
4852
|
-
arr.set(r, c, FormulaValue.fromObjectSync(at.call(
|
|
4918
|
+
arr.set(r, c, FormulaValue.fromObjectSync(at.call(this.#sheet, startRow + r, startCol + c)));
|
|
4853
4919
|
}
|
|
4854
4920
|
}
|
|
4855
4921
|
} else {
|
|
@@ -4875,7 +4941,7 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4875
4941
|
* silently reading nothing would make SUM(A:A) return 0 on a full column.
|
|
4876
4942
|
*/
|
|
4877
4943
|
getFullRangeValues(startCol, endCol, startRow, endRow) {
|
|
4878
|
-
const bounds =
|
|
4944
|
+
const bounds = this.#sheet.usedBounds;
|
|
4879
4945
|
if (!bounds) return FormulaValue.errorRef;
|
|
4880
4946
|
if (bounds.rowCount === 0 || bounds.columnCount === 0) return FormulaValue.blank;
|
|
4881
4947
|
const r0 = startRow ?? 1;
|
|
@@ -4890,7 +4956,7 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4890
4956
|
getSheetCellValue(sheetName, cellReference) {
|
|
4891
4957
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4892
4958
|
try {
|
|
4893
|
-
return
|
|
4959
|
+
return this.#forSheet(sheetName).getCellValue(cellReference);
|
|
4894
4960
|
} catch {
|
|
4895
4961
|
return FormulaValue.errorRef;
|
|
4896
4962
|
}
|
|
@@ -4898,7 +4964,7 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4898
4964
|
getSheetRangeValues(sheetName, startRef, endRef) {
|
|
4899
4965
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4900
4966
|
try {
|
|
4901
|
-
return
|
|
4967
|
+
return this.#forSheet(sheetName).getRangeValues(startRef, endRef);
|
|
4902
4968
|
} catch {
|
|
4903
4969
|
return FormulaValue.errorRef;
|
|
4904
4970
|
}
|
|
@@ -4907,13 +4973,28 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4907
4973
|
getSheetFullRangeValues(sheetName, startCol, endCol, startRow, endRow) {
|
|
4908
4974
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4909
4975
|
try {
|
|
4910
|
-
return
|
|
4976
|
+
return this.#forSheet(sheetName).getFullRangeValues(startCol, endCol, startRow, endRow);
|
|
4911
4977
|
} catch {
|
|
4912
4978
|
return FormulaValue.errorRef;
|
|
4913
4979
|
}
|
|
4914
4980
|
}
|
|
4981
|
+
/**
|
|
4982
|
+
* A sibling context bound to another sheet, sharing this one's circular-
|
|
4983
|
+
* reference guard and range memo. These contexts only read values — they
|
|
4984
|
+
* never evaluate an AST — so the name resolver and recursion budget are
|
|
4985
|
+
* deliberately not carried over; passing them would suggest a scoping
|
|
4986
|
+
* relationship that does not exist. The memo, on the other hand, must be
|
|
4987
|
+
* carried: cross-sheet ranges are the most expensive reads in a workbook.
|
|
4988
|
+
*/
|
|
4989
|
+
#forSheet(sheetName) {
|
|
4990
|
+
const sheet = this.workbook.getWorksheet(sheetName);
|
|
4991
|
+
const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, this.#evaluating, this.#functions);
|
|
4992
|
+
ctx.workbook = this.workbook;
|
|
4993
|
+
ctx.rangeCache = this.rangeCache;
|
|
4994
|
+
return ctx;
|
|
4995
|
+
}
|
|
4915
4996
|
resolveNamedRange(name) {
|
|
4916
|
-
const scoped =
|
|
4997
|
+
const scoped = this.#lookupScope(name);
|
|
4917
4998
|
if (scoped !== void 0) return scoped;
|
|
4918
4999
|
return this.nameResolver ? this.nameResolver(name) : FormulaValue.errorName;
|
|
4919
5000
|
}
|
|
@@ -4922,12 +5003,21 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4922
5003
|
* cell. Requires the worksheet to expose getSpillRange; otherwise #REF!.
|
|
4923
5004
|
*/
|
|
4924
5005
|
resolveSpillRange(anchorRef) {
|
|
4925
|
-
const spillRef =
|
|
5006
|
+
const spillRef = this.#sheet.getSpillRange?.(anchorRef);
|
|
4926
5007
|
if (!spillRef) return FormulaValue.errorRef;
|
|
4927
5008
|
const colon = spillRef.indexOf(":");
|
|
4928
5009
|
if (colon < 0) return this.getCellValue(spillRef);
|
|
4929
5010
|
return this.getRangeValues(spillRef.slice(0, colon), spillRef.slice(colon + 1));
|
|
4930
5011
|
}
|
|
5012
|
+
/** {@link resolveSpillRange} for an anchor on another sheet ('S'!A1#). */
|
|
5013
|
+
resolveSheetSpillRange(sheetName, anchorRef) {
|
|
5014
|
+
if (!this.workbook) return FormulaValue.errorRef;
|
|
5015
|
+
try {
|
|
5016
|
+
return this.#forSheet(sheetName).resolveSpillRange(anchorRef);
|
|
5017
|
+
} catch {
|
|
5018
|
+
return FormulaValue.errorRef;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
4931
5021
|
getRangeBounds(startRef, endRef) {
|
|
4932
5022
|
const s = parseCellRef(startRef);
|
|
4933
5023
|
const e = parseCellRef(endRef);
|
|
@@ -4936,94 +5026,63 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4936
5026
|
// ── Function dispatch ─────────────────────────────────────────────────────
|
|
4937
5027
|
/** True if a built-in function with this name is registered. */
|
|
4938
5028
|
hasFunction(name) {
|
|
4939
|
-
return
|
|
5029
|
+
return this.#functions.has(name);
|
|
4940
5030
|
}
|
|
4941
5031
|
evaluateFunction(name, args) {
|
|
4942
|
-
if (!
|
|
5032
|
+
if (!this.#functions.has(name)) {
|
|
4943
5033
|
const named = this.resolveNamedRange(name);
|
|
4944
5034
|
if (named.isLambda) {
|
|
4945
5035
|
return named.lambdaVal.call(args.map((a) => a.evaluate(this)));
|
|
4946
5036
|
}
|
|
4947
5037
|
}
|
|
4948
|
-
return
|
|
5038
|
+
return this.#functions.execute(name, args, this);
|
|
4949
5039
|
}
|
|
4950
5040
|
};
|
|
4951
|
-
_sheet = new WeakMap();
|
|
4952
|
-
_functions2 = new WeakMap();
|
|
4953
|
-
_evaluating = new WeakMap();
|
|
4954
|
-
_scopes = new WeakMap();
|
|
4955
|
-
_FormulaContext_instances = new WeakSet();
|
|
4956
|
-
lookupScope_fn = function(name) {
|
|
4957
|
-
if (__privateGet(this, _scopes).length === 0) return void 0;
|
|
4958
|
-
const key = name.toUpperCase();
|
|
4959
|
-
for (let i = __privateGet(this, _scopes).length - 1; i >= 0; i--) {
|
|
4960
|
-
const v = __privateGet(this, _scopes)[i].get(key);
|
|
4961
|
-
if (v !== void 0) return v;
|
|
4962
|
-
}
|
|
4963
|
-
return void 0;
|
|
4964
|
-
};
|
|
4965
|
-
/**
|
|
4966
|
-
* A sibling context bound to another sheet, sharing this one's circular-
|
|
4967
|
-
* reference guard and range memo. These contexts only read values — they
|
|
4968
|
-
* never evaluate an AST — so the name resolver and recursion budget are
|
|
4969
|
-
* deliberately not carried over; passing them would suggest a scoping
|
|
4970
|
-
* relationship that does not exist. The memo, on the other hand, must be
|
|
4971
|
-
* carried: cross-sheet ranges are the most expensive reads in a workbook.
|
|
4972
|
-
*/
|
|
4973
|
-
forSheet_fn = function(sheetName) {
|
|
4974
|
-
const sheet = this.workbook.getWorksheet(sheetName);
|
|
4975
|
-
const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, __privateGet(this, _evaluating), __privateGet(this, _functions2));
|
|
4976
|
-
ctx.workbook = this.workbook;
|
|
4977
|
-
ctx.rangeCache = this.rangeCache;
|
|
4978
|
-
return ctx;
|
|
4979
|
-
};
|
|
4980
|
-
var FormulaContext = _FormulaContext;
|
|
4981
5041
|
|
|
4982
5042
|
// src/lru-cache.ts
|
|
4983
|
-
var _map, _capacity, _hits, _misses, _evictions;
|
|
4984
5043
|
var LruCache = class {
|
|
5044
|
+
#map;
|
|
5045
|
+
#capacity;
|
|
5046
|
+
#hits = 0;
|
|
5047
|
+
#misses = 0;
|
|
5048
|
+
#evictions = 0;
|
|
4985
5049
|
constructor(capacity) {
|
|
4986
|
-
__privateAdd(this, _map);
|
|
4987
|
-
__privateAdd(this, _capacity);
|
|
4988
|
-
__privateAdd(this, _hits, 0);
|
|
4989
|
-
__privateAdd(this, _misses, 0);
|
|
4990
|
-
__privateAdd(this, _evictions, 0);
|
|
4991
5050
|
if (capacity <= 0) throw new RangeError("LruCache capacity must be > 0.");
|
|
4992
|
-
|
|
4993
|
-
|
|
5051
|
+
this.#capacity = capacity;
|
|
5052
|
+
this.#map = /* @__PURE__ */ new Map();
|
|
4994
5053
|
}
|
|
4995
5054
|
get capacity() {
|
|
4996
|
-
return
|
|
5055
|
+
return this.#capacity;
|
|
4997
5056
|
}
|
|
4998
5057
|
get size() {
|
|
4999
|
-
return
|
|
5058
|
+
return this.#map.size;
|
|
5000
5059
|
}
|
|
5001
5060
|
get hits() {
|
|
5002
|
-
return
|
|
5061
|
+
return this.#hits;
|
|
5003
5062
|
}
|
|
5004
5063
|
get misses() {
|
|
5005
|
-
return
|
|
5064
|
+
return this.#misses;
|
|
5006
5065
|
}
|
|
5007
5066
|
get evictions() {
|
|
5008
|
-
return
|
|
5067
|
+
return this.#evictions;
|
|
5009
5068
|
}
|
|
5010
5069
|
get hitRate() {
|
|
5011
|
-
const total =
|
|
5012
|
-
return total === 0 ? 0 :
|
|
5070
|
+
const total = this.#hits + this.#misses;
|
|
5071
|
+
return total === 0 ? 0 : this.#hits / total;
|
|
5013
5072
|
}
|
|
5014
5073
|
/**
|
|
5015
5074
|
* Returns the value for key, or undefined if not present.
|
|
5016
5075
|
* Moves the accessed key to the "most recently used" end.
|
|
5017
5076
|
*/
|
|
5018
5077
|
get(key) {
|
|
5019
|
-
const value2 =
|
|
5078
|
+
const value2 = this.#map.get(key);
|
|
5020
5079
|
if (value2 === void 0) {
|
|
5021
|
-
|
|
5080
|
+
this.#misses++;
|
|
5022
5081
|
return void 0;
|
|
5023
5082
|
}
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5083
|
+
this.#map.delete(key);
|
|
5084
|
+
this.#map.set(key, value2);
|
|
5085
|
+
this.#hits++;
|
|
5027
5086
|
return value2;
|
|
5028
5087
|
}
|
|
5029
5088
|
/**
|
|
@@ -5031,44 +5090,39 @@ var LruCache = class {
|
|
|
5031
5090
|
* Evicts the least-recently-used entry if over capacity.
|
|
5032
5091
|
*/
|
|
5033
5092
|
set(key, value2) {
|
|
5034
|
-
if (
|
|
5035
|
-
|
|
5036
|
-
} else if (
|
|
5037
|
-
const oldest =
|
|
5038
|
-
|
|
5039
|
-
|
|
5093
|
+
if (this.#map.has(key)) {
|
|
5094
|
+
this.#map.delete(key);
|
|
5095
|
+
} else if (this.#map.size >= this.#capacity) {
|
|
5096
|
+
const oldest = this.#map.keys().next().value;
|
|
5097
|
+
this.#map.delete(oldest);
|
|
5098
|
+
this.#evictions++;
|
|
5040
5099
|
}
|
|
5041
|
-
|
|
5100
|
+
this.#map.set(key, value2);
|
|
5042
5101
|
}
|
|
5043
5102
|
/** Removes a key from the cache. Returns true if it existed. */
|
|
5044
5103
|
delete(key) {
|
|
5045
|
-
return
|
|
5104
|
+
return this.#map.delete(key);
|
|
5046
5105
|
}
|
|
5047
5106
|
/** Returns true if the key is present (does NOT update LRU order). */
|
|
5048
5107
|
has(key) {
|
|
5049
|
-
return
|
|
5108
|
+
return this.#map.has(key);
|
|
5050
5109
|
}
|
|
5051
5110
|
/** Removes all entries and resets statistics. */
|
|
5052
5111
|
clear() {
|
|
5053
|
-
|
|
5054
|
-
|
|
5112
|
+
this.#map.clear();
|
|
5113
|
+
this.#hits = this.#misses = this.#evictions = 0;
|
|
5055
5114
|
}
|
|
5056
5115
|
/** Resizes the cache, evicting oldest entries if necessary. */
|
|
5057
5116
|
resize(newCapacity) {
|
|
5058
5117
|
if (newCapacity <= 0) throw new RangeError("Capacity must be > 0.");
|
|
5059
|
-
|
|
5060
|
-
while (
|
|
5061
|
-
const oldest =
|
|
5062
|
-
|
|
5063
|
-
|
|
5118
|
+
this.#capacity = newCapacity;
|
|
5119
|
+
while (this.#map.size > newCapacity) {
|
|
5120
|
+
const oldest = this.#map.keys().next().value;
|
|
5121
|
+
this.#map.delete(oldest);
|
|
5122
|
+
this.#evictions++;
|
|
5064
5123
|
}
|
|
5065
5124
|
}
|
|
5066
5125
|
};
|
|
5067
|
-
_map = new WeakMap();
|
|
5068
|
-
_capacity = new WeakMap();
|
|
5069
|
-
_hits = new WeakMap();
|
|
5070
|
-
_misses = new WeakMap();
|
|
5071
|
-
_evictions = new WeakMap();
|
|
5072
5126
|
|
|
5073
5127
|
// src/formula-evaluator.ts
|
|
5074
5128
|
var DEFAULT_CACHE_CAPACITY = 1e4;
|
|
@@ -5176,143 +5230,126 @@ var TILE_ROWS = 64;
|
|
|
5176
5230
|
var TILE_COLS = 64;
|
|
5177
5231
|
var MAX_TILES_PER_RANGE = 32;
|
|
5178
5232
|
var DEFAULT_MAX_CELLS = 2e6;
|
|
5179
|
-
var
|
|
5180
|
-
|
|
5233
|
+
var RangeCache = class _RangeCache {
|
|
5234
|
+
/** entry key → the memoised rectangle. Insertion-ordered (FIFO eviction). */
|
|
5235
|
+
#entries = /* @__PURE__ */ new Map();
|
|
5236
|
+
/** `sheet tr tc` → entry keys whose rectangle overlaps that tile. */
|
|
5237
|
+
#tiles = /* @__PURE__ */ new Map();
|
|
5238
|
+
/** sheet → entry keys whose rectangle is too broad to tile. */
|
|
5239
|
+
#wide = /* @__PURE__ */ new Map();
|
|
5240
|
+
/** entry key → buckets it was filed under, for cheap removal. */
|
|
5241
|
+
#placements = /* @__PURE__ */ new Map();
|
|
5242
|
+
/** Number of cells across all cached rectangles, kept under {@link #maxCells}. */
|
|
5243
|
+
#cells = 0;
|
|
5244
|
+
#maxCells;
|
|
5245
|
+
hits = 0;
|
|
5246
|
+
misses = 0;
|
|
5181
5247
|
constructor(maxCells = DEFAULT_MAX_CELLS) {
|
|
5182
|
-
|
|
5183
|
-
/** `SHEET r0 c0 r1 c1` → the memoised array value. Insertion-ordered (FIFO eviction). */
|
|
5184
|
-
__privateAdd(this, _entries, /* @__PURE__ */ new Map());
|
|
5185
|
-
/** `sheet tr tc` → entry keys whose rectangle overlaps that tile. */
|
|
5186
|
-
__privateAdd(this, _tiles, /* @__PURE__ */ new Map());
|
|
5187
|
-
/** sheet → entry keys whose rectangle is too broad to tile. */
|
|
5188
|
-
__privateAdd(this, _wide, /* @__PURE__ */ new Map());
|
|
5189
|
-
/** entry key → buckets it was filed under, for cheap removal. */
|
|
5190
|
-
__privateAdd(this, _placements, /* @__PURE__ */ new Map());
|
|
5191
|
-
/** entry key → its area, so eviction can give the budget back. */
|
|
5192
|
-
__privateAdd(this, _sizes, /* @__PURE__ */ new Map());
|
|
5193
|
-
/** Number of cells across all cached rectangles, kept under {@link #maxCells}. */
|
|
5194
|
-
__privateAdd(this, _cells, 0);
|
|
5195
|
-
__privateAdd(this, _maxCells);
|
|
5196
|
-
this.hits = 0;
|
|
5197
|
-
this.misses = 0;
|
|
5198
|
-
__privateSet(this, _maxCells, maxCells);
|
|
5248
|
+
this.#maxCells = maxCells;
|
|
5199
5249
|
}
|
|
5200
5250
|
get size() {
|
|
5201
|
-
return
|
|
5251
|
+
return this.#entries.size;
|
|
5202
5252
|
}
|
|
5203
5253
|
get cachedCells() {
|
|
5204
|
-
return
|
|
5254
|
+
return this.#cells;
|
|
5205
5255
|
}
|
|
5206
5256
|
static key(sheet, r0, c0, r1, c1) {
|
|
5207
5257
|
return `${sheet} ${r0} ${c0} ${r1} ${c1}`;
|
|
5208
5258
|
}
|
|
5209
5259
|
get(sheet, r0, c0, r1, c1) {
|
|
5210
|
-
const hit =
|
|
5260
|
+
const hit = this.#entries.get(_RangeCache.key(sheet, r0, c0, r1, c1));
|
|
5211
5261
|
if (hit === void 0) this.misses++;
|
|
5212
5262
|
else this.hits++;
|
|
5213
|
-
return hit;
|
|
5263
|
+
return hit?.value;
|
|
5214
5264
|
}
|
|
5215
5265
|
/** Memoises a rectangle's value. `cells` is its area, for the budget. */
|
|
5216
5266
|
set(sheet, r0, c0, r1, c1, value2, cells) {
|
|
5217
|
-
if (cells <= 0 || cells >
|
|
5267
|
+
if (cells <= 0 || cells > this.#maxCells) return;
|
|
5218
5268
|
const key = _RangeCache.key(sheet, r0, c0, r1, c1);
|
|
5219
|
-
if (
|
|
5220
|
-
|
|
5221
|
-
|
|
5269
|
+
if (this.#entries.has(key)) this.#drop(key);
|
|
5270
|
+
this.#entries.set(key, { value: value2, sheet, r0, c0, r1, c1, cells });
|
|
5271
|
+
this.#cells += cells;
|
|
5222
5272
|
const buckets = [];
|
|
5223
5273
|
const tileRow0 = r0 / TILE_ROWS | 0, tileRow1 = r1 / TILE_ROWS | 0;
|
|
5224
5274
|
const tileCol0 = c0 / TILE_COLS | 0, tileCol1 = c1 / TILE_COLS | 0;
|
|
5225
5275
|
const tileCount = (tileRow1 - tileRow0 + 1) * (tileCol1 - tileCol0 + 1);
|
|
5226
5276
|
if (tileCount > MAX_TILES_PER_RANGE) {
|
|
5227
|
-
|
|
5277
|
+
this.#fileIn(this.#wide, sheet, key, buckets);
|
|
5228
5278
|
} else {
|
|
5229
5279
|
for (let tr = tileRow0; tr <= tileRow1; tr++) {
|
|
5230
5280
|
for (let tc = tileCol0; tc <= tileCol1; tc++) {
|
|
5231
|
-
|
|
5232
|
-
__privateMethod(this, _RangeCache_instances, fileIn_fn).call(this, __privateGet(this, _tiles), bucket, key, buckets, `t ${bucket}`);
|
|
5281
|
+
this.#fileIn(this.#tiles, `${sheet} ${tr} ${tc}`, key, buckets);
|
|
5233
5282
|
}
|
|
5234
5283
|
}
|
|
5235
5284
|
}
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
if (__privateGet(this, _cells) <= __privateGet(this, _maxCells)) break;
|
|
5285
|
+
this.#placements.set(key, buckets);
|
|
5286
|
+
if (this.#cells > this.#maxCells) {
|
|
5287
|
+
for (const oldest of this.#entries.keys()) {
|
|
5288
|
+
if (this.#cells <= this.#maxCells) break;
|
|
5241
5289
|
if (oldest === key) continue;
|
|
5242
|
-
|
|
5290
|
+
this.#drop(oldest);
|
|
5243
5291
|
}
|
|
5244
5292
|
}
|
|
5245
5293
|
}
|
|
5246
|
-
/**
|
|
5294
|
+
/**
|
|
5295
|
+
* Drops every cached rectangle containing the given cell.
|
|
5296
|
+
*
|
|
5297
|
+
* The bucket sets are iterated in place. Removing an element of a `Set` while
|
|
5298
|
+
* iterating it is defined behaviour — the iterator simply does not revisit it —
|
|
5299
|
+
* and copying instead cost one array per write, which at one write per formula
|
|
5300
|
+
* was a real share of a recalculation.
|
|
5301
|
+
*/
|
|
5247
5302
|
invalidateCell(sheet, row2, col) {
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
if (
|
|
5303
|
+
const tile = this.#tiles.get(`${sheet} ${row2 / TILE_ROWS | 0} ${col / TILE_COLS | 0}`);
|
|
5304
|
+
if (tile !== void 0) {
|
|
5305
|
+
for (const key of tile) {
|
|
5306
|
+
const entry = this.#entries.get(key);
|
|
5307
|
+
if (entry !== void 0 && _RangeCache.#contains(entry, sheet, row2, col)) this.#drop(key);
|
|
5253
5308
|
}
|
|
5254
5309
|
}
|
|
5255
|
-
const wide =
|
|
5256
|
-
if (wide) {
|
|
5257
|
-
for (const key of
|
|
5258
|
-
|
|
5310
|
+
const wide = this.#wide.get(sheet);
|
|
5311
|
+
if (wide !== void 0) {
|
|
5312
|
+
for (const key of wide) {
|
|
5313
|
+
const entry = this.#entries.get(key);
|
|
5314
|
+
if (entry !== void 0 && _RangeCache.#contains(entry, sheet, row2, col)) this.#drop(key);
|
|
5259
5315
|
}
|
|
5260
5316
|
}
|
|
5261
5317
|
}
|
|
5262
5318
|
clear() {
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5319
|
+
this.#entries.clear();
|
|
5320
|
+
this.#tiles.clear();
|
|
5321
|
+
this.#wide.clear();
|
|
5322
|
+
this.#placements.clear();
|
|
5323
|
+
this.#cells = 0;
|
|
5324
|
+
}
|
|
5325
|
+
static #contains(entry, sheet, row2, col) {
|
|
5326
|
+
return row2 >= entry.r0 && row2 <= entry.r1 && col >= entry.c0 && col <= entry.c1 && entry.sheet === sheet;
|
|
5327
|
+
}
|
|
5328
|
+
#drop(key) {
|
|
5329
|
+
const entry = this.#entries.get(key);
|
|
5330
|
+
if (entry === void 0) return;
|
|
5331
|
+
this.#entries.delete(key);
|
|
5332
|
+
this.#cells -= entry.cells;
|
|
5333
|
+
const buckets = this.#placements.get(key);
|
|
5334
|
+
if (buckets !== void 0) {
|
|
5335
|
+
for (const { store, bucketKey, set } of buckets) {
|
|
5336
|
+
set.delete(key);
|
|
5337
|
+
if (set.size === 0) store.delete(bucketKey);
|
|
5338
|
+
}
|
|
5339
|
+
this.#placements.delete(key);
|
|
5340
|
+
}
|
|
5269
5341
|
}
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
_sizes = new WeakMap();
|
|
5276
|
-
_cells = new WeakMap();
|
|
5277
|
-
_maxCells = new WeakMap();
|
|
5278
|
-
_RangeCache_static = new WeakSet();
|
|
5279
|
-
contains_fn = function(key, sheet, row2, col) {
|
|
5280
|
-
const parts = key.split(" ");
|
|
5281
|
-
const n = parts.length;
|
|
5282
|
-
const c1 = +parts[n - 1], r1 = +parts[n - 2], c0 = +parts[n - 3], r0 = +parts[n - 4];
|
|
5283
|
-
if (parts.slice(0, n - 4).join(" ") !== sheet) return false;
|
|
5284
|
-
return row2 >= r0 && row2 <= r1 && col >= c0 && col <= c1;
|
|
5285
|
-
};
|
|
5286
|
-
_RangeCache_instances = new WeakSet();
|
|
5287
|
-
drop_fn = function(key) {
|
|
5288
|
-
if (!__privateGet(this, _entries).delete(key)) return;
|
|
5289
|
-
__privateSet(this, _cells, __privateGet(this, _cells) - (__privateGet(this, _sizes).get(key) ?? 0));
|
|
5290
|
-
__privateGet(this, _sizes).delete(key);
|
|
5291
|
-
const buckets = __privateGet(this, _placements).get(key);
|
|
5292
|
-
if (buckets) {
|
|
5293
|
-
for (const bucket of buckets) {
|
|
5294
|
-
const store = bucket.charCodeAt(0) === 119 ? __privateGet(this, _wide) : __privateGet(this, _tiles);
|
|
5295
|
-
const bucketKey = bucket.slice(2);
|
|
5296
|
-
const set = store.get(bucketKey);
|
|
5297
|
-
if (!set) continue;
|
|
5298
|
-
set.delete(key);
|
|
5299
|
-
if (set.size === 0) store.delete(bucketKey);
|
|
5342
|
+
#fileIn(store, bucketKey, entryKey, buckets) {
|
|
5343
|
+
let set = store.get(bucketKey);
|
|
5344
|
+
if (!set) {
|
|
5345
|
+
set = /* @__PURE__ */ new Set();
|
|
5346
|
+
store.set(bucketKey, set);
|
|
5300
5347
|
}
|
|
5301
|
-
|
|
5348
|
+
if (set.has(entryKey)) return;
|
|
5349
|
+
set.add(entryKey);
|
|
5350
|
+
buckets.push({ store, bucketKey, set });
|
|
5302
5351
|
}
|
|
5303
5352
|
};
|
|
5304
|
-
fileIn_fn = function(store, bucketKey, entryKey, buckets, bucketId) {
|
|
5305
|
-
let set = store.get(bucketKey);
|
|
5306
|
-
if (!set) {
|
|
5307
|
-
set = /* @__PURE__ */ new Set();
|
|
5308
|
-
store.set(bucketKey, set);
|
|
5309
|
-
}
|
|
5310
|
-
if (set.has(entryKey)) return;
|
|
5311
|
-
set.add(entryKey);
|
|
5312
|
-
buckets.push(bucketId);
|
|
5313
|
-
};
|
|
5314
|
-
__privateAdd(_RangeCache, _RangeCache_static);
|
|
5315
|
-
var RangeCache = _RangeCache;
|
|
5316
5353
|
function extractReferences(ast, formulaSheet, registry = FunctionRegistry.default, resolveName2) {
|
|
5317
5354
|
const refs = [];
|
|
5318
5355
|
let volatile = false;
|
|
@@ -5392,24 +5429,20 @@ var MAX_EXACT_SPAN = 16;
|
|
|
5392
5429
|
var MAX_BUCKETS_PER_REF = 64;
|
|
5393
5430
|
var tileOfRow = (row2) => row2 / TILE_ROWS2 | 0;
|
|
5394
5431
|
var tileOfCol = (col) => col / TILE_COLS2 | 0;
|
|
5395
|
-
var _buckets, _placements2, _DependencyIndex_instances, visit_fn, file_fn;
|
|
5396
5432
|
var DependencyIndex = class {
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
/** formula key → the buckets it was filed under, for cheap removal. */
|
|
5411
|
-
__privateAdd(this, _placements2, /* @__PURE__ */ new Map());
|
|
5412
|
-
}
|
|
5433
|
+
/**
|
|
5434
|
+
* All buckets in one map, keyed by a discriminating prefix:
|
|
5435
|
+
*
|
|
5436
|
+
* `t s tr tc` rectangle compact in both dimensions
|
|
5437
|
+
* `c s col tr` narrow columns, row tiles (the common case)
|
|
5438
|
+
* `C s col` narrow columns, every row (`A:A`)
|
|
5439
|
+
* `r s row tc` narrow rows, column tiles
|
|
5440
|
+
* `R s row` narrow rows, every column (`1:1`)
|
|
5441
|
+
* `w s` broad in both dimensions
|
|
5442
|
+
*/
|
|
5443
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
5444
|
+
/** formula key → the buckets it was filed under, for cheap removal. */
|
|
5445
|
+
#placements = /* @__PURE__ */ new Map();
|
|
5413
5446
|
/** Registers (or re-registers) a formula's precedent rectangles. */
|
|
5414
5447
|
add(formulaKey, refs) {
|
|
5415
5448
|
this.remove(formulaKey);
|
|
@@ -5426,12 +5459,12 @@ var DependencyIndex = class {
|
|
|
5426
5459
|
if (colSpan * rowTiles <= MAX_BUCKETS_PER_REF && rowTiles <= MAX_TILES_PER_REF) {
|
|
5427
5460
|
for (let col = ref.startCol; col <= ref.endCol; col++) {
|
|
5428
5461
|
for (let tr = rowTile0; tr <= rowTile1; tr++) {
|
|
5429
|
-
|
|
5462
|
+
this.#file(`c ${ref.sheet} ${col} ${tr}`, formulaKey, buckets);
|
|
5430
5463
|
}
|
|
5431
5464
|
}
|
|
5432
5465
|
} else {
|
|
5433
5466
|
for (let col = ref.startCol; col <= ref.endCol; col++) {
|
|
5434
|
-
|
|
5467
|
+
this.#file(`C ${ref.sheet} ${col}`, formulaKey, buckets);
|
|
5435
5468
|
}
|
|
5436
5469
|
}
|
|
5437
5470
|
continue;
|
|
@@ -5440,12 +5473,12 @@ var DependencyIndex = class {
|
|
|
5440
5473
|
if (rowSpan * colTiles <= MAX_BUCKETS_PER_REF && colTiles <= MAX_TILES_PER_REF) {
|
|
5441
5474
|
for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
|
|
5442
5475
|
for (let tc = colTile0; tc <= colTile1; tc++) {
|
|
5443
|
-
|
|
5476
|
+
this.#file(`r ${ref.sheet} ${row2} ${tc}`, formulaKey, buckets);
|
|
5444
5477
|
}
|
|
5445
5478
|
}
|
|
5446
5479
|
} else {
|
|
5447
5480
|
for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
|
|
5448
|
-
|
|
5481
|
+
this.#file(`R ${ref.sheet} ${row2}`, formulaKey, buckets);
|
|
5449
5482
|
}
|
|
5450
5483
|
}
|
|
5451
5484
|
continue;
|
|
@@ -5453,31 +5486,31 @@ var DependencyIndex = class {
|
|
|
5453
5486
|
if (rowTiles * colTiles <= MAX_TILES_PER_REF) {
|
|
5454
5487
|
for (let tr = rowTile0; tr <= rowTile1; tr++) {
|
|
5455
5488
|
for (let tc = colTile0; tc <= colTile1; tc++) {
|
|
5456
|
-
|
|
5489
|
+
this.#file(`t ${ref.sheet} ${tr} ${tc}`, formulaKey, buckets);
|
|
5457
5490
|
}
|
|
5458
5491
|
}
|
|
5459
5492
|
continue;
|
|
5460
5493
|
}
|
|
5461
|
-
|
|
5494
|
+
this.#file(`w ${ref.sheet}`, formulaKey, buckets);
|
|
5462
5495
|
}
|
|
5463
|
-
|
|
5496
|
+
this.#placements.set(formulaKey, buckets);
|
|
5464
5497
|
}
|
|
5465
5498
|
/** Forgets a formula. */
|
|
5466
5499
|
remove(formulaKey) {
|
|
5467
|
-
const buckets =
|
|
5500
|
+
const buckets = this.#placements.get(formulaKey);
|
|
5468
5501
|
if (!buckets) return;
|
|
5469
5502
|
for (const bucket of buckets) {
|
|
5470
|
-
const set =
|
|
5503
|
+
const set = this.#buckets.get(bucket);
|
|
5471
5504
|
if (!set) continue;
|
|
5472
5505
|
set.delete(formulaKey);
|
|
5473
|
-
if (set.size === 0)
|
|
5506
|
+
if (set.size === 0) this.#buckets.delete(bucket);
|
|
5474
5507
|
}
|
|
5475
|
-
|
|
5508
|
+
this.#placements.delete(formulaKey);
|
|
5476
5509
|
}
|
|
5477
5510
|
/** Drops every registration. */
|
|
5478
5511
|
clear() {
|
|
5479
|
-
|
|
5480
|
-
|
|
5512
|
+
this.#buckets.clear();
|
|
5513
|
+
this.#placements.clear();
|
|
5481
5514
|
}
|
|
5482
5515
|
/**
|
|
5483
5516
|
* Calls `visit` with the key of each formula that *may* read the given cell.
|
|
@@ -5489,31 +5522,28 @@ var DependencyIndex = class {
|
|
|
5489
5522
|
candidates(sheet, row2, col, visit) {
|
|
5490
5523
|
const tr = tileOfRow(row2);
|
|
5491
5524
|
const tc = tileOfCol(col);
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5525
|
+
this.#visit(`c ${sheet} ${col} ${tr}`, visit);
|
|
5526
|
+
this.#visit(`C ${sheet} ${col}`, visit);
|
|
5527
|
+
this.#visit(`r ${sheet} ${row2} ${tc}`, visit);
|
|
5528
|
+
this.#visit(`R ${sheet} ${row2}`, visit);
|
|
5529
|
+
this.#visit(`t ${sheet} ${tr} ${tc}`, visit);
|
|
5530
|
+
this.#visit(`w ${sheet}`, visit);
|
|
5531
|
+
}
|
|
5532
|
+
#visit(bucket, visit) {
|
|
5533
|
+
const set = this.#buckets.get(bucket);
|
|
5534
|
+
if (set) for (const key of set) visit(key);
|
|
5535
|
+
}
|
|
5536
|
+
#file(bucket, formulaKey, buckets) {
|
|
5537
|
+
let set = this.#buckets.get(bucket);
|
|
5538
|
+
if (!set) {
|
|
5539
|
+
set = /* @__PURE__ */ new Set();
|
|
5540
|
+
this.#buckets.set(bucket, set);
|
|
5541
|
+
}
|
|
5542
|
+
if (set.has(formulaKey)) return;
|
|
5543
|
+
set.add(formulaKey);
|
|
5544
|
+
buckets.push(bucket);
|
|
5498
5545
|
}
|
|
5499
5546
|
};
|
|
5500
|
-
_buckets = new WeakMap();
|
|
5501
|
-
_placements2 = new WeakMap();
|
|
5502
|
-
_DependencyIndex_instances = new WeakSet();
|
|
5503
|
-
visit_fn = function(bucket, visit) {
|
|
5504
|
-
const set = __privateGet(this, _buckets).get(bucket);
|
|
5505
|
-
if (set) for (const key of set) visit(key);
|
|
5506
|
-
};
|
|
5507
|
-
file_fn = function(bucket, formulaKey, buckets) {
|
|
5508
|
-
let set = __privateGet(this, _buckets).get(bucket);
|
|
5509
|
-
if (!set) {
|
|
5510
|
-
set = /* @__PURE__ */ new Set();
|
|
5511
|
-
__privateGet(this, _buckets).set(bucket, set);
|
|
5512
|
-
}
|
|
5513
|
-
if (set.has(formulaKey)) return;
|
|
5514
|
-
set.add(formulaKey);
|
|
5515
|
-
buckets.push(bucket);
|
|
5516
|
-
};
|
|
5517
5547
|
|
|
5518
5548
|
// src/recalc-engine.ts
|
|
5519
5549
|
function createWorkbookRecalcModel(workbook) {
|
|
@@ -5546,6 +5576,7 @@ function createWorkbookRecalcModel(workbook) {
|
|
|
5546
5576
|
};
|
|
5547
5577
|
}
|
|
5548
5578
|
var MAX_ADOPTED_SPILL_CELLS = 262144;
|
|
5579
|
+
var resultOf = (fv2) => fv2.isBlank ? 0 : fv2.toObject();
|
|
5549
5580
|
var normSheet = (s) => s.toUpperCase();
|
|
5550
5581
|
var normRef = (r) => r.replace(/\$/g, "").toUpperCase();
|
|
5551
5582
|
var keyOf = (sheet, ref) => `${normSheet(sheet)}!${normRef(ref)}`;
|
|
@@ -5556,42 +5587,70 @@ function parseKey(key) {
|
|
|
5556
5587
|
const { row: row2, column: column2 } = parseCellRef(ref);
|
|
5557
5588
|
return { sheet, ref, row: row2, col: column2 };
|
|
5558
5589
|
}
|
|
5559
|
-
var _model, _registry, _formulas, _sheetNames, _spills, _spillOwner, _dependents, _volatiles, _sheetLikes, _workbookLike, _rangeCache, _RecalcEngine_instances, writeCell_fn, register_fn, unregister_fn, rememberSheet_fn, adoptDeclaredSpill_fn, recalcInOrder_fn, applyShift_fn, shiftPrecedents_fn, shiftSpillBookkeeping_fn, recalcFrom_fn, runFixpoint_fn, addVolatiles_fn, dependentClosure_fn, forEachDirectDependent_fn, evaluatePass_fn, writeResult_fn, isOccupied_fn, clearSpill_fn, evaluateFormulaValue_fn, sheetLike_fn, buildSheetLike_fn, buildWorkbookLike_fn;
|
|
5560
5590
|
var RecalcEngine = class {
|
|
5591
|
+
#model;
|
|
5592
|
+
#registry;
|
|
5593
|
+
#formulas = /* @__PURE__ */ new Map();
|
|
5594
|
+
/** upper-cased sheet key → original casing, for case-correct model I/O. */
|
|
5595
|
+
#sheetNames = /* @__PURE__ */ new Map();
|
|
5596
|
+
/** anchor cellKey → set of spilled (non-anchor) cellKeys it currently owns. */
|
|
5597
|
+
#spills = /* @__PURE__ */ new Map();
|
|
5598
|
+
/** spilled cellKey → the anchor cellKey that owns it. */
|
|
5599
|
+
#spillOwner = /* @__PURE__ */ new Map();
|
|
5600
|
+
/**
|
|
5601
|
+
* Reverse dependency index, so finding the formulas that read a cell does not
|
|
5602
|
+
* mean testing every registered formula.
|
|
5603
|
+
*/
|
|
5604
|
+
#dependents = new DependencyIndex();
|
|
5605
|
+
/**
|
|
5606
|
+
* Keys of the volatile formulas. Maintained incrementally: scanning every
|
|
5607
|
+
* formula for volatility on each edit is O(F) per keystroke on its own.
|
|
5608
|
+
*/
|
|
5609
|
+
#volatiles = /* @__PURE__ */ new Set();
|
|
5610
|
+
/** Memoised per-sheet adapters — these are stateless w.r.t. the formula. */
|
|
5611
|
+
#sheetLikes = /* @__PURE__ */ new Map();
|
|
5612
|
+
#workbookLike;
|
|
5613
|
+
/**
|
|
5614
|
+
* Memo for range reads. The engine owns it because it owns the writes: every
|
|
5615
|
+
* value written goes through {@link #writeCell}, which drops the cached
|
|
5616
|
+
* rectangles containing that cell. Nothing else may write to the model behind
|
|
5617
|
+
* the engine's back without calling {@link onCellChanged} — that was already
|
|
5618
|
+
* true for dependency tracking, and now also keeps this memo honest.
|
|
5619
|
+
*/
|
|
5620
|
+
#rangeCache = new RangeCache();
|
|
5561
5621
|
constructor(model, registry = FunctionRegistry.default) {
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
__privateSet(this, _registry, registry);
|
|
5622
|
+
this.#model = model;
|
|
5623
|
+
this.#registry = registry;
|
|
5624
|
+
}
|
|
5625
|
+
/**
|
|
5626
|
+
* The single write path into the model. Writing a value invalidates the
|
|
5627
|
+
* cached rectangles that contain the cell, so a formula evaluated later in the
|
|
5628
|
+
* same pass cannot read a pre-write copy of a range. Coordinates are passed in
|
|
5629
|
+
* where the caller already knows them, to avoid re-parsing the reference.
|
|
5630
|
+
*/
|
|
5631
|
+
#writeCell(sheetName, ref, value2, row2, col) {
|
|
5632
|
+
this.#model.setCellValue(sheetName, ref, value2);
|
|
5633
|
+
if (row2 === void 0 || col === void 0) {
|
|
5634
|
+
const parsed = parseCellRef(ref);
|
|
5635
|
+
row2 = parsed.row;
|
|
5636
|
+
col = parsed.column;
|
|
5637
|
+
}
|
|
5638
|
+
this.#rangeCache.invalidateCell(normSheet(sheetName), row2, col);
|
|
5639
|
+
}
|
|
5640
|
+
/** Registers an entry in the map, the reverse index and the volatile set. */
|
|
5641
|
+
#register(entry) {
|
|
5642
|
+
this.#formulas.set(entry.key, entry);
|
|
5643
|
+
this.#dependents.add(entry.key, entry.refs);
|
|
5644
|
+
if (entry.volatile) this.#volatiles.add(entry.key);
|
|
5645
|
+
else this.#volatiles.delete(entry.key);
|
|
5646
|
+
}
|
|
5647
|
+
#unregister(key) {
|
|
5648
|
+
this.#formulas.delete(key);
|
|
5649
|
+
this.#dependents.remove(key);
|
|
5650
|
+
this.#volatiles.delete(key);
|
|
5651
|
+
}
|
|
5652
|
+
#rememberSheet(name) {
|
|
5653
|
+
this.#sheetNames.set(normSheet(name), name);
|
|
5595
5654
|
}
|
|
5596
5655
|
// ── Registration ─────────────────────────────────────────────────────────────
|
|
5597
5656
|
/**
|
|
@@ -5609,12 +5668,12 @@ var RecalcEngine = class {
|
|
|
5609
5668
|
const { refs, volatile } = extractReferences(
|
|
5610
5669
|
ast,
|
|
5611
5670
|
s,
|
|
5612
|
-
|
|
5613
|
-
|
|
5671
|
+
this.#registry,
|
|
5672
|
+
this.#model.getDefinedName ? (n, sh) => this.#model.getDefinedName(n, sh) : void 0
|
|
5614
5673
|
);
|
|
5615
|
-
|
|
5674
|
+
this.#rememberSheet(sheet);
|
|
5616
5675
|
const key = keyOf(s, r);
|
|
5617
|
-
|
|
5676
|
+
this.#register({
|
|
5618
5677
|
sheet: s,
|
|
5619
5678
|
sheetName: sheet,
|
|
5620
5679
|
ref: r,
|
|
@@ -5626,9 +5685,9 @@ var RecalcEngine = class {
|
|
|
5626
5685
|
refs,
|
|
5627
5686
|
volatile
|
|
5628
5687
|
});
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
return
|
|
5688
|
+
this.#model.setCellFormula?.(sheet, r, text);
|
|
5689
|
+
this.#rangeCache.invalidateCell(s, row2, column2);
|
|
5690
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }], key);
|
|
5632
5691
|
}
|
|
5633
5692
|
/**
|
|
5634
5693
|
* Indexes many formulas **without evaluating anything**.
|
|
@@ -5650,7 +5709,7 @@ var RecalcEngine = class {
|
|
|
5650
5709
|
* construct in a large workbook should not abort the load.
|
|
5651
5710
|
*/
|
|
5652
5711
|
registerBulk(entries) {
|
|
5653
|
-
const resolveName2 =
|
|
5712
|
+
const resolveName2 = this.#model.getDefinedName ? (n, sh) => this.#model.getDefinedName(n, sh) : void 0;
|
|
5654
5713
|
let registered = 0;
|
|
5655
5714
|
const failed = [];
|
|
5656
5715
|
for (const entry of entries) {
|
|
@@ -5662,10 +5721,10 @@ var RecalcEngine = class {
|
|
|
5662
5721
|
try {
|
|
5663
5722
|
const { row: row2, column: column2 } = parseCellRef(r);
|
|
5664
5723
|
const ast = parseFormulaUncached(text);
|
|
5665
|
-
const { refs, volatile } = extractReferences(ast, s,
|
|
5666
|
-
|
|
5724
|
+
const { refs, volatile } = extractReferences(ast, s, this.#registry, resolveName2);
|
|
5725
|
+
this.#rememberSheet(sheet);
|
|
5667
5726
|
const key = keyOf(s, r);
|
|
5668
|
-
|
|
5727
|
+
this.#register({
|
|
5669
5728
|
sheet: s,
|
|
5670
5729
|
sheetName: sheet,
|
|
5671
5730
|
ref: r,
|
|
@@ -5677,15 +5736,60 @@ var RecalcEngine = class {
|
|
|
5677
5736
|
refs,
|
|
5678
5737
|
volatile
|
|
5679
5738
|
});
|
|
5680
|
-
|
|
5739
|
+
this.#adoptDeclaredSpill(s, sheet, r, key, row2, column2);
|
|
5681
5740
|
registered++;
|
|
5682
5741
|
} catch (err) {
|
|
5683
5742
|
failed.push({ sheet, ref, error: err instanceof Error ? err.message : String(err) });
|
|
5684
5743
|
}
|
|
5685
5744
|
}
|
|
5686
|
-
if (registered > 0)
|
|
5745
|
+
if (registered > 0) this.#rangeCache.clear();
|
|
5687
5746
|
return { registered, failed };
|
|
5688
5747
|
}
|
|
5748
|
+
/**
|
|
5749
|
+
* Takes ownership of the spill block a *file* already declared for an anchor.
|
|
5750
|
+
*
|
|
5751
|
+
* Excel stores a dynamic array as one formula on the anchor plus the spilled
|
|
5752
|
+
* results written as plain cached values in the cells below/right, and records
|
|
5753
|
+
* the extent in the anchor's `<f t="array" ref="…">`. Without this step the
|
|
5754
|
+
* engine meets those cached values on the first recalculation, sees literal
|
|
5755
|
+
* content inside the block it wants to spill into, and reports `#SPILL!` for
|
|
5756
|
+
* every dynamic array in every real file. Registering the declared block as
|
|
5757
|
+
* this anchor's existing spill makes them what they actually are — the anchor's
|
|
5758
|
+
* own territory, free to overwrite — and reuses the ordinary shrink path, so a
|
|
5759
|
+
* result smaller than the declared block clears the leftovers.
|
|
5760
|
+
*
|
|
5761
|
+
* Cost is one Set entry per already-spilled cell, paid once at load. A declared
|
|
5762
|
+
* block far larger than any real spill (a corrupt or hand-written `ref`) is
|
|
5763
|
+
* skipped rather than materialised.
|
|
5764
|
+
*/
|
|
5765
|
+
#adoptDeclaredSpill(s, sheetName, ref, anchorKey, row2, col) {
|
|
5766
|
+
const declared = this.#model.getSpillRange?.(sheetName, ref);
|
|
5767
|
+
if (!declared || !declared.includes(":")) return;
|
|
5768
|
+
let rect;
|
|
5769
|
+
try {
|
|
5770
|
+
rect = parseRangeRef(declared);
|
|
5771
|
+
} catch {
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
if (rect.startRow !== row2 || rect.startColumn !== col) return;
|
|
5775
|
+
const height = rect.endRow - rect.startRow + 1;
|
|
5776
|
+
const width = rect.endColumn - rect.startColumn + 1;
|
|
5777
|
+
if (height < 1 || width < 1) return;
|
|
5778
|
+
if (height * width > MAX_ADOPTED_SPILL_CELLS) return;
|
|
5779
|
+
if (height === 1 && width === 1) return;
|
|
5780
|
+
const members = /* @__PURE__ */ new Set();
|
|
5781
|
+
for (let r = rect.startRow; r <= rect.endRow; r++) {
|
|
5782
|
+
for (let c = rect.startColumn; c <= rect.endColumn; c++) {
|
|
5783
|
+
if (r === row2 && c === col) continue;
|
|
5784
|
+
const key = keyOf(s, cellRefFromRowCol(r, c));
|
|
5785
|
+
if (this.#formulas.has(key)) continue;
|
|
5786
|
+
if (this.#spillOwner.has(key)) continue;
|
|
5787
|
+
members.add(key);
|
|
5788
|
+
this.#spillOwner.set(key, anchorKey);
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
if (members.size > 0) this.#spills.set(anchorKey, members);
|
|
5792
|
+
}
|
|
5689
5793
|
/**
|
|
5690
5794
|
* Removes a formula from a cell and recalculates its dependents (which may now
|
|
5691
5795
|
* read a plain input value instead). Returns the recomputed dependent cells.
|
|
@@ -5694,36 +5798,36 @@ var RecalcEngine = class {
|
|
|
5694
5798
|
const s = normSheet(sheet);
|
|
5695
5799
|
const r = normRef(ref);
|
|
5696
5800
|
const key = keyOf(s, r);
|
|
5697
|
-
if (!
|
|
5698
|
-
|
|
5699
|
-
|
|
5801
|
+
if (!this.#formulas.has(key)) return [];
|
|
5802
|
+
this.#unregister(key);
|
|
5803
|
+
this.#model.clearCell?.(sheet, r);
|
|
5700
5804
|
const { row: row2, column: column2 } = parseCellRef(r);
|
|
5701
|
-
|
|
5702
|
-
return
|
|
5805
|
+
this.#rangeCache.invalidateCell(s, row2, column2);
|
|
5806
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }]);
|
|
5703
5807
|
}
|
|
5704
5808
|
/**
|
|
5705
5809
|
* Writes a raw input value to a cell and recalculates every formula that
|
|
5706
5810
|
* depends on it. Use this for non-formula edits so dependents stay current.
|
|
5707
5811
|
*/
|
|
5708
5812
|
setCellValue(sheet, ref, value2) {
|
|
5709
|
-
|
|
5813
|
+
this.#rememberSheet(sheet);
|
|
5710
5814
|
const s = normSheet(sheet);
|
|
5711
5815
|
const r = normRef(ref);
|
|
5712
5816
|
const { row: row2, column: column2 } = parseCellRef(r);
|
|
5713
|
-
|
|
5714
|
-
return
|
|
5817
|
+
this.#writeCell(sheet, r, value2, row2, column2);
|
|
5818
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }]);
|
|
5715
5819
|
}
|
|
5716
5820
|
/**
|
|
5717
5821
|
* Signals that a cell changed by some external means (the model was already
|
|
5718
5822
|
* updated) and recalculates its dependents.
|
|
5719
5823
|
*/
|
|
5720
5824
|
onCellChanged(sheet, ref) {
|
|
5721
|
-
|
|
5825
|
+
this.#rememberSheet(sheet);
|
|
5722
5826
|
const s = normSheet(sheet);
|
|
5723
5827
|
const r = normRef(ref);
|
|
5724
5828
|
const { row: row2, column: column2 } = parseCellRef(r);
|
|
5725
|
-
|
|
5726
|
-
return
|
|
5829
|
+
this.#rangeCache.invalidateCell(s, row2, column2);
|
|
5830
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }]);
|
|
5727
5831
|
}
|
|
5728
5832
|
/**
|
|
5729
5833
|
* Recalculates every registered formula.
|
|
@@ -5756,18 +5860,61 @@ var RecalcEngine = class {
|
|
|
5756
5860
|
recalcAll(options) {
|
|
5757
5861
|
const order = options?.order;
|
|
5758
5862
|
if (!order || order.length === 0) {
|
|
5759
|
-
return
|
|
5863
|
+
return this.#evaluatePass([...this.#formulas.values()]).results;
|
|
5760
5864
|
}
|
|
5761
|
-
const attempt =
|
|
5865
|
+
const attempt = this.#recalcInOrder(order);
|
|
5762
5866
|
if (attempt.conflict) {
|
|
5763
5867
|
options?.onOrderConflict?.(attempt.conflict);
|
|
5764
|
-
return
|
|
5868
|
+
return this.#evaluatePass([...this.#formulas.values()]).results;
|
|
5765
5869
|
}
|
|
5766
5870
|
return attempt.results;
|
|
5767
5871
|
}
|
|
5872
|
+
/**
|
|
5873
|
+
* One pass in a caller-supplied order, watching for inversions.
|
|
5874
|
+
*
|
|
5875
|
+
* The check is the mirror image of the topological sort: instead of asking
|
|
5876
|
+
* "which of my precedents come first?", it asks, of each formula just
|
|
5877
|
+
* evaluated, "has anything that reads me already run?". The dependency index
|
|
5878
|
+
* answers that directly, and one positive answer is enough to know the order
|
|
5879
|
+
* cannot be trusted.
|
|
5880
|
+
*/
|
|
5881
|
+
#recalcInOrder(order) {
|
|
5882
|
+
const results = [];
|
|
5883
|
+
const changed = [];
|
|
5884
|
+
const done = /* @__PURE__ */ new Set();
|
|
5885
|
+
let spilled = false;
|
|
5886
|
+
for (const { sheet, ref } of order) {
|
|
5887
|
+
const key = keyOf(sheet, ref);
|
|
5888
|
+
if (done.has(key)) continue;
|
|
5889
|
+
const entry = this.#formulas.get(key);
|
|
5890
|
+
if (!entry) continue;
|
|
5891
|
+
done.add(key);
|
|
5892
|
+
const written = this.#writeResult(entry);
|
|
5893
|
+
results.push(written.result);
|
|
5894
|
+
changed.push(...written.changed);
|
|
5895
|
+
if (written.result.spill) spilled = true;
|
|
5896
|
+
let conflict;
|
|
5897
|
+
this.#forEachDirectDependent({ sheet: entry.sheet, row: entry.row, col: entry.col }, (dep) => {
|
|
5898
|
+
if (conflict || dep.key === entry.key) return;
|
|
5899
|
+
if (done.has(dep.key)) conflict = { dependent: dep.key, precedent: entry.key };
|
|
5900
|
+
});
|
|
5901
|
+
if (conflict) return { results: [], conflict };
|
|
5902
|
+
}
|
|
5903
|
+
const rest = [...this.#formulas.values()].filter((e) => !done.has(e.key));
|
|
5904
|
+
if (rest.length > 0) {
|
|
5905
|
+
const pass = this.#evaluatePass(rest);
|
|
5906
|
+
results.push(...pass.results);
|
|
5907
|
+
changed.push(...pass.changed);
|
|
5908
|
+
}
|
|
5909
|
+
if (spilled) {
|
|
5910
|
+
const evaluated = new Set(done);
|
|
5911
|
+
results.push(...this.#runFixpoint(this.#dependentClosure(changed, evaluated), evaluated));
|
|
5912
|
+
}
|
|
5913
|
+
return { results };
|
|
5914
|
+
}
|
|
5768
5915
|
/** The precedents recorded for a formula cell (mainly for tests/inspection). */
|
|
5769
5916
|
getPrecedents(sheet, ref) {
|
|
5770
|
-
return
|
|
5917
|
+
return this.#formulas.get(keyOf(sheet, ref))?.refs ?? [];
|
|
5771
5918
|
}
|
|
5772
5919
|
// ── Structural edits ─────────────────────────────────────────────────────────
|
|
5773
5920
|
/**
|
|
@@ -5782,7 +5929,7 @@ var RecalcEngine = class {
|
|
|
5782
5929
|
* engine.onRowsInserted('S1', 3, 2);
|
|
5783
5930
|
*/
|
|
5784
5931
|
onRowsInserted(sheet, at, count2 = 1) {
|
|
5785
|
-
return
|
|
5932
|
+
return this.#applyShift({ dim: "row", kind: "insert", at, count: count2, sheetName: sheet });
|
|
5786
5933
|
}
|
|
5787
5934
|
/**
|
|
5788
5935
|
* Tells the engine rows were deleted on a sheet. Call AFTER the
|
|
@@ -5792,556 +5939,420 @@ var RecalcEngine = class {
|
|
|
5792
5939
|
* volatiles) are recomputed.
|
|
5793
5940
|
*/
|
|
5794
5941
|
onRowsDeleted(sheet, at, count2 = 1) {
|
|
5795
|
-
return
|
|
5942
|
+
return this.#applyShift({ dim: "row", kind: "delete", at, count: count2, sheetName: sheet });
|
|
5796
5943
|
}
|
|
5797
5944
|
/** Column counterpart to {@link onRowsInserted}. */
|
|
5798
5945
|
onColumnsInserted(sheet, at, count2 = 1) {
|
|
5799
|
-
return
|
|
5946
|
+
return this.#applyShift({ dim: "col", kind: "insert", at, count: count2, sheetName: sheet });
|
|
5800
5947
|
}
|
|
5801
5948
|
/** Column counterpart to {@link onRowsDeleted}. */
|
|
5802
5949
|
onColumnsDeleted(sheet, at, count2 = 1) {
|
|
5803
|
-
return
|
|
5804
|
-
}
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
else __privateGet(this, _volatiles).delete(entry.key);
|
|
5839
|
-
};
|
|
5840
|
-
unregister_fn = function(key) {
|
|
5841
|
-
__privateGet(this, _formulas).delete(key);
|
|
5842
|
-
__privateGet(this, _dependents).remove(key);
|
|
5843
|
-
__privateGet(this, _volatiles).delete(key);
|
|
5844
|
-
};
|
|
5845
|
-
rememberSheet_fn = function(name) {
|
|
5846
|
-
__privateGet(this, _sheetNames).set(normSheet(name), name);
|
|
5847
|
-
};
|
|
5848
|
-
/**
|
|
5849
|
-
* Takes ownership of the spill block a *file* already declared for an anchor.
|
|
5850
|
-
*
|
|
5851
|
-
* Excel stores a dynamic array as one formula on the anchor plus the spilled
|
|
5852
|
-
* results written as plain cached values in the cells below/right, and records
|
|
5853
|
-
* the extent in the anchor's `<f t="array" ref="…">`. Without this step the
|
|
5854
|
-
* engine meets those cached values on the first recalculation, sees literal
|
|
5855
|
-
* content inside the block it wants to spill into, and reports `#SPILL!` for
|
|
5856
|
-
* every dynamic array in every real file. Registering the declared block as
|
|
5857
|
-
* this anchor's existing spill makes them what they actually are — the anchor's
|
|
5858
|
-
* own territory, free to overwrite — and reuses the ordinary shrink path, so a
|
|
5859
|
-
* result smaller than the declared block clears the leftovers.
|
|
5860
|
-
*
|
|
5861
|
-
* Cost is one Set entry per already-spilled cell, paid once at load. A declared
|
|
5862
|
-
* block far larger than any real spill (a corrupt or hand-written `ref`) is
|
|
5863
|
-
* skipped rather than materialised.
|
|
5864
|
-
*/
|
|
5865
|
-
adoptDeclaredSpill_fn = function(s, sheetName, ref, anchorKey, row2, col) {
|
|
5866
|
-
const declared = __privateGet(this, _model).getSpillRange?.(sheetName, ref);
|
|
5867
|
-
if (!declared || !declared.includes(":")) return;
|
|
5868
|
-
let rect;
|
|
5869
|
-
try {
|
|
5870
|
-
rect = parseRangeRef(declared);
|
|
5871
|
-
} catch {
|
|
5872
|
-
return;
|
|
5873
|
-
}
|
|
5874
|
-
if (rect.startRow !== row2 || rect.startColumn !== col) return;
|
|
5875
|
-
const height = rect.endRow - rect.startRow + 1;
|
|
5876
|
-
const width = rect.endColumn - rect.startColumn + 1;
|
|
5877
|
-
if (height < 1 || width < 1) return;
|
|
5878
|
-
if (height * width > MAX_ADOPTED_SPILL_CELLS) return;
|
|
5879
|
-
if (height === 1 && width === 1) return;
|
|
5880
|
-
const members = /* @__PURE__ */ new Set();
|
|
5881
|
-
for (let r = rect.startRow; r <= rect.endRow; r++) {
|
|
5882
|
-
for (let c = rect.startColumn; c <= rect.endColumn; c++) {
|
|
5883
|
-
if (r === row2 && c === col) continue;
|
|
5884
|
-
const key = keyOf(s, cellRefFromRowCol(r, c));
|
|
5885
|
-
if (__privateGet(this, _formulas).has(key)) continue;
|
|
5886
|
-
if (__privateGet(this, _spillOwner).has(key)) continue;
|
|
5887
|
-
members.add(key);
|
|
5888
|
-
__privateGet(this, _spillOwner).set(key, anchorKey);
|
|
5889
|
-
}
|
|
5890
|
-
}
|
|
5891
|
-
if (members.size > 0) __privateGet(this, _spills).set(anchorKey, members);
|
|
5892
|
-
};
|
|
5893
|
-
/**
|
|
5894
|
-
* One pass in a caller-supplied order, watching for inversions.
|
|
5895
|
-
*
|
|
5896
|
-
* The check is the mirror image of the topological sort: instead of asking
|
|
5897
|
-
* "which of my precedents come first?", it asks, of each formula just
|
|
5898
|
-
* evaluated, "has anything that reads me already run?". The dependency index
|
|
5899
|
-
* answers that directly, and one positive answer is enough to know the order
|
|
5900
|
-
* cannot be trusted.
|
|
5901
|
-
*/
|
|
5902
|
-
recalcInOrder_fn = function(order) {
|
|
5903
|
-
const results = [];
|
|
5904
|
-
const changed = [];
|
|
5905
|
-
const done = /* @__PURE__ */ new Set();
|
|
5906
|
-
let spilled = false;
|
|
5907
|
-
for (const { sheet, ref } of order) {
|
|
5908
|
-
const key = keyOf(sheet, ref);
|
|
5909
|
-
if (done.has(key)) continue;
|
|
5910
|
-
const entry = __privateGet(this, _formulas).get(key);
|
|
5911
|
-
if (!entry) continue;
|
|
5912
|
-
done.add(key);
|
|
5913
|
-
const written = __privateMethod(this, _RecalcEngine_instances, writeResult_fn).call(this, entry);
|
|
5914
|
-
results.push(written.result);
|
|
5915
|
-
changed.push(...written.changed);
|
|
5916
|
-
if (written.result.spill) spilled = true;
|
|
5917
|
-
let conflict;
|
|
5918
|
-
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: entry.sheet, row: entry.row, col: entry.col }, (dep) => {
|
|
5919
|
-
if (conflict || dep.key === entry.key) return;
|
|
5920
|
-
if (done.has(dep.key)) conflict = { dependent: dep.key, precedent: entry.key };
|
|
5921
|
-
});
|
|
5922
|
-
if (conflict) return { results: [], conflict };
|
|
5923
|
-
}
|
|
5924
|
-
const rest = [...__privateGet(this, _formulas).values()].filter((e) => !done.has(e.key));
|
|
5925
|
-
if (rest.length > 0) {
|
|
5926
|
-
const pass = __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, rest);
|
|
5927
|
-
results.push(...pass.results);
|
|
5928
|
-
changed.push(...pass.changed);
|
|
5929
|
-
}
|
|
5930
|
-
if (spilled) {
|
|
5931
|
-
const evaluated = new Set(done);
|
|
5932
|
-
results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, changed, evaluated), evaluated));
|
|
5933
|
-
}
|
|
5934
|
-
return { results };
|
|
5935
|
-
};
|
|
5936
|
-
applyShift_fn = function(shift) {
|
|
5937
|
-
const editedSheet = normSheet(shift.sheetName);
|
|
5938
|
-
const isRowShift = shift.dim === "row";
|
|
5939
|
-
const bandEnd = shift.at + shift.count - 1;
|
|
5940
|
-
const rekeyed = [];
|
|
5941
|
-
const needsRecalc = /* @__PURE__ */ new Set();
|
|
5942
|
-
for (const e of __privateGet(this, _formulas).values()) {
|
|
5943
|
-
let row2 = e.row;
|
|
5944
|
-
let col = e.col;
|
|
5945
|
-
if (e.sheet === editedSheet) {
|
|
5946
|
-
const shifted = shiftIndex(isRowShift ? row2 : col, shift);
|
|
5947
|
-
if (shifted === null) continue;
|
|
5948
|
-
if (isRowShift) row2 = shifted;
|
|
5949
|
-
else col = shifted;
|
|
5950
|
-
}
|
|
5951
|
-
const touchedBand = shift.kind === "delete" && e.refs.some((r) => r.sheet === editedSheet && (isRowShift ? r.endRow >= shift.at && r.startRow <= bandEnd : r.endCol >= shift.at && r.startCol <= bandEnd));
|
|
5952
|
-
const rewritten = shiftFormulaRefs(e.formula, shift, e.sheetName);
|
|
5953
|
-
const moved = row2 !== e.row || col !== e.col;
|
|
5954
|
-
const ref = moved ? cellRefFromRowCol(row2, col) : e.ref;
|
|
5955
|
-
const key = moved ? keyOf(e.sheet, ref) : e.key;
|
|
5956
|
-
rekeyed.push({
|
|
5957
|
-
...e,
|
|
5958
|
-
ref,
|
|
5959
|
-
key,
|
|
5960
|
-
row: row2,
|
|
5961
|
-
col,
|
|
5962
|
-
formula: rewritten.changed ? rewritten.text : e.formula,
|
|
5963
|
-
// A rewritten formula's cached AST no longer matches its text; it is
|
|
5964
|
-
// re-parsed lazily, and only if the cell is actually evaluated again.
|
|
5965
|
-
ast: rewritten.changed ? void 0 : e.ast,
|
|
5966
|
-
refs: __privateMethod(this, _RecalcEngine_instances, shiftPrecedents_fn).call(this, e.refs, shift, editedSheet)
|
|
5967
|
-
// Volatility is a property of which *functions* a formula calls, and
|
|
5968
|
-
// rewriting references cannot change that.
|
|
5969
|
-
});
|
|
5970
|
-
if (touchedBand || rewritten.hasRefError) needsRecalc.add(key);
|
|
5971
|
-
}
|
|
5972
|
-
__privateSet(this, _formulas, /* @__PURE__ */ new Map());
|
|
5973
|
-
__privateGet(this, _dependents).clear();
|
|
5974
|
-
__privateGet(this, _volatiles).clear();
|
|
5975
|
-
for (const e of rekeyed) __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, e);
|
|
5976
|
-
__privateMethod(this, _RecalcEngine_instances, shiftSpillBookkeeping_fn).call(this, shift, editedSheet);
|
|
5977
|
-
__privateGet(this, _rangeCache).clear();
|
|
5978
|
-
const pending = /* @__PURE__ */ new Map();
|
|
5979
|
-
for (const k of needsRecalc) {
|
|
5980
|
-
const e = __privateGet(this, _formulas).get(k);
|
|
5981
|
-
if (e) pending.set(k, e);
|
|
5982
|
-
}
|
|
5983
|
-
for (const e of __privateGet(this, _formulas).values()) {
|
|
5984
|
-
if (e.volatile) pending.set(e.key, e);
|
|
5985
|
-
}
|
|
5986
|
-
return __privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, /* @__PURE__ */ new Set());
|
|
5987
|
-
};
|
|
5988
|
-
/**
|
|
5989
|
-
* Translates precedent rectangles across a structural edit.
|
|
5990
|
-
*
|
|
5991
|
-
* This is arithmetic rather than a re-parse of the rewritten formula, and the
|
|
5992
|
-
* two are equivalent by construction: `shiftFormulaRefs` derives the new
|
|
5993
|
-
* reference text by calling `shiftSpan`/`shiftIndex` on exactly these numbers,
|
|
5994
|
-
* so applying the same functions to the stored rectangle yields the same
|
|
5995
|
-
* region. A rectangle that is entirely deleted disappears, matching the
|
|
5996
|
-
* `#REF!` the text rewrite produces.
|
|
5997
|
-
*/
|
|
5998
|
-
shiftPrecedents_fn = function(refs, shift, editedSheet) {
|
|
5999
|
-
const out = [];
|
|
6000
|
-
for (const r of refs) {
|
|
6001
|
-
if (r.sheet !== editedSheet) {
|
|
6002
|
-
out.push(r);
|
|
6003
|
-
continue;
|
|
6004
|
-
}
|
|
6005
|
-
if (shift.dim === "row") {
|
|
6006
|
-
const span = shiftSpan(r.startRow, r.endRow, shift);
|
|
6007
|
-
if (!span) continue;
|
|
6008
|
-
out.push({
|
|
6009
|
-
sheet: r.sheet,
|
|
6010
|
-
startRow: span.start,
|
|
6011
|
-
endRow: Math.min(span.end, EXCEL_MAX_ROWS),
|
|
6012
|
-
startCol: r.startCol,
|
|
6013
|
-
endCol: r.endCol
|
|
6014
|
-
});
|
|
6015
|
-
} else {
|
|
6016
|
-
const span = shiftSpan(r.startCol, r.endCol, shift);
|
|
6017
|
-
if (!span) continue;
|
|
6018
|
-
out.push({
|
|
6019
|
-
sheet: r.sheet,
|
|
6020
|
-
startRow: r.startRow,
|
|
6021
|
-
endRow: r.endRow,
|
|
6022
|
-
startCol: span.start,
|
|
6023
|
-
endCol: Math.min(span.end, EXCEL_MAX_COLUMNS)
|
|
5950
|
+
return this.#applyShift({ dim: "col", kind: "delete", at, count: count2, sheetName: sheet });
|
|
5951
|
+
}
|
|
5952
|
+
#applyShift(shift) {
|
|
5953
|
+
const editedSheet = normSheet(shift.sheetName);
|
|
5954
|
+
const isRowShift = shift.dim === "row";
|
|
5955
|
+
const bandEnd = shift.at + shift.count - 1;
|
|
5956
|
+
const rekeyed = [];
|
|
5957
|
+
const needsRecalc = /* @__PURE__ */ new Set();
|
|
5958
|
+
for (const e of this.#formulas.values()) {
|
|
5959
|
+
let row2 = e.row;
|
|
5960
|
+
let col = e.col;
|
|
5961
|
+
if (e.sheet === editedSheet) {
|
|
5962
|
+
const shifted = shiftIndex(isRowShift ? row2 : col, shift);
|
|
5963
|
+
if (shifted === null) continue;
|
|
5964
|
+
if (isRowShift) row2 = shifted;
|
|
5965
|
+
else col = shifted;
|
|
5966
|
+
}
|
|
5967
|
+
const touchedBand = shift.kind === "delete" && e.refs.some((r) => r.sheet === editedSheet && (isRowShift ? r.endRow >= shift.at && r.startRow <= bandEnd : r.endCol >= shift.at && r.startCol <= bandEnd));
|
|
5968
|
+
const rewritten = shiftFormulaRefs(e.formula, shift, e.sheetName);
|
|
5969
|
+
const moved = row2 !== e.row || col !== e.col;
|
|
5970
|
+
const ref = moved ? cellRefFromRowCol(row2, col) : e.ref;
|
|
5971
|
+
const key = moved ? keyOf(e.sheet, ref) : e.key;
|
|
5972
|
+
rekeyed.push({
|
|
5973
|
+
...e,
|
|
5974
|
+
ref,
|
|
5975
|
+
key,
|
|
5976
|
+
row: row2,
|
|
5977
|
+
col,
|
|
5978
|
+
formula: rewritten.changed ? rewritten.text : e.formula,
|
|
5979
|
+
// A rewritten formula's cached AST no longer matches its text; it is
|
|
5980
|
+
// re-parsed lazily, and only if the cell is actually evaluated again.
|
|
5981
|
+
ast: rewritten.changed ? void 0 : e.ast,
|
|
5982
|
+
refs: this.#shiftPrecedents(e.refs, shift, editedSheet)
|
|
5983
|
+
// Volatility is a property of which *functions* a formula calls, and
|
|
5984
|
+
// rewriting references cannot change that.
|
|
6024
5985
|
});
|
|
5986
|
+
if (touchedBand || rewritten.hasRefError) needsRecalc.add(key);
|
|
5987
|
+
}
|
|
5988
|
+
this.#formulas = /* @__PURE__ */ new Map();
|
|
5989
|
+
this.#dependents.clear();
|
|
5990
|
+
this.#volatiles.clear();
|
|
5991
|
+
for (const e of rekeyed) this.#register(e);
|
|
5992
|
+
this.#shiftSpillBookkeeping(shift, editedSheet);
|
|
5993
|
+
this.#rangeCache.clear();
|
|
5994
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5995
|
+
for (const k of needsRecalc) {
|
|
5996
|
+
const e = this.#formulas.get(k);
|
|
5997
|
+
if (e) pending.set(k, e);
|
|
6025
5998
|
}
|
|
5999
|
+
for (const e of this.#formulas.values()) {
|
|
6000
|
+
if (e.volatile) pending.set(e.key, e);
|
|
6001
|
+
}
|
|
6002
|
+
return this.#runFixpoint(pending, /* @__PURE__ */ new Set());
|
|
6026
6003
|
}
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6004
|
+
/**
|
|
6005
|
+
* Translates precedent rectangles across a structural edit.
|
|
6006
|
+
*
|
|
6007
|
+
* This is arithmetic rather than a re-parse of the rewritten formula, and the
|
|
6008
|
+
* two are equivalent by construction: `shiftFormulaRefs` derives the new
|
|
6009
|
+
* reference text by calling `shiftSpan`/`shiftIndex` on exactly these numbers,
|
|
6010
|
+
* so applying the same functions to the stored rectangle yields the same
|
|
6011
|
+
* region. A rectangle that is entirely deleted disappears, matching the
|
|
6012
|
+
* `#REF!` the text rewrite produces.
|
|
6013
|
+
*/
|
|
6014
|
+
#shiftPrecedents(refs, shift, editedSheet) {
|
|
6015
|
+
const out = [];
|
|
6016
|
+
for (const r of refs) {
|
|
6017
|
+
if (r.sheet !== editedSheet) {
|
|
6018
|
+
out.push(r);
|
|
6019
|
+
continue;
|
|
6020
|
+
}
|
|
6021
|
+
if (shift.dim === "row") {
|
|
6022
|
+
const span = shiftSpan(r.startRow, r.endRow, shift);
|
|
6023
|
+
if (!span) continue;
|
|
6024
|
+
out.push({
|
|
6025
|
+
sheet: r.sheet,
|
|
6026
|
+
startRow: span.start,
|
|
6027
|
+
endRow: Math.min(span.end, EXCEL_MAX_ROWS),
|
|
6028
|
+
startCol: r.startCol,
|
|
6029
|
+
endCol: r.endCol
|
|
6030
|
+
});
|
|
6031
|
+
} else {
|
|
6032
|
+
const span = shiftSpan(r.startCol, r.endCol, shift);
|
|
6033
|
+
if (!span) continue;
|
|
6034
|
+
out.push({
|
|
6035
|
+
sheet: r.sheet,
|
|
6036
|
+
startRow: r.startRow,
|
|
6037
|
+
endRow: r.endRow,
|
|
6038
|
+
startCol: span.start,
|
|
6039
|
+
endCol: Math.min(span.end, EXCEL_MAX_COLUMNS)
|
|
6040
|
+
});
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
return out;
|
|
6044
|
+
}
|
|
6045
|
+
/** Translates spill ownership across a structural edit on `editedSheet`. */
|
|
6046
|
+
#shiftSpillBookkeeping(shift, editedSheet) {
|
|
6047
|
+
const shiftCellKey = (key) => {
|
|
6048
|
+
const { sheet, row: row2, col } = parseKey(key);
|
|
6049
|
+
if (sheet !== editedSheet) return key;
|
|
6050
|
+
const r = shift.dim === "row" ? shiftIndex(row2, shift) : row2;
|
|
6051
|
+
const c = shift.dim === "col" ? shiftIndex(col, shift) : col;
|
|
6052
|
+
if (r === null || c === null) return null;
|
|
6053
|
+
return keyOf(sheet, cellRefFromRowCol(r, c));
|
|
6054
|
+
};
|
|
6055
|
+
const newSpills = /* @__PURE__ */ new Map();
|
|
6056
|
+
this.#spillOwner.clear();
|
|
6057
|
+
for (const [anchor, members] of this.#spills) {
|
|
6058
|
+
const newAnchor = shiftCellKey(anchor);
|
|
6059
|
+
if (newAnchor === null) {
|
|
6060
|
+
for (const m of members) {
|
|
6061
|
+
const shifted = shiftCellKey(m);
|
|
6062
|
+
if (!shifted) continue;
|
|
6063
|
+
const cell = parseKey(shifted);
|
|
6064
|
+
this.#writeCell(this.#sheetNames.get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
6065
|
+
}
|
|
6066
|
+
continue;
|
|
6067
|
+
}
|
|
6068
|
+
const newMembers = /* @__PURE__ */ new Set();
|
|
6044
6069
|
for (const m of members) {
|
|
6045
6070
|
const shifted = shiftCellKey(m);
|
|
6046
6071
|
if (!shifted) continue;
|
|
6047
|
-
|
|
6048
|
-
|
|
6072
|
+
newMembers.add(shifted);
|
|
6073
|
+
this.#spillOwner.set(shifted, newAnchor);
|
|
6049
6074
|
}
|
|
6050
|
-
|
|
6075
|
+
newSpills.set(newAnchor, newMembers);
|
|
6051
6076
|
}
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6077
|
+
this.#spills = newSpills;
|
|
6078
|
+
}
|
|
6079
|
+
// ── Internals ────────────────────────────────────────────────────────────────
|
|
6080
|
+
/**
|
|
6081
|
+
* Recomputes formulas affected by the given changed cells, to a fixpoint.
|
|
6082
|
+
*
|
|
6083
|
+
* A single topological pass is not enough once dynamic arrays exist: a
|
|
6084
|
+
* formula that spills changes cells the graph could not know about until the
|
|
6085
|
+
* array was computed, and those cells may have their own dependents. So we
|
|
6086
|
+
* evaluate a pass, collect every cell whose value actually changed (formula
|
|
6087
|
+
* outputs plus spilled/cleared cells), then recompute the dependents of those
|
|
6088
|
+
* cells, repeating until nothing new is affected (bounded for safety).
|
|
6089
|
+
*/
|
|
6090
|
+
#recalcFrom(seeds, includeKey) {
|
|
6091
|
+
const results = [];
|
|
6092
|
+
const evaluated = /* @__PURE__ */ new Set();
|
|
6093
|
+
let pending = this.#dependentClosure(seeds, evaluated);
|
|
6094
|
+
this.#addVolatiles(pending);
|
|
6095
|
+
if (includeKey) {
|
|
6096
|
+
const self = this.#formulas.get(includeKey);
|
|
6097
|
+
if (self) pending.set(includeKey, self);
|
|
6098
|
+
}
|
|
6099
|
+
results.push(...this.#runFixpoint(pending, evaluated));
|
|
6100
|
+
return results;
|
|
6101
|
+
}
|
|
6102
|
+
/** Evaluates `pending` entries to a fixpoint, following spill-induced changes. */
|
|
6103
|
+
#runFixpoint(pending, evaluated) {
|
|
6104
|
+
const results = [];
|
|
6105
|
+
let guard = 0;
|
|
6106
|
+
const maxIterations = this.#formulas.size + 8;
|
|
6107
|
+
while (pending.size > 0 && guard++ <= maxIterations) {
|
|
6108
|
+
const { results: passResults, changed } = this.#evaluatePass([...pending.values()]);
|
|
6109
|
+
results.push(...passResults);
|
|
6110
|
+
for (const k of pending.keys()) evaluated.add(k);
|
|
6111
|
+
pending = this.#dependentClosure(changed, evaluated);
|
|
6112
|
+
}
|
|
6113
|
+
return results;
|
|
6114
|
+
}
|
|
6115
|
+
/** Adds every volatile formula to a pending set. */
|
|
6116
|
+
#addVolatiles(pending) {
|
|
6117
|
+
for (const key of this.#volatiles) {
|
|
6118
|
+
const e = this.#formulas.get(key);
|
|
6119
|
+
if (e) pending.set(key, e);
|
|
6058
6120
|
}
|
|
6059
|
-
newSpills.set(newAnchor, newMembers);
|
|
6060
6121
|
}
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
__privateMethod(this, _RecalcEngine_instances, addVolatiles_fn).call(this, pending);
|
|
6079
|
-
if (includeKey) {
|
|
6080
|
-
const self = __privateGet(this, _formulas).get(includeKey);
|
|
6081
|
-
if (self) pending.set(includeKey, self);
|
|
6082
|
-
}
|
|
6083
|
-
results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, evaluated));
|
|
6084
|
-
return results;
|
|
6085
|
-
};
|
|
6086
|
-
/** Evaluates `pending` entries to a fixpoint, following spill-induced changes. */
|
|
6087
|
-
runFixpoint_fn = function(pending, evaluated) {
|
|
6088
|
-
const results = [];
|
|
6089
|
-
let guard = 0;
|
|
6090
|
-
const maxIterations = __privateGet(this, _formulas).size + 8;
|
|
6091
|
-
while (pending.size > 0 && guard++ <= maxIterations) {
|
|
6092
|
-
const { results: passResults, changed } = __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...pending.values()]);
|
|
6093
|
-
results.push(...passResults);
|
|
6094
|
-
for (const k of pending.keys()) evaluated.add(k);
|
|
6095
|
-
pending = __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, changed, evaluated);
|
|
6096
|
-
}
|
|
6097
|
-
return results;
|
|
6098
|
-
};
|
|
6099
|
-
/** Adds every volatile formula to a pending set. */
|
|
6100
|
-
addVolatiles_fn = function(pending) {
|
|
6101
|
-
for (const key of __privateGet(this, _volatiles)) {
|
|
6102
|
-
const e = __privateGet(this, _formulas).get(key);
|
|
6103
|
-
if (e) pending.set(key, e);
|
|
6122
|
+
/**
|
|
6123
|
+
* Formula entries (not yet evaluated) whose precedents include one of the
|
|
6124
|
+
* cells, transitively. The queue is walked with a head index — Array.shift()
|
|
6125
|
+
* is O(n) per dequeue, which turns a long dependency chain quadratic.
|
|
6126
|
+
*/
|
|
6127
|
+
#dependentClosure(cells, evaluated) {
|
|
6128
|
+
const out = /* @__PURE__ */ new Map();
|
|
6129
|
+
const queue = [...cells];
|
|
6130
|
+
for (let head = 0; head < queue.length; head++) {
|
|
6131
|
+
const cell = queue[head];
|
|
6132
|
+
this.#forEachDirectDependent(cell, (dep) => {
|
|
6133
|
+
if (out.has(dep.key) || evaluated.has(dep.key)) return;
|
|
6134
|
+
out.set(dep.key, dep);
|
|
6135
|
+
queue.push({ sheet: dep.sheet, row: dep.row, col: dep.col });
|
|
6136
|
+
});
|
|
6137
|
+
}
|
|
6138
|
+
return out;
|
|
6104
6139
|
}
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6140
|
+
/**
|
|
6141
|
+
* Visits the formula entries whose precedents include the given cell, via the
|
|
6142
|
+
* reverse index. The index over-approximates (it buckets rectangles into
|
|
6143
|
+
* tiles), so each candidate is still confirmed against its real rectangles.
|
|
6144
|
+
*/
|
|
6145
|
+
#forEachDirectDependent(cell, visit) {
|
|
6146
|
+
this.#dependents.candidates(cell.sheet, cell.row, cell.col, (key) => {
|
|
6147
|
+
const e = this.#formulas.get(key);
|
|
6148
|
+
if (!e) return;
|
|
6149
|
+
for (const ref of e.refs) {
|
|
6150
|
+
if (refContains(ref, cell.sheet, cell.row, cell.col)) {
|
|
6151
|
+
visit(e);
|
|
6152
|
+
return;
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6120
6155
|
});
|
|
6121
6156
|
}
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
*/
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
const
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6157
|
+
/**
|
|
6158
|
+
* Topologically sorts the given entries and evaluates them, writing results
|
|
6159
|
+
* (scalars or spilled arrays) back to the model. Returns the results and the
|
|
6160
|
+
* flat list of cells whose value changed. Entries left in a cycle are #REF!.
|
|
6161
|
+
*/
|
|
6162
|
+
#evaluatePass(entries) {
|
|
6163
|
+
const inSet = /* @__PURE__ */ new Map();
|
|
6164
|
+
for (const e of entries) inSet.set(e.key, e);
|
|
6165
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
6166
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
6167
|
+
for (const k of inSet.keys()) {
|
|
6168
|
+
indegree.set(k, 0);
|
|
6169
|
+
dependents.set(k, []);
|
|
6170
|
+
}
|
|
6171
|
+
for (const pe of entries) {
|
|
6172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6173
|
+
this.#forEachDirectDependent({ sheet: pe.sheet, row: pe.row, col: pe.col }, (dep) => {
|
|
6174
|
+
if (dep.key === pe.key || !inSet.has(dep.key) || seen.has(dep.key)) return;
|
|
6175
|
+
seen.add(dep.key);
|
|
6176
|
+
dependents.get(pe.key).push(dep.key);
|
|
6177
|
+
indegree.set(dep.key, indegree.get(dep.key) + 1);
|
|
6178
|
+
});
|
|
6179
|
+
}
|
|
6180
|
+
const ready = [];
|
|
6181
|
+
for (const [k, d] of indegree) if (d === 0) ready.push(k);
|
|
6182
|
+
const order = [];
|
|
6183
|
+
for (let head = 0; head < ready.length; head++) {
|
|
6184
|
+
const k = ready[head];
|
|
6185
|
+
order.push(k);
|
|
6186
|
+
for (const d of dependents.get(k)) {
|
|
6187
|
+
const left2 = indegree.get(d) - 1;
|
|
6188
|
+
indegree.set(d, left2);
|
|
6189
|
+
if (left2 === 0) ready.push(d);
|
|
6137
6190
|
}
|
|
6138
6191
|
}
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: pe.sheet, row: pe.row, col: pe.col }, (dep) => {
|
|
6158
|
-
if (dep.key === pe.key || !inSet.has(dep.key) || seen.has(dep.key)) return;
|
|
6159
|
-
seen.add(dep.key);
|
|
6160
|
-
dependents.get(pe.key).push(dep.key);
|
|
6161
|
-
indegree.set(dep.key, indegree.get(dep.key) + 1);
|
|
6162
|
-
});
|
|
6192
|
+
const results = [];
|
|
6193
|
+
const changed = [];
|
|
6194
|
+
const emitted = new Set(order);
|
|
6195
|
+
for (const k of order) {
|
|
6196
|
+
const e = inSet.get(k);
|
|
6197
|
+
const { result, changed: c } = this.#writeResult(e);
|
|
6198
|
+
results.push(result);
|
|
6199
|
+
changed.push(...c);
|
|
6200
|
+
}
|
|
6201
|
+
for (const [k, e] of inSet) {
|
|
6202
|
+
if (emitted.has(k)) continue;
|
|
6203
|
+
this.#clearSpill(e.key, changed);
|
|
6204
|
+
const value2 = FormulaValue.errorRef.toObject();
|
|
6205
|
+
this.#writeCell(e.sheetName, e.ref, value2, e.row, e.col);
|
|
6206
|
+
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
6207
|
+
results.push({ sheet: e.sheetName, ref: e.ref, value: value2, circular: true });
|
|
6208
|
+
}
|
|
6209
|
+
return { results, changed };
|
|
6163
6210
|
}
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
};
|
|
6195
|
-
/**
|
|
6196
|
-
* Evaluates one formula and writes its result. Scalars go to the anchor cell.
|
|
6197
|
-
* Arrays spill across a block anchored at the formula cell: on a collision
|
|
6198
|
-
* with existing content the anchor becomes #SPILL! and nothing spills; when
|
|
6199
|
-
* an array shrinks, previously-spilled cells it no longer covers are cleared.
|
|
6200
|
-
*/
|
|
6201
|
-
writeResult_fn = function(e) {
|
|
6202
|
-
const anchorKey = e.key;
|
|
6203
|
-
const fv2 = __privateMethod(this, _RecalcEngine_instances, evaluateFormulaValue_fn).call(this, e);
|
|
6204
|
-
const changed = [];
|
|
6205
|
-
const prevSpill = __privateGet(this, _spills).get(anchorKey) ?? /* @__PURE__ */ new Set();
|
|
6206
|
-
if (!fv2.isArray) {
|
|
6207
|
-
__privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
|
|
6208
|
-
__privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
|
|
6209
|
-
const value2 = fv2.toObject();
|
|
6210
|
-
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
|
|
6211
|
-
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
6212
|
-
return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
|
|
6213
|
-
}
|
|
6214
|
-
const arr = fv2.arrayVal;
|
|
6215
|
-
const rows2 = arr.rows, cols = arr.columns;
|
|
6216
|
-
let collides = false;
|
|
6217
|
-
for (let dr = 0; dr < rows2 && !collides; dr++) {
|
|
6218
|
-
for (let dc = 0; dc < cols && !collides; dc++) {
|
|
6219
|
-
if (dr === 0 && dc === 0) continue;
|
|
6220
|
-
const r = e.row + dr, c = e.col + dc;
|
|
6221
|
-
const cellKey2 = keyOf(e.sheet, cellRefFromRowCol(r, c));
|
|
6222
|
-
if (__privateMethod(this, _RecalcEngine_instances, isOccupied_fn).call(this, cellKey2, e.sheetName, cellRefFromRowCol(r, c), anchorKey, prevSpill)) {
|
|
6223
|
-
collides = true;
|
|
6211
|
+
/**
|
|
6212
|
+
* Evaluates one formula and writes its result. Scalars go to the anchor cell.
|
|
6213
|
+
* Arrays spill across a block anchored at the formula cell: on a collision
|
|
6214
|
+
* with existing content the anchor becomes #SPILL! and nothing spills; when
|
|
6215
|
+
* an array shrinks, previously-spilled cells it no longer covers are cleared.
|
|
6216
|
+
*/
|
|
6217
|
+
#writeResult(e) {
|
|
6218
|
+
const anchorKey = e.key;
|
|
6219
|
+
const fv2 = this.#evaluateFormulaValue(e);
|
|
6220
|
+
const changed = [];
|
|
6221
|
+
const prevSpill = this.#spills.get(anchorKey) ?? /* @__PURE__ */ new Set();
|
|
6222
|
+
if (!fv2.isArray) {
|
|
6223
|
+
this.#clearSpill(anchorKey, changed);
|
|
6224
|
+
this.#model.setCellArrayRef?.(e.sheetName, e.ref, null);
|
|
6225
|
+
const value2 = resultOf(fv2);
|
|
6226
|
+
this.#writeCell(e.sheetName, e.ref, value2, e.row, e.col);
|
|
6227
|
+
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
6228
|
+
return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
|
|
6229
|
+
}
|
|
6230
|
+
const arr = fv2.arrayVal;
|
|
6231
|
+
const rows2 = arr.rows, cols = arr.columns;
|
|
6232
|
+
let collides = false;
|
|
6233
|
+
for (let dr = 0; dr < rows2 && !collides; dr++) {
|
|
6234
|
+
for (let dc = 0; dc < cols && !collides; dc++) {
|
|
6235
|
+
if (dr === 0 && dc === 0) continue;
|
|
6236
|
+
const r = e.row + dr, c = e.col + dc;
|
|
6237
|
+
const cellKey2 = keyOf(e.sheet, cellRefFromRowCol(r, c));
|
|
6238
|
+
if (this.#isOccupied(cellKey2, e.sheetName, cellRefFromRowCol(r, c), anchorKey, prevSpill)) {
|
|
6239
|
+
collides = true;
|
|
6240
|
+
}
|
|
6224
6241
|
}
|
|
6225
6242
|
}
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6243
|
+
if (collides) {
|
|
6244
|
+
this.#clearSpill(anchorKey, changed);
|
|
6245
|
+
this.#model.setCellArrayRef?.(e.sheetName, e.ref, null);
|
|
6246
|
+
const value2 = FormulaValue.error(9 /* Spill */).toObject();
|
|
6247
|
+
this.#writeCell(e.sheetName, e.ref, value2, e.row, e.col);
|
|
6248
|
+
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
6249
|
+
return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
|
|
6250
|
+
}
|
|
6251
|
+
const newSpill = /* @__PURE__ */ new Set();
|
|
6252
|
+
for (let dr = 0; dr < rows2; dr++) {
|
|
6253
|
+
for (let dc = 0; dc < cols; dc++) {
|
|
6254
|
+
const r = e.row + dr, c = e.col + dc;
|
|
6255
|
+
const ref = cellRefFromRowCol(r, c);
|
|
6256
|
+
this.#writeCell(e.sheetName, ref, resultOf(arr.get(dr, dc)), r, c);
|
|
6257
|
+
changed.push({ sheet: e.sheet, row: r, col: c });
|
|
6258
|
+
if (dr !== 0 || dc !== 0) {
|
|
6259
|
+
const cellKey2 = keyOf(e.sheet, ref);
|
|
6260
|
+
newSpill.add(cellKey2);
|
|
6261
|
+
this.#spillOwner.set(cellKey2, anchorKey);
|
|
6262
|
+
}
|
|
6246
6263
|
}
|
|
6247
6264
|
}
|
|
6265
|
+
for (const oldKey of prevSpill) {
|
|
6266
|
+
if (newSpill.has(oldKey)) continue;
|
|
6267
|
+
const cell = parseKey(oldKey);
|
|
6268
|
+
this.#writeCell(this.#sheetNames.get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
6269
|
+
this.#spillOwner.delete(oldKey);
|
|
6270
|
+
changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
|
|
6271
|
+
}
|
|
6272
|
+
this.#spills.set(anchorKey, newSpill);
|
|
6273
|
+
const spillRef = `${e.ref}:${cellRefFromRowCol(e.row + rows2 - 1, e.col + cols - 1)}`;
|
|
6274
|
+
this.#model.setCellArrayRef?.(e.sheetName, e.ref, spillRef);
|
|
6275
|
+
return {
|
|
6276
|
+
result: { sheet: e.sheetName, ref: e.ref, value: resultOf(arr.get(0, 0)), spill: { rows: rows2, cols } },
|
|
6277
|
+
changed
|
|
6278
|
+
};
|
|
6248
6279
|
}
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6280
|
+
/** Whether a target cell already holds content that blocks a spill. */
|
|
6281
|
+
#isOccupied(cellKey2, sheetName, ref, anchorKey, prevSpill) {
|
|
6282
|
+
if (this.#formulas.has(cellKey2) && cellKey2 !== anchorKey) return true;
|
|
6283
|
+
const owner = this.#spillOwner.get(cellKey2);
|
|
6284
|
+
if (owner && owner !== anchorKey) return true;
|
|
6285
|
+
if (prevSpill.has(cellKey2)) return false;
|
|
6286
|
+
const v = this.#model.getCellValue(sheetName, ref);
|
|
6287
|
+
return v !== null && v !== void 0 && v !== "";
|
|
6288
|
+
}
|
|
6289
|
+
/** Clears an anchor's spilled cells (not the anchor itself) and forgets them. */
|
|
6290
|
+
#clearSpill(anchorKey, changed) {
|
|
6291
|
+
const spill = this.#spills.get(anchorKey);
|
|
6292
|
+
if (!spill) return;
|
|
6293
|
+
for (const cellKey2 of spill) {
|
|
6294
|
+
const cell = parseKey(cellKey2);
|
|
6295
|
+
this.#writeCell(this.#sheetNames.get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
6296
|
+
this.#spillOwner.delete(cellKey2);
|
|
6297
|
+
changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
|
|
6298
|
+
}
|
|
6299
|
+
this.#spills.delete(anchorKey);
|
|
6300
|
+
}
|
|
6301
|
+
#evaluateFormulaValue(e) {
|
|
6302
|
+
if (!e.ast) {
|
|
6303
|
+
try {
|
|
6304
|
+
e.ast = parseFormulaUncached(e.formula);
|
|
6305
|
+
} catch {
|
|
6306
|
+
return FormulaValue.errorValue;
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
return evaluateAst(e.ast, this.#sheetLike(e.sheetName), {
|
|
6310
|
+
workbook: this.#buildWorkbookLike(),
|
|
6311
|
+
formulaRow: e.row,
|
|
6312
|
+
formulaCol: e.col,
|
|
6313
|
+
rangeCache: this.#rangeCache
|
|
6314
|
+
});
|
|
6255
6315
|
}
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
if (__privateGet(this, _formulas).has(cellKey2) && cellKey2 !== anchorKey) return true;
|
|
6267
|
-
const owner = __privateGet(this, _spillOwner).get(cellKey2);
|
|
6268
|
-
if (owner && owner !== anchorKey) return true;
|
|
6269
|
-
if (prevSpill.has(cellKey2)) return false;
|
|
6270
|
-
const v = __privateGet(this, _model).getCellValue(sheetName, ref);
|
|
6271
|
-
return v !== null && v !== void 0 && v !== "";
|
|
6272
|
-
};
|
|
6273
|
-
/** Clears an anchor's spilled cells (not the anchor itself) and forgets them. */
|
|
6274
|
-
clearSpill_fn = function(anchorKey, changed) {
|
|
6275
|
-
const spill = __privateGet(this, _spills).get(anchorKey);
|
|
6276
|
-
if (!spill) return;
|
|
6277
|
-
for (const cellKey2 of spill) {
|
|
6278
|
-
const cell = parseKey(cellKey2);
|
|
6279
|
-
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
6280
|
-
__privateGet(this, _spillOwner).delete(cellKey2);
|
|
6281
|
-
changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
|
|
6282
|
-
}
|
|
6283
|
-
__privateGet(this, _spills).delete(anchorKey);
|
|
6284
|
-
};
|
|
6285
|
-
evaluateFormulaValue_fn = function(e) {
|
|
6286
|
-
if (!e.ast) {
|
|
6287
|
-
try {
|
|
6288
|
-
e.ast = parseFormulaUncached(e.formula);
|
|
6289
|
-
} catch {
|
|
6290
|
-
return FormulaValue.errorValue;
|
|
6316
|
+
/**
|
|
6317
|
+
* Per-sheet evaluator adapter. Memoised: these hold no per-formula state, and
|
|
6318
|
+
* rebuilding one (with its four closures) for every formula evaluated made a
|
|
6319
|
+
* full recalc allocate several objects per cell.
|
|
6320
|
+
*/
|
|
6321
|
+
#sheetLike(sheetName) {
|
|
6322
|
+
let cached = this.#sheetLikes.get(sheetName);
|
|
6323
|
+
if (!cached) {
|
|
6324
|
+
cached = this.#buildSheetLike(sheetName);
|
|
6325
|
+
this.#sheetLikes.set(sheetName, cached);
|
|
6291
6326
|
}
|
|
6327
|
+
return cached;
|
|
6328
|
+
}
|
|
6329
|
+
#buildSheetLike(sheetName) {
|
|
6330
|
+
const model = this.#model;
|
|
6331
|
+
return {
|
|
6332
|
+
name: sheetName,
|
|
6333
|
+
getCellValue: (ref) => this.#model.getCellValue(sheetName, ref),
|
|
6334
|
+
getCellValueAt: model.getCellValueAt ? (row2, column2) => model.getCellValueAt(sheetName, row2, column2) : void 0,
|
|
6335
|
+
// A getter, not a snapshot: the extent grows as formulas write results,
|
|
6336
|
+
// and a whole-column reference read later in the same pass must see it.
|
|
6337
|
+
get usedBounds() {
|
|
6338
|
+
return model.getUsedBounds?.(sheetName);
|
|
6339
|
+
},
|
|
6340
|
+
isRowHidden: this.#model.isRowHidden ? (row2) => this.#model.isRowHidden(sheetName, row2) : void 0,
|
|
6341
|
+
getCellFormula: this.#model.getCellFormula ? (ref) => this.#model.getCellFormula(sheetName, ref) : void 0,
|
|
6342
|
+
getSpillRange: this.#model.getSpillRange ? (ref) => this.#model.getSpillRange(sheetName, ref) : void 0
|
|
6343
|
+
};
|
|
6344
|
+
}
|
|
6345
|
+
#buildWorkbookLike() {
|
|
6346
|
+
if (this.#workbookLike) return this.#workbookLike;
|
|
6347
|
+
const self = this;
|
|
6348
|
+
return this.#workbookLike = {
|
|
6349
|
+
get worksheets() {
|
|
6350
|
+
return [...self.#sheetNames.values()].map((n) => self.#sheetLike(n));
|
|
6351
|
+
},
|
|
6352
|
+
getWorksheet: (name) => self.#sheetLike(name),
|
|
6353
|
+
getDefinedName: this.#model.getDefinedName ? (name, sheet) => this.#model.getDefinedName(name, sheet) : void 0
|
|
6354
|
+
};
|
|
6292
6355
|
}
|
|
6293
|
-
return evaluateAst(e.ast, __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName), {
|
|
6294
|
-
workbook: __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this),
|
|
6295
|
-
formulaRow: e.row,
|
|
6296
|
-
formulaCol: e.col,
|
|
6297
|
-
rangeCache: __privateGet(this, _rangeCache)
|
|
6298
|
-
});
|
|
6299
|
-
};
|
|
6300
|
-
/**
|
|
6301
|
-
* Per-sheet evaluator adapter. Memoised: these hold no per-formula state, and
|
|
6302
|
-
* rebuilding one (with its four closures) for every formula evaluated made a
|
|
6303
|
-
* full recalc allocate several objects per cell.
|
|
6304
|
-
*/
|
|
6305
|
-
sheetLike_fn = function(sheetName) {
|
|
6306
|
-
let cached = __privateGet(this, _sheetLikes).get(sheetName);
|
|
6307
|
-
if (!cached) {
|
|
6308
|
-
cached = __privateMethod(this, _RecalcEngine_instances, buildSheetLike_fn).call(this, sheetName);
|
|
6309
|
-
__privateGet(this, _sheetLikes).set(sheetName, cached);
|
|
6310
|
-
}
|
|
6311
|
-
return cached;
|
|
6312
|
-
};
|
|
6313
|
-
buildSheetLike_fn = function(sheetName) {
|
|
6314
|
-
const model = __privateGet(this, _model);
|
|
6315
|
-
return {
|
|
6316
|
-
name: sheetName,
|
|
6317
|
-
getCellValue: (ref) => __privateGet(this, _model).getCellValue(sheetName, ref),
|
|
6318
|
-
getCellValueAt: model.getCellValueAt ? (row2, column2) => model.getCellValueAt(sheetName, row2, column2) : void 0,
|
|
6319
|
-
// A getter, not a snapshot: the extent grows as formulas write results,
|
|
6320
|
-
// and a whole-column reference read later in the same pass must see it.
|
|
6321
|
-
get usedBounds() {
|
|
6322
|
-
return model.getUsedBounds?.(sheetName);
|
|
6323
|
-
},
|
|
6324
|
-
isRowHidden: __privateGet(this, _model).isRowHidden ? (row2) => __privateGet(this, _model).isRowHidden(sheetName, row2) : void 0,
|
|
6325
|
-
getCellFormula: __privateGet(this, _model).getCellFormula ? (ref) => __privateGet(this, _model).getCellFormula(sheetName, ref) : void 0,
|
|
6326
|
-
getSpillRange: __privateGet(this, _model).getSpillRange ? (ref) => __privateGet(this, _model).getSpillRange(sheetName, ref) : void 0
|
|
6327
|
-
};
|
|
6328
|
-
};
|
|
6329
|
-
buildWorkbookLike_fn = function() {
|
|
6330
|
-
if (__privateGet(this, _workbookLike)) return __privateGet(this, _workbookLike);
|
|
6331
|
-
const self = this;
|
|
6332
|
-
return __privateSet(this, _workbookLike, {
|
|
6333
|
-
get worksheets() {
|
|
6334
|
-
return [...__privateGet(self, _sheetNames).values()].map((n) => {
|
|
6335
|
-
var _a;
|
|
6336
|
-
return __privateMethod(_a = self, _RecalcEngine_instances, sheetLike_fn).call(_a, n);
|
|
6337
|
-
});
|
|
6338
|
-
},
|
|
6339
|
-
getWorksheet: (name) => {
|
|
6340
|
-
var _a;
|
|
6341
|
-
return __privateMethod(_a = self, _RecalcEngine_instances, sheetLike_fn).call(_a, name);
|
|
6342
|
-
},
|
|
6343
|
-
getDefinedName: __privateGet(this, _model).getDefinedName ? (name, sheet) => __privateGet(this, _model).getDefinedName(name, sheet) : void 0
|
|
6344
|
-
});
|
|
6345
6356
|
};
|
|
6346
6357
|
|
|
6347
6358
|
export { ArrayValue, BinaryOpNode, BinaryOperator, BlankNode, BooleanNode, CallNode, CellReferenceNode, ErrorNode, FormulaContext, FormulaError, FormulaException, FormulaHelper, FormulaLexer, FormulaNode, FormulaParser, FormulaValue, FullRangeReferenceNode, FunctionCallNode, FunctionRegistry, ImplicitIntersectionNode, LruCache, NamedRangeNode, NumberNode, RangeCache, RangeReferenceNode, RecalcEngine, SheetCellReferenceNode, SheetRangeReferenceNode, SpillReferenceNode, StringNode, TokenType, UnaryOpNode, UnaryOperator, clearAstCache, createWorkbookRecalcModel, evaluateAst, evaluateFormula, extractReferences, getAstCacheStats, parseFormula, parseFormulaUncached, refContains, registerAggregate, registerArray, registerDate, registerFinance, registerInfo, registerLogical, registerLookup, registerMath, registerMeta, registerStatistical, registerText, setAstCacheCapacity };
|