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