@devmm/puredocs-excel-formula 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1580 -1569
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +1581 -1570
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.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));
|
|
1125
|
+
// ── Token helpers ──────────────────────────────────────────────────────────
|
|
1126
|
+
get #current() {
|
|
1127
|
+
return this.#tokens[this.#pos] ?? { type: 31 /* EOF */, value: "", position: -1 };
|
|
1187
1128
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1129
|
+
#advance() {
|
|
1130
|
+
const t = this.#current;
|
|
1131
|
+
this.#pos++;
|
|
1132
|
+
return t;
|
|
1191
1133
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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);
|
|
1235
|
+
}
|
|
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);
|
|
1253
|
+
}
|
|
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);
|
|
1231
1267
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
case 2 /* Boolean */:
|
|
1240
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1241
|
-
return new BooleanNode(token.value === "TRUE");
|
|
1242
|
-
case 30 /* ErrorLiteral */:
|
|
1243
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1244
|
-
return new ErrorNode(FormulaValue.error(FormulaValue.errorFromString(token.value)));
|
|
1245
|
-
case 3 /* CellReference */: {
|
|
1246
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1247
|
-
if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
|
|
1248
|
-
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1249
|
-
const end = __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 3 /* CellReference */);
|
|
1250
|
-
return new RangeReferenceNode(token.value, end.value);
|
|
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);
|
|
1251
1275
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
return new SheetRangeReferenceNode(sheetName, cellRef, end.value);
|
|
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;
|
|
1265
1288
|
}
|
|
1266
|
-
|
|
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, "");
|
|
@@ -2817,6 +2818,8 @@ function registerLookup(r) {
|
|
|
2817
2818
|
r.register("COLUMNS", columns, 1, 1);
|
|
2818
2819
|
r.register("OFFSET", offset, 3, 5, true);
|
|
2819
2820
|
r.register("INDIRECT", indirect, 1, 2, true);
|
|
2821
|
+
r.register("ANCHORARRAY", anchorArray, 1, 1);
|
|
2822
|
+
r.register("SINGLE", single, 1, 1);
|
|
2820
2823
|
}
|
|
2821
2824
|
function num2(a, c, i) {
|
|
2822
2825
|
const r = exports.FormulaHelper.evalDouble(a[i], c);
|
|
@@ -2840,8 +2843,9 @@ function vlookup(a, c) {
|
|
|
2840
2843
|
const tbl = tableVal.arrayVal;
|
|
2841
2844
|
if (colIdx < 0 || colIdx >= tbl.columns) return FormulaValue.errorRef;
|
|
2842
2845
|
if (exactMatch) {
|
|
2846
|
+
const pattern = lookupPattern(lookupVal);
|
|
2843
2847
|
for (let r = 0; r < tbl.rows; r++) {
|
|
2844
|
-
if (
|
|
2848
|
+
if (matchesLookup(tbl.get(r, 0), lookupVal, pattern)) return tbl.get(r, colIdx);
|
|
2845
2849
|
}
|
|
2846
2850
|
return FormulaValue.errorNA;
|
|
2847
2851
|
}
|
|
@@ -2870,8 +2874,9 @@ function hlookup(a, c) {
|
|
|
2870
2874
|
const tbl = tableVal.arrayVal;
|
|
2871
2875
|
if (rowIdx < 0 || rowIdx >= tbl.rows) return FormulaValue.errorRef;
|
|
2872
2876
|
if (exactMatch) {
|
|
2877
|
+
const pattern = lookupPattern(lookupVal);
|
|
2873
2878
|
for (let col = 0; col < tbl.columns; col++) {
|
|
2874
|
-
if (
|
|
2879
|
+
if (matchesLookup(tbl.get(0, col), lookupVal, pattern)) return tbl.get(rowIdx, col);
|
|
2875
2880
|
}
|
|
2876
2881
|
return FormulaValue.errorNA;
|
|
2877
2882
|
}
|
|
@@ -2882,18 +2887,13 @@ function hlookup(a, c) {
|
|
|
2882
2887
|
}
|
|
2883
2888
|
return best >= 0 ? tbl.get(rowIdx, best) : FormulaValue.errorNA;
|
|
2884
2889
|
}
|
|
2885
|
-
function
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
} else if (ch === "*") out += ".*";
|
|
2893
|
-
else if (ch === "?") out += ".";
|
|
2894
|
-
else out += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2895
|
-
}
|
|
2896
|
-
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());
|
|
2897
2897
|
}
|
|
2898
2898
|
function findMatch(n, get, lookupVal, matchMode, searchMode) {
|
|
2899
2899
|
if ((searchMode === 2 || searchMode === -2) && matchMode !== 2) {
|
|
@@ -2917,8 +2917,11 @@ function findMatch(n, get, lookupVal, matchMode, searchMode) {
|
|
|
2917
2917
|
const reverse = searchMode === -1;
|
|
2918
2918
|
const order = reverse ? Array.from({ length: n }, (_, k) => n - 1 - k) : Array.from({ length: n }, (_, k) => k);
|
|
2919
2919
|
if (matchMode === 2) {
|
|
2920
|
-
const re =
|
|
2921
|
-
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
|
+
}
|
|
2922
2925
|
return -1;
|
|
2923
2926
|
}
|
|
2924
2927
|
if (matchMode === 0) {
|
|
@@ -3028,13 +3031,15 @@ function match(a, c) {
|
|
|
3028
3031
|
matchType = Math.round(r.n);
|
|
3029
3032
|
}
|
|
3030
3033
|
if (!arrVal.isArray) {
|
|
3031
|
-
|
|
3034
|
+
const single2 = matchType === 0 ? matchesLookup(arrVal, lookupVal, lookupPattern(lookupVal)) : FormulaValue.areEqual(arrVal, lookupVal);
|
|
3035
|
+
return single2 ? FormulaValue.one : FormulaValue.errorNA;
|
|
3032
3036
|
}
|
|
3033
3037
|
const arr = arrVal.arrayVal;
|
|
3034
3038
|
const len2 = arr.length;
|
|
3035
3039
|
if (matchType === 0) {
|
|
3040
|
+
const pattern = lookupPattern(lookupVal);
|
|
3036
3041
|
for (let i = 0; i < len2; i++) {
|
|
3037
|
-
if (
|
|
3042
|
+
if (matchesLookup(arr.getFlat(i), lookupVal, pattern)) return FormulaValue.number(i + 1);
|
|
3038
3043
|
}
|
|
3039
3044
|
return FormulaValue.errorNA;
|
|
3040
3045
|
}
|
|
@@ -3264,6 +3269,15 @@ function offset(a, c) {
|
|
|
3264
3269
|
puredocsExcel.cellRefFromRowCol(newRow + height - 1, newCol + width - 1)
|
|
3265
3270
|
);
|
|
3266
3271
|
}
|
|
3272
|
+
function anchorArray(a, c) {
|
|
3273
|
+
const arg2 = a[0];
|
|
3274
|
+
if (arg2 instanceof CellReferenceNode) return c.resolveSpillRange(arg2.reference);
|
|
3275
|
+
if (arg2 instanceof SheetCellReferenceNode) return c.resolveSheetSpillRange(arg2.sheetName, arg2.cellReference);
|
|
3276
|
+
return FormulaValue.errorRef;
|
|
3277
|
+
}
|
|
3278
|
+
function single(a, c) {
|
|
3279
|
+
return new ImplicitIntersectionNode(a[0]).evaluate(c);
|
|
3280
|
+
}
|
|
3267
3281
|
|
|
3268
3282
|
// src/functions/statistical-functions.ts
|
|
3269
3283
|
function registerStatistical(r) {
|
|
@@ -4544,24 +4558,20 @@ function toRow(a, c) {
|
|
|
4544
4558
|
|
|
4545
4559
|
// src/function-registry.ts
|
|
4546
4560
|
var MAX_LIFTED_CELLS = 1048576;
|
|
4547
|
-
var
|
|
4548
|
-
|
|
4549
|
-
constructor() {
|
|
4550
|
-
__privateAdd(this, _FunctionRegistry_instances);
|
|
4551
|
-
__privateAdd(this, _functions, /* @__PURE__ */ new Map());
|
|
4552
|
-
}
|
|
4561
|
+
var FunctionRegistry = class _FunctionRegistry {
|
|
4562
|
+
static #default;
|
|
4553
4563
|
/** Lazily-created default registry with all built-in functions registered. */
|
|
4554
4564
|
static get default() {
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
__privateMethod(_a = __privateGet(_FunctionRegistry, _default), _FunctionRegistry_instances, registerBuiltIns_fn).call(_a);
|
|
4565
|
+
if (!_FunctionRegistry.#default) {
|
|
4566
|
+
_FunctionRegistry.#default = new _FunctionRegistry();
|
|
4567
|
+
_FunctionRegistry.#default.#registerBuiltIns();
|
|
4559
4568
|
}
|
|
4560
|
-
return
|
|
4569
|
+
return _FunctionRegistry.#default;
|
|
4561
4570
|
}
|
|
4571
|
+
#functions = /* @__PURE__ */ new Map();
|
|
4562
4572
|
/** Registers a function. Name is normalised to UPPERCASE. */
|
|
4563
4573
|
register(name, handler, minArgs = 0, maxArgs = -1, isVolatile = false) {
|
|
4564
|
-
|
|
4574
|
+
this.#functions.set(name.toUpperCase(), {
|
|
4565
4575
|
name: name.toUpperCase(),
|
|
4566
4576
|
minArgs,
|
|
4567
4577
|
maxArgs,
|
|
@@ -4587,7 +4597,7 @@ var _FunctionRegistry = class _FunctionRegistry {
|
|
|
4587
4597
|
* `register`, or lifting would map it over the array it is meant to reduce.
|
|
4588
4598
|
*/
|
|
4589
4599
|
registerElementwise(name, handler, minArgs = 0, maxArgs = -1) {
|
|
4590
|
-
|
|
4600
|
+
this.#functions.set(name.toUpperCase(), {
|
|
4591
4601
|
name: name.toUpperCase(),
|
|
4592
4602
|
minArgs,
|
|
4593
4603
|
maxArgs,
|
|
@@ -4598,81 +4608,84 @@ var _FunctionRegistry = class _FunctionRegistry {
|
|
|
4598
4608
|
}
|
|
4599
4609
|
/** Executes a function by name with argument validation. */
|
|
4600
4610
|
execute(name, args, ctx) {
|
|
4601
|
-
|
|
4602
|
-
const def = __privateGet(this, _functions).get(name.toUpperCase());
|
|
4611
|
+
const def = this.#functions.get(name.toUpperCase());
|
|
4603
4612
|
if (!def) return FormulaValue.errorName;
|
|
4604
4613
|
if (args.length < def.minArgs) return FormulaValue.errorValue;
|
|
4605
4614
|
if (def.maxArgs >= 0 && args.length > def.maxArgs) return FormulaValue.errorValue;
|
|
4606
4615
|
try {
|
|
4607
|
-
return def.elementwise ?
|
|
4616
|
+
return def.elementwise ? _FunctionRegistry.#executeElementwise(def, args, ctx) : def.handler(args, ctx);
|
|
4608
4617
|
} catch {
|
|
4609
4618
|
return FormulaValue.errorValue;
|
|
4610
4619
|
}
|
|
4611
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
|
+
}
|
|
4612
4667
|
/** Returns true if the named function is volatile (NOW, RAND, etc.). */
|
|
4613
4668
|
isVolatile(name) {
|
|
4614
|
-
return
|
|
4669
|
+
return this.#functions.get(name.toUpperCase())?.isVolatile ?? false;
|
|
4615
4670
|
}
|
|
4616
4671
|
/** Returns true if the function is registered. */
|
|
4617
4672
|
has(name) {
|
|
4618
|
-
return
|
|
4619
|
-
}
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
}
|
|
4633
|
-
if (rows2 === 1 && cols === 1) {
|
|
4634
|
-
return def.handler(values.map((v) => new ValueNode(v.isArray ? v.arrayVal.get(0, 0) : v)), ctx);
|
|
4635
|
-
}
|
|
4636
|
-
if (rows2 * cols > MAX_LIFTED_CELLS) return FormulaValue.errorValue;
|
|
4637
|
-
for (const v of values) {
|
|
4638
|
-
if (!v.isArray) continue;
|
|
4639
|
-
const a = v.arrayVal;
|
|
4640
|
-
if (a.rows !== 1 && a.rows !== rows2 || a.columns !== 1 && a.columns !== cols) {
|
|
4641
|
-
return FormulaValue.errorValue;
|
|
4642
|
-
}
|
|
4643
|
-
}
|
|
4644
|
-
const nodes = values.map((v) => new ValueNode(v));
|
|
4645
|
-
const out = new ArrayValue(rows2, cols);
|
|
4646
|
-
for (let r = 0; r < rows2; r++) {
|
|
4647
|
-
for (let c = 0; c < cols; c++) {
|
|
4648
|
-
for (let i = 0; i < values.length; i++) {
|
|
4649
|
-
const v = values[i];
|
|
4650
|
-
if (!v.isArray) continue;
|
|
4651
|
-
const a = v.arrayVal;
|
|
4652
|
-
nodes[i].value = a.get(a.rows === 1 ? 0 : r, a.columns === 1 ? 0 : c);
|
|
4653
|
-
}
|
|
4654
|
-
out.set(r, c, def.handler(nodes, ctx));
|
|
4655
|
-
}
|
|
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);
|
|
4656
4687
|
}
|
|
4657
|
-
return FormulaValue.array(out);
|
|
4658
|
-
};
|
|
4659
|
-
_FunctionRegistry_instances = new WeakSet();
|
|
4660
|
-
registerBuiltIns_fn = function() {
|
|
4661
|
-
registerMath(this);
|
|
4662
|
-
registerText(this);
|
|
4663
|
-
registerLogical(this);
|
|
4664
|
-
registerDate(this);
|
|
4665
|
-
registerLookup(this);
|
|
4666
|
-
registerStatistical(this);
|
|
4667
|
-
registerInfo(this);
|
|
4668
|
-
registerFinance(this);
|
|
4669
|
-
registerMeta(this);
|
|
4670
|
-
registerAggregate(this);
|
|
4671
|
-
registerArray(this);
|
|
4672
4688
|
};
|
|
4673
|
-
__privateAdd(_FunctionRegistry, _FunctionRegistry_static);
|
|
4674
|
-
__privateAdd(_FunctionRegistry, _default);
|
|
4675
|
-
var FunctionRegistry = _FunctionRegistry;
|
|
4676
4689
|
exports.FormulaHelper = void 0;
|
|
4677
4690
|
((FormulaHelper2) => {
|
|
4678
4691
|
function evalDouble(node, ctx) {
|
|
@@ -4745,7 +4758,7 @@ exports.FormulaHelper = void 0;
|
|
|
4745
4758
|
const test = (v) => wanted(FormulaValue.compare(v, rhsValue));
|
|
4746
4759
|
return rhsNum !== null ? { kind: "compareNumber", number: rhsNum, op, test } : { kind: "other", test };
|
|
4747
4760
|
}
|
|
4748
|
-
if (
|
|
4761
|
+
if (hasWildcardSyntax(trimmed)) {
|
|
4749
4762
|
const pattern = wildcardToRegex(trimmed);
|
|
4750
4763
|
return { kind: "other", test: (v) => pattern.test(v.asText()) };
|
|
4751
4764
|
}
|
|
@@ -4777,53 +4790,106 @@ exports.FormulaHelper = void 0;
|
|
|
4777
4790
|
return compileCriteria(criteria).test(value2);
|
|
4778
4791
|
}
|
|
4779
4792
|
FormulaHelper2.matchesCriteria = matchesCriteria;
|
|
4780
|
-
function
|
|
4781
|
-
|
|
4782
|
-
|
|
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;
|
|
4783
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, "\\$&");
|
|
4784
4817
|
})(exports.FormulaHelper || (exports.FormulaHelper = {}));
|
|
4785
4818
|
|
|
4786
4819
|
// src/formula-context.ts
|
|
4787
|
-
var
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
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 = [];
|
|
4806
4856
|
/** Pushes a new binding frame (used by LET/LAMBDA). */
|
|
4807
4857
|
pushScope(bindings) {
|
|
4808
|
-
|
|
4858
|
+
this.#scopes.push(bindings);
|
|
4809
4859
|
}
|
|
4810
4860
|
/** Pops the innermost binding frame. */
|
|
4811
4861
|
popScope() {
|
|
4812
|
-
|
|
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;
|
|
4813
4879
|
}
|
|
4814
4880
|
get worksheet() {
|
|
4815
|
-
return
|
|
4881
|
+
return this.#sheet;
|
|
4816
4882
|
}
|
|
4817
4883
|
// ── Cell/Range resolution ─────────────────────────────────────────────────
|
|
4818
4884
|
getCellValue(cellReference) {
|
|
4819
4885
|
const norm = cellReference.replace(/\$/g, "").toUpperCase();
|
|
4820
|
-
if (
|
|
4821
|
-
|
|
4886
|
+
if (this.#evaluating.has(norm)) return FormulaValue.errorRef;
|
|
4887
|
+
this.#evaluating.add(norm);
|
|
4822
4888
|
try {
|
|
4823
|
-
const raw =
|
|
4889
|
+
const raw = this.#sheet.getCellValue(cellReference);
|
|
4824
4890
|
return FormulaValue.fromObjectSync(raw);
|
|
4825
4891
|
} finally {
|
|
4826
|
-
|
|
4892
|
+
this.#evaluating.delete(norm);
|
|
4827
4893
|
}
|
|
4828
4894
|
}
|
|
4829
4895
|
getRangeValues(startRef, endRef) {
|
|
@@ -4841,17 +4907,17 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4841
4907
|
const rows2 = endRow - startRow + 1;
|
|
4842
4908
|
const cols = endCol - startCol + 1;
|
|
4843
4909
|
const cache = rows2 * cols > 1 ? this.rangeCache : void 0;
|
|
4844
|
-
const sheetKey = cache ?
|
|
4910
|
+
const sheetKey = cache ? this.#sheet.name.toUpperCase() : "";
|
|
4845
4911
|
if (cache) {
|
|
4846
4912
|
const hit = cache.get(sheetKey, startRow, startCol, endRow, endCol);
|
|
4847
4913
|
if (hit !== void 0) return hit;
|
|
4848
4914
|
}
|
|
4849
4915
|
const arr = new ArrayValue(rows2, cols);
|
|
4850
|
-
const at =
|
|
4916
|
+
const at = this.#sheet.getCellValueAt;
|
|
4851
4917
|
if (at) {
|
|
4852
4918
|
for (let r = 0; r < rows2; r++) {
|
|
4853
4919
|
for (let c = 0; c < cols; c++) {
|
|
4854
|
-
arr.set(r, c, FormulaValue.fromObjectSync(at.call(
|
|
4920
|
+
arr.set(r, c, FormulaValue.fromObjectSync(at.call(this.#sheet, startRow + r, startCol + c)));
|
|
4855
4921
|
}
|
|
4856
4922
|
}
|
|
4857
4923
|
} else {
|
|
@@ -4877,7 +4943,7 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4877
4943
|
* silently reading nothing would make SUM(A:A) return 0 on a full column.
|
|
4878
4944
|
*/
|
|
4879
4945
|
getFullRangeValues(startCol, endCol, startRow, endRow) {
|
|
4880
|
-
const bounds =
|
|
4946
|
+
const bounds = this.#sheet.usedBounds;
|
|
4881
4947
|
if (!bounds) return FormulaValue.errorRef;
|
|
4882
4948
|
if (bounds.rowCount === 0 || bounds.columnCount === 0) return FormulaValue.blank;
|
|
4883
4949
|
const r0 = startRow ?? 1;
|
|
@@ -4892,7 +4958,7 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4892
4958
|
getSheetCellValue(sheetName, cellReference) {
|
|
4893
4959
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4894
4960
|
try {
|
|
4895
|
-
return
|
|
4961
|
+
return this.#forSheet(sheetName).getCellValue(cellReference);
|
|
4896
4962
|
} catch {
|
|
4897
4963
|
return FormulaValue.errorRef;
|
|
4898
4964
|
}
|
|
@@ -4900,7 +4966,7 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4900
4966
|
getSheetRangeValues(sheetName, startRef, endRef) {
|
|
4901
4967
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4902
4968
|
try {
|
|
4903
|
-
return
|
|
4969
|
+
return this.#forSheet(sheetName).getRangeValues(startRef, endRef);
|
|
4904
4970
|
} catch {
|
|
4905
4971
|
return FormulaValue.errorRef;
|
|
4906
4972
|
}
|
|
@@ -4909,13 +4975,28 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4909
4975
|
getSheetFullRangeValues(sheetName, startCol, endCol, startRow, endRow) {
|
|
4910
4976
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4911
4977
|
try {
|
|
4912
|
-
return
|
|
4978
|
+
return this.#forSheet(sheetName).getFullRangeValues(startCol, endCol, startRow, endRow);
|
|
4913
4979
|
} catch {
|
|
4914
4980
|
return FormulaValue.errorRef;
|
|
4915
4981
|
}
|
|
4916
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
|
+
}
|
|
4917
4998
|
resolveNamedRange(name) {
|
|
4918
|
-
const scoped =
|
|
4999
|
+
const scoped = this.#lookupScope(name);
|
|
4919
5000
|
if (scoped !== void 0) return scoped;
|
|
4920
5001
|
return this.nameResolver ? this.nameResolver(name) : FormulaValue.errorName;
|
|
4921
5002
|
}
|
|
@@ -4924,12 +5005,21 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4924
5005
|
* cell. Requires the worksheet to expose getSpillRange; otherwise #REF!.
|
|
4925
5006
|
*/
|
|
4926
5007
|
resolveSpillRange(anchorRef) {
|
|
4927
|
-
const spillRef =
|
|
5008
|
+
const spillRef = this.#sheet.getSpillRange?.(anchorRef);
|
|
4928
5009
|
if (!spillRef) return FormulaValue.errorRef;
|
|
4929
5010
|
const colon = spillRef.indexOf(":");
|
|
4930
5011
|
if (colon < 0) return this.getCellValue(spillRef);
|
|
4931
5012
|
return this.getRangeValues(spillRef.slice(0, colon), spillRef.slice(colon + 1));
|
|
4932
5013
|
}
|
|
5014
|
+
/** {@link resolveSpillRange} for an anchor on another sheet ('S'!A1#). */
|
|
5015
|
+
resolveSheetSpillRange(sheetName, anchorRef) {
|
|
5016
|
+
if (!this.workbook) return FormulaValue.errorRef;
|
|
5017
|
+
try {
|
|
5018
|
+
return this.#forSheet(sheetName).resolveSpillRange(anchorRef);
|
|
5019
|
+
} catch {
|
|
5020
|
+
return FormulaValue.errorRef;
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
4933
5023
|
getRangeBounds(startRef, endRef) {
|
|
4934
5024
|
const s = puredocsExcel.parseCellRef(startRef);
|
|
4935
5025
|
const e = puredocsExcel.parseCellRef(endRef);
|
|
@@ -4938,94 +5028,63 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4938
5028
|
// ── Function dispatch ─────────────────────────────────────────────────────
|
|
4939
5029
|
/** True if a built-in function with this name is registered. */
|
|
4940
5030
|
hasFunction(name) {
|
|
4941
|
-
return
|
|
5031
|
+
return this.#functions.has(name);
|
|
4942
5032
|
}
|
|
4943
5033
|
evaluateFunction(name, args) {
|
|
4944
|
-
if (!
|
|
5034
|
+
if (!this.#functions.has(name)) {
|
|
4945
5035
|
const named = this.resolveNamedRange(name);
|
|
4946
5036
|
if (named.isLambda) {
|
|
4947
5037
|
return named.lambdaVal.call(args.map((a) => a.evaluate(this)));
|
|
4948
5038
|
}
|
|
4949
5039
|
}
|
|
4950
|
-
return
|
|
5040
|
+
return this.#functions.execute(name, args, this);
|
|
4951
5041
|
}
|
|
4952
5042
|
};
|
|
4953
|
-
_sheet = new WeakMap();
|
|
4954
|
-
_functions2 = new WeakMap();
|
|
4955
|
-
_evaluating = new WeakMap();
|
|
4956
|
-
_scopes = new WeakMap();
|
|
4957
|
-
_FormulaContext_instances = new WeakSet();
|
|
4958
|
-
lookupScope_fn = function(name) {
|
|
4959
|
-
if (__privateGet(this, _scopes).length === 0) return void 0;
|
|
4960
|
-
const key = name.toUpperCase();
|
|
4961
|
-
for (let i = __privateGet(this, _scopes).length - 1; i >= 0; i--) {
|
|
4962
|
-
const v = __privateGet(this, _scopes)[i].get(key);
|
|
4963
|
-
if (v !== void 0) return v;
|
|
4964
|
-
}
|
|
4965
|
-
return void 0;
|
|
4966
|
-
};
|
|
4967
|
-
/**
|
|
4968
|
-
* A sibling context bound to another sheet, sharing this one's circular-
|
|
4969
|
-
* reference guard and range memo. These contexts only read values — they
|
|
4970
|
-
* never evaluate an AST — so the name resolver and recursion budget are
|
|
4971
|
-
* deliberately not carried over; passing them would suggest a scoping
|
|
4972
|
-
* relationship that does not exist. The memo, on the other hand, must be
|
|
4973
|
-
* carried: cross-sheet ranges are the most expensive reads in a workbook.
|
|
4974
|
-
*/
|
|
4975
|
-
forSheet_fn = function(sheetName) {
|
|
4976
|
-
const sheet = this.workbook.getWorksheet(sheetName);
|
|
4977
|
-
const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, __privateGet(this, _evaluating), __privateGet(this, _functions2));
|
|
4978
|
-
ctx.workbook = this.workbook;
|
|
4979
|
-
ctx.rangeCache = this.rangeCache;
|
|
4980
|
-
return ctx;
|
|
4981
|
-
};
|
|
4982
|
-
var FormulaContext = _FormulaContext;
|
|
4983
5043
|
|
|
4984
5044
|
// src/lru-cache.ts
|
|
4985
|
-
var _map, _capacity, _hits, _misses, _evictions;
|
|
4986
5045
|
var LruCache = class {
|
|
5046
|
+
#map;
|
|
5047
|
+
#capacity;
|
|
5048
|
+
#hits = 0;
|
|
5049
|
+
#misses = 0;
|
|
5050
|
+
#evictions = 0;
|
|
4987
5051
|
constructor(capacity) {
|
|
4988
|
-
__privateAdd(this, _map);
|
|
4989
|
-
__privateAdd(this, _capacity);
|
|
4990
|
-
__privateAdd(this, _hits, 0);
|
|
4991
|
-
__privateAdd(this, _misses, 0);
|
|
4992
|
-
__privateAdd(this, _evictions, 0);
|
|
4993
5052
|
if (capacity <= 0) throw new RangeError("LruCache capacity must be > 0.");
|
|
4994
|
-
|
|
4995
|
-
|
|
5053
|
+
this.#capacity = capacity;
|
|
5054
|
+
this.#map = /* @__PURE__ */ new Map();
|
|
4996
5055
|
}
|
|
4997
5056
|
get capacity() {
|
|
4998
|
-
return
|
|
5057
|
+
return this.#capacity;
|
|
4999
5058
|
}
|
|
5000
5059
|
get size() {
|
|
5001
|
-
return
|
|
5060
|
+
return this.#map.size;
|
|
5002
5061
|
}
|
|
5003
5062
|
get hits() {
|
|
5004
|
-
return
|
|
5063
|
+
return this.#hits;
|
|
5005
5064
|
}
|
|
5006
5065
|
get misses() {
|
|
5007
|
-
return
|
|
5066
|
+
return this.#misses;
|
|
5008
5067
|
}
|
|
5009
5068
|
get evictions() {
|
|
5010
|
-
return
|
|
5069
|
+
return this.#evictions;
|
|
5011
5070
|
}
|
|
5012
5071
|
get hitRate() {
|
|
5013
|
-
const total =
|
|
5014
|
-
return total === 0 ? 0 :
|
|
5072
|
+
const total = this.#hits + this.#misses;
|
|
5073
|
+
return total === 0 ? 0 : this.#hits / total;
|
|
5015
5074
|
}
|
|
5016
5075
|
/**
|
|
5017
5076
|
* Returns the value for key, or undefined if not present.
|
|
5018
5077
|
* Moves the accessed key to the "most recently used" end.
|
|
5019
5078
|
*/
|
|
5020
5079
|
get(key) {
|
|
5021
|
-
const value2 =
|
|
5080
|
+
const value2 = this.#map.get(key);
|
|
5022
5081
|
if (value2 === void 0) {
|
|
5023
|
-
|
|
5082
|
+
this.#misses++;
|
|
5024
5083
|
return void 0;
|
|
5025
5084
|
}
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5085
|
+
this.#map.delete(key);
|
|
5086
|
+
this.#map.set(key, value2);
|
|
5087
|
+
this.#hits++;
|
|
5029
5088
|
return value2;
|
|
5030
5089
|
}
|
|
5031
5090
|
/**
|
|
@@ -5033,44 +5092,39 @@ var LruCache = class {
|
|
|
5033
5092
|
* Evicts the least-recently-used entry if over capacity.
|
|
5034
5093
|
*/
|
|
5035
5094
|
set(key, value2) {
|
|
5036
|
-
if (
|
|
5037
|
-
|
|
5038
|
-
} else if (
|
|
5039
|
-
const oldest =
|
|
5040
|
-
|
|
5041
|
-
|
|
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++;
|
|
5042
5101
|
}
|
|
5043
|
-
|
|
5102
|
+
this.#map.set(key, value2);
|
|
5044
5103
|
}
|
|
5045
5104
|
/** Removes a key from the cache. Returns true if it existed. */
|
|
5046
5105
|
delete(key) {
|
|
5047
|
-
return
|
|
5106
|
+
return this.#map.delete(key);
|
|
5048
5107
|
}
|
|
5049
5108
|
/** Returns true if the key is present (does NOT update LRU order). */
|
|
5050
5109
|
has(key) {
|
|
5051
|
-
return
|
|
5110
|
+
return this.#map.has(key);
|
|
5052
5111
|
}
|
|
5053
5112
|
/** Removes all entries and resets statistics. */
|
|
5054
5113
|
clear() {
|
|
5055
|
-
|
|
5056
|
-
|
|
5114
|
+
this.#map.clear();
|
|
5115
|
+
this.#hits = this.#misses = this.#evictions = 0;
|
|
5057
5116
|
}
|
|
5058
5117
|
/** Resizes the cache, evicting oldest entries if necessary. */
|
|
5059
5118
|
resize(newCapacity) {
|
|
5060
5119
|
if (newCapacity <= 0) throw new RangeError("Capacity must be > 0.");
|
|
5061
|
-
|
|
5062
|
-
while (
|
|
5063
|
-
const oldest =
|
|
5064
|
-
|
|
5065
|
-
|
|
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++;
|
|
5066
5125
|
}
|
|
5067
5126
|
}
|
|
5068
5127
|
};
|
|
5069
|
-
_map = new WeakMap();
|
|
5070
|
-
_capacity = new WeakMap();
|
|
5071
|
-
_hits = new WeakMap();
|
|
5072
|
-
_misses = new WeakMap();
|
|
5073
|
-
_evictions = new WeakMap();
|
|
5074
5128
|
|
|
5075
5129
|
// src/formula-evaluator.ts
|
|
5076
5130
|
var DEFAULT_CACHE_CAPACITY = 1e4;
|
|
@@ -5178,143 +5232,126 @@ var TILE_ROWS = 64;
|
|
|
5178
5232
|
var TILE_COLS = 64;
|
|
5179
5233
|
var MAX_TILES_PER_RANGE = 32;
|
|
5180
5234
|
var DEFAULT_MAX_CELLS = 2e6;
|
|
5181
|
-
var
|
|
5182
|
-
|
|
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;
|
|
5183
5249
|
constructor(maxCells = DEFAULT_MAX_CELLS) {
|
|
5184
|
-
|
|
5185
|
-
/** `SHEET r0 c0 r1 c1` → the memoised array value. Insertion-ordered (FIFO eviction). */
|
|
5186
|
-
__privateAdd(this, _entries, /* @__PURE__ */ new Map());
|
|
5187
|
-
/** `sheet tr tc` → entry keys whose rectangle overlaps that tile. */
|
|
5188
|
-
__privateAdd(this, _tiles, /* @__PURE__ */ new Map());
|
|
5189
|
-
/** sheet → entry keys whose rectangle is too broad to tile. */
|
|
5190
|
-
__privateAdd(this, _wide, /* @__PURE__ */ new Map());
|
|
5191
|
-
/** entry key → buckets it was filed under, for cheap removal. */
|
|
5192
|
-
__privateAdd(this, _placements, /* @__PURE__ */ new Map());
|
|
5193
|
-
/** entry key → its area, so eviction can give the budget back. */
|
|
5194
|
-
__privateAdd(this, _sizes, /* @__PURE__ */ new Map());
|
|
5195
|
-
/** Number of cells across all cached rectangles, kept under {@link #maxCells}. */
|
|
5196
|
-
__privateAdd(this, _cells, 0);
|
|
5197
|
-
__privateAdd(this, _maxCells);
|
|
5198
|
-
this.hits = 0;
|
|
5199
|
-
this.misses = 0;
|
|
5200
|
-
__privateSet(this, _maxCells, maxCells);
|
|
5250
|
+
this.#maxCells = maxCells;
|
|
5201
5251
|
}
|
|
5202
5252
|
get size() {
|
|
5203
|
-
return
|
|
5253
|
+
return this.#entries.size;
|
|
5204
5254
|
}
|
|
5205
5255
|
get cachedCells() {
|
|
5206
|
-
return
|
|
5256
|
+
return this.#cells;
|
|
5207
5257
|
}
|
|
5208
5258
|
static key(sheet, r0, c0, r1, c1) {
|
|
5209
5259
|
return `${sheet} ${r0} ${c0} ${r1} ${c1}`;
|
|
5210
5260
|
}
|
|
5211
5261
|
get(sheet, r0, c0, r1, c1) {
|
|
5212
|
-
const hit =
|
|
5262
|
+
const hit = this.#entries.get(_RangeCache.key(sheet, r0, c0, r1, c1));
|
|
5213
5263
|
if (hit === void 0) this.misses++;
|
|
5214
5264
|
else this.hits++;
|
|
5215
|
-
return hit;
|
|
5265
|
+
return hit?.value;
|
|
5216
5266
|
}
|
|
5217
5267
|
/** Memoises a rectangle's value. `cells` is its area, for the budget. */
|
|
5218
5268
|
set(sheet, r0, c0, r1, c1, value2, cells) {
|
|
5219
|
-
if (cells <= 0 || cells >
|
|
5269
|
+
if (cells <= 0 || cells > this.#maxCells) return;
|
|
5220
5270
|
const key = _RangeCache.key(sheet, r0, c0, r1, c1);
|
|
5221
|
-
if (
|
|
5222
|
-
|
|
5223
|
-
|
|
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;
|
|
5224
5274
|
const buckets = [];
|
|
5225
5275
|
const tileRow0 = r0 / TILE_ROWS | 0, tileRow1 = r1 / TILE_ROWS | 0;
|
|
5226
5276
|
const tileCol0 = c0 / TILE_COLS | 0, tileCol1 = c1 / TILE_COLS | 0;
|
|
5227
5277
|
const tileCount = (tileRow1 - tileRow0 + 1) * (tileCol1 - tileCol0 + 1);
|
|
5228
5278
|
if (tileCount > MAX_TILES_PER_RANGE) {
|
|
5229
|
-
|
|
5279
|
+
this.#fileIn(this.#wide, sheet, key, buckets);
|
|
5230
5280
|
} else {
|
|
5231
5281
|
for (let tr = tileRow0; tr <= tileRow1; tr++) {
|
|
5232
5282
|
for (let tc = tileCol0; tc <= tileCol1; tc++) {
|
|
5233
|
-
|
|
5234
|
-
__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);
|
|
5235
5284
|
}
|
|
5236
5285
|
}
|
|
5237
5286
|
}
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
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;
|
|
5243
5291
|
if (oldest === key) continue;
|
|
5244
|
-
|
|
5292
|
+
this.#drop(oldest);
|
|
5245
5293
|
}
|
|
5246
5294
|
}
|
|
5247
5295
|
}
|
|
5248
|
-
/**
|
|
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
|
+
*/
|
|
5249
5304
|
invalidateCell(sheet, row2, col) {
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
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);
|
|
5255
5310
|
}
|
|
5256
5311
|
}
|
|
5257
|
-
const wide =
|
|
5258
|
-
if (wide) {
|
|
5259
|
-
for (const key of
|
|
5260
|
-
|
|
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);
|
|
5261
5317
|
}
|
|
5262
5318
|
}
|
|
5263
5319
|
}
|
|
5264
5320
|
clear() {
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
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
|
+
}
|
|
5271
5343
|
}
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
_sizes = new WeakMap();
|
|
5278
|
-
_cells = new WeakMap();
|
|
5279
|
-
_maxCells = new WeakMap();
|
|
5280
|
-
_RangeCache_static = new WeakSet();
|
|
5281
|
-
contains_fn = function(key, sheet, row2, col) {
|
|
5282
|
-
const parts = key.split(" ");
|
|
5283
|
-
const n = parts.length;
|
|
5284
|
-
const c1 = +parts[n - 1], r1 = +parts[n - 2], c0 = +parts[n - 3], r0 = +parts[n - 4];
|
|
5285
|
-
if (parts.slice(0, n - 4).join(" ") !== sheet) return false;
|
|
5286
|
-
return row2 >= r0 && row2 <= r1 && col >= c0 && col <= c1;
|
|
5287
|
-
};
|
|
5288
|
-
_RangeCache_instances = new WeakSet();
|
|
5289
|
-
drop_fn = function(key) {
|
|
5290
|
-
if (!__privateGet(this, _entries).delete(key)) return;
|
|
5291
|
-
__privateSet(this, _cells, __privateGet(this, _cells) - (__privateGet(this, _sizes).get(key) ?? 0));
|
|
5292
|
-
__privateGet(this, _sizes).delete(key);
|
|
5293
|
-
const buckets = __privateGet(this, _placements).get(key);
|
|
5294
|
-
if (buckets) {
|
|
5295
|
-
for (const bucket of buckets) {
|
|
5296
|
-
const store = bucket.charCodeAt(0) === 119 ? __privateGet(this, _wide) : __privateGet(this, _tiles);
|
|
5297
|
-
const bucketKey = bucket.slice(2);
|
|
5298
|
-
const set = store.get(bucketKey);
|
|
5299
|
-
if (!set) continue;
|
|
5300
|
-
set.delete(key);
|
|
5301
|
-
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);
|
|
5302
5349
|
}
|
|
5303
|
-
|
|
5350
|
+
if (set.has(entryKey)) return;
|
|
5351
|
+
set.add(entryKey);
|
|
5352
|
+
buckets.push({ store, bucketKey, set });
|
|
5304
5353
|
}
|
|
5305
5354
|
};
|
|
5306
|
-
fileIn_fn = function(store, bucketKey, entryKey, buckets, bucketId) {
|
|
5307
|
-
let set = store.get(bucketKey);
|
|
5308
|
-
if (!set) {
|
|
5309
|
-
set = /* @__PURE__ */ new Set();
|
|
5310
|
-
store.set(bucketKey, set);
|
|
5311
|
-
}
|
|
5312
|
-
if (set.has(entryKey)) return;
|
|
5313
|
-
set.add(entryKey);
|
|
5314
|
-
buckets.push(bucketId);
|
|
5315
|
-
};
|
|
5316
|
-
__privateAdd(_RangeCache, _RangeCache_static);
|
|
5317
|
-
var RangeCache = _RangeCache;
|
|
5318
5355
|
function extractReferences(ast, formulaSheet, registry = FunctionRegistry.default, resolveName2) {
|
|
5319
5356
|
const refs = [];
|
|
5320
5357
|
let volatile = false;
|
|
@@ -5394,24 +5431,20 @@ var MAX_EXACT_SPAN = 16;
|
|
|
5394
5431
|
var MAX_BUCKETS_PER_REF = 64;
|
|
5395
5432
|
var tileOfRow = (row2) => row2 / TILE_ROWS2 | 0;
|
|
5396
5433
|
var tileOfCol = (col) => col / TILE_COLS2 | 0;
|
|
5397
|
-
var _buckets, _placements2, _DependencyIndex_instances, visit_fn, file_fn;
|
|
5398
5434
|
var DependencyIndex = class {
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
/** formula key → the buckets it was filed under, for cheap removal. */
|
|
5413
|
-
__privateAdd(this, _placements2, /* @__PURE__ */ new Map());
|
|
5414
|
-
}
|
|
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();
|
|
5415
5448
|
/** Registers (or re-registers) a formula's precedent rectangles. */
|
|
5416
5449
|
add(formulaKey, refs) {
|
|
5417
5450
|
this.remove(formulaKey);
|
|
@@ -5428,12 +5461,12 @@ var DependencyIndex = class {
|
|
|
5428
5461
|
if (colSpan * rowTiles <= MAX_BUCKETS_PER_REF && rowTiles <= MAX_TILES_PER_REF) {
|
|
5429
5462
|
for (let col = ref.startCol; col <= ref.endCol; col++) {
|
|
5430
5463
|
for (let tr = rowTile0; tr <= rowTile1; tr++) {
|
|
5431
|
-
|
|
5464
|
+
this.#file(`c ${ref.sheet} ${col} ${tr}`, formulaKey, buckets);
|
|
5432
5465
|
}
|
|
5433
5466
|
}
|
|
5434
5467
|
} else {
|
|
5435
5468
|
for (let col = ref.startCol; col <= ref.endCol; col++) {
|
|
5436
|
-
|
|
5469
|
+
this.#file(`C ${ref.sheet} ${col}`, formulaKey, buckets);
|
|
5437
5470
|
}
|
|
5438
5471
|
}
|
|
5439
5472
|
continue;
|
|
@@ -5442,12 +5475,12 @@ var DependencyIndex = class {
|
|
|
5442
5475
|
if (rowSpan * colTiles <= MAX_BUCKETS_PER_REF && colTiles <= MAX_TILES_PER_REF) {
|
|
5443
5476
|
for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
|
|
5444
5477
|
for (let tc = colTile0; tc <= colTile1; tc++) {
|
|
5445
|
-
|
|
5478
|
+
this.#file(`r ${ref.sheet} ${row2} ${tc}`, formulaKey, buckets);
|
|
5446
5479
|
}
|
|
5447
5480
|
}
|
|
5448
5481
|
} else {
|
|
5449
5482
|
for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
|
|
5450
|
-
|
|
5483
|
+
this.#file(`R ${ref.sheet} ${row2}`, formulaKey, buckets);
|
|
5451
5484
|
}
|
|
5452
5485
|
}
|
|
5453
5486
|
continue;
|
|
@@ -5455,31 +5488,31 @@ var DependencyIndex = class {
|
|
|
5455
5488
|
if (rowTiles * colTiles <= MAX_TILES_PER_REF) {
|
|
5456
5489
|
for (let tr = rowTile0; tr <= rowTile1; tr++) {
|
|
5457
5490
|
for (let tc = colTile0; tc <= colTile1; tc++) {
|
|
5458
|
-
|
|
5491
|
+
this.#file(`t ${ref.sheet} ${tr} ${tc}`, formulaKey, buckets);
|
|
5459
5492
|
}
|
|
5460
5493
|
}
|
|
5461
5494
|
continue;
|
|
5462
5495
|
}
|
|
5463
|
-
|
|
5496
|
+
this.#file(`w ${ref.sheet}`, formulaKey, buckets);
|
|
5464
5497
|
}
|
|
5465
|
-
|
|
5498
|
+
this.#placements.set(formulaKey, buckets);
|
|
5466
5499
|
}
|
|
5467
5500
|
/** Forgets a formula. */
|
|
5468
5501
|
remove(formulaKey) {
|
|
5469
|
-
const buckets =
|
|
5502
|
+
const buckets = this.#placements.get(formulaKey);
|
|
5470
5503
|
if (!buckets) return;
|
|
5471
5504
|
for (const bucket of buckets) {
|
|
5472
|
-
const set =
|
|
5505
|
+
const set = this.#buckets.get(bucket);
|
|
5473
5506
|
if (!set) continue;
|
|
5474
5507
|
set.delete(formulaKey);
|
|
5475
|
-
if (set.size === 0)
|
|
5508
|
+
if (set.size === 0) this.#buckets.delete(bucket);
|
|
5476
5509
|
}
|
|
5477
|
-
|
|
5510
|
+
this.#placements.delete(formulaKey);
|
|
5478
5511
|
}
|
|
5479
5512
|
/** Drops every registration. */
|
|
5480
5513
|
clear() {
|
|
5481
|
-
|
|
5482
|
-
|
|
5514
|
+
this.#buckets.clear();
|
|
5515
|
+
this.#placements.clear();
|
|
5483
5516
|
}
|
|
5484
5517
|
/**
|
|
5485
5518
|
* Calls `visit` with the key of each formula that *may* read the given cell.
|
|
@@ -5491,31 +5524,28 @@ var DependencyIndex = class {
|
|
|
5491
5524
|
candidates(sheet, row2, col, visit) {
|
|
5492
5525
|
const tr = tileOfRow(row2);
|
|
5493
5526
|
const tc = tileOfCol(col);
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
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);
|
|
5500
5547
|
}
|
|
5501
5548
|
};
|
|
5502
|
-
_buckets = new WeakMap();
|
|
5503
|
-
_placements2 = new WeakMap();
|
|
5504
|
-
_DependencyIndex_instances = new WeakSet();
|
|
5505
|
-
visit_fn = function(bucket, visit) {
|
|
5506
|
-
const set = __privateGet(this, _buckets).get(bucket);
|
|
5507
|
-
if (set) for (const key of set) visit(key);
|
|
5508
|
-
};
|
|
5509
|
-
file_fn = function(bucket, formulaKey, buckets) {
|
|
5510
|
-
let set = __privateGet(this, _buckets).get(bucket);
|
|
5511
|
-
if (!set) {
|
|
5512
|
-
set = /* @__PURE__ */ new Set();
|
|
5513
|
-
__privateGet(this, _buckets).set(bucket, set);
|
|
5514
|
-
}
|
|
5515
|
-
if (set.has(formulaKey)) return;
|
|
5516
|
-
set.add(formulaKey);
|
|
5517
|
-
buckets.push(bucket);
|
|
5518
|
-
};
|
|
5519
5549
|
|
|
5520
5550
|
// src/recalc-engine.ts
|
|
5521
5551
|
function createWorkbookRecalcModel(workbook) {
|
|
@@ -5548,6 +5578,7 @@ function createWorkbookRecalcModel(workbook) {
|
|
|
5548
5578
|
};
|
|
5549
5579
|
}
|
|
5550
5580
|
var MAX_ADOPTED_SPILL_CELLS = 262144;
|
|
5581
|
+
var resultOf = (fv2) => fv2.isBlank ? 0 : fv2.toObject();
|
|
5551
5582
|
var normSheet = (s) => s.toUpperCase();
|
|
5552
5583
|
var normRef = (r) => r.replace(/\$/g, "").toUpperCase();
|
|
5553
5584
|
var keyOf = (sheet, ref) => `${normSheet(sheet)}!${normRef(ref)}`;
|
|
@@ -5558,42 +5589,70 @@ function parseKey(key) {
|
|
|
5558
5589
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(ref);
|
|
5559
5590
|
return { sheet, ref, row: row2, col: column2 };
|
|
5560
5591
|
}
|
|
5561
|
-
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;
|
|
5562
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();
|
|
5563
5623
|
constructor(model, registry = FunctionRegistry.default) {
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
__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);
|
|
5597
5656
|
}
|
|
5598
5657
|
// ── Registration ─────────────────────────────────────────────────────────────
|
|
5599
5658
|
/**
|
|
@@ -5611,12 +5670,12 @@ var RecalcEngine = class {
|
|
|
5611
5670
|
const { refs, volatile } = extractReferences(
|
|
5612
5671
|
ast,
|
|
5613
5672
|
s,
|
|
5614
|
-
|
|
5615
|
-
|
|
5673
|
+
this.#registry,
|
|
5674
|
+
this.#model.getDefinedName ? (n, sh) => this.#model.getDefinedName(n, sh) : void 0
|
|
5616
5675
|
);
|
|
5617
|
-
|
|
5676
|
+
this.#rememberSheet(sheet);
|
|
5618
5677
|
const key = keyOf(s, r);
|
|
5619
|
-
|
|
5678
|
+
this.#register({
|
|
5620
5679
|
sheet: s,
|
|
5621
5680
|
sheetName: sheet,
|
|
5622
5681
|
ref: r,
|
|
@@ -5628,9 +5687,9 @@ var RecalcEngine = class {
|
|
|
5628
5687
|
refs,
|
|
5629
5688
|
volatile
|
|
5630
5689
|
});
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
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);
|
|
5634
5693
|
}
|
|
5635
5694
|
/**
|
|
5636
5695
|
* Indexes many formulas **without evaluating anything**.
|
|
@@ -5652,7 +5711,7 @@ var RecalcEngine = class {
|
|
|
5652
5711
|
* construct in a large workbook should not abort the load.
|
|
5653
5712
|
*/
|
|
5654
5713
|
registerBulk(entries) {
|
|
5655
|
-
const resolveName2 =
|
|
5714
|
+
const resolveName2 = this.#model.getDefinedName ? (n, sh) => this.#model.getDefinedName(n, sh) : void 0;
|
|
5656
5715
|
let registered = 0;
|
|
5657
5716
|
const failed = [];
|
|
5658
5717
|
for (const entry of entries) {
|
|
@@ -5664,10 +5723,10 @@ var RecalcEngine = class {
|
|
|
5664
5723
|
try {
|
|
5665
5724
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5666
5725
|
const ast = parseFormulaUncached(text);
|
|
5667
|
-
const { refs, volatile } = extractReferences(ast, s,
|
|
5668
|
-
|
|
5726
|
+
const { refs, volatile } = extractReferences(ast, s, this.#registry, resolveName2);
|
|
5727
|
+
this.#rememberSheet(sheet);
|
|
5669
5728
|
const key = keyOf(s, r);
|
|
5670
|
-
|
|
5729
|
+
this.#register({
|
|
5671
5730
|
sheet: s,
|
|
5672
5731
|
sheetName: sheet,
|
|
5673
5732
|
ref: r,
|
|
@@ -5679,15 +5738,60 @@ var RecalcEngine = class {
|
|
|
5679
5738
|
refs,
|
|
5680
5739
|
volatile
|
|
5681
5740
|
});
|
|
5682
|
-
|
|
5741
|
+
this.#adoptDeclaredSpill(s, sheet, r, key, row2, column2);
|
|
5683
5742
|
registered++;
|
|
5684
5743
|
} catch (err) {
|
|
5685
5744
|
failed.push({ sheet, ref, error: err instanceof Error ? err.message : String(err) });
|
|
5686
5745
|
}
|
|
5687
5746
|
}
|
|
5688
|
-
if (registered > 0)
|
|
5747
|
+
if (registered > 0) this.#rangeCache.clear();
|
|
5689
5748
|
return { registered, failed };
|
|
5690
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
|
+
}
|
|
5691
5795
|
/**
|
|
5692
5796
|
* Removes a formula from a cell and recalculates its dependents (which may now
|
|
5693
5797
|
* read a plain input value instead). Returns the recomputed dependent cells.
|
|
@@ -5696,36 +5800,36 @@ var RecalcEngine = class {
|
|
|
5696
5800
|
const s = normSheet(sheet);
|
|
5697
5801
|
const r = normRef(ref);
|
|
5698
5802
|
const key = keyOf(s, r);
|
|
5699
|
-
if (!
|
|
5700
|
-
|
|
5701
|
-
|
|
5803
|
+
if (!this.#formulas.has(key)) return [];
|
|
5804
|
+
this.#unregister(key);
|
|
5805
|
+
this.#model.clearCell?.(sheet, r);
|
|
5702
5806
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5703
|
-
|
|
5704
|
-
return
|
|
5807
|
+
this.#rangeCache.invalidateCell(s, row2, column2);
|
|
5808
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }]);
|
|
5705
5809
|
}
|
|
5706
5810
|
/**
|
|
5707
5811
|
* Writes a raw input value to a cell and recalculates every formula that
|
|
5708
5812
|
* depends on it. Use this for non-formula edits so dependents stay current.
|
|
5709
5813
|
*/
|
|
5710
5814
|
setCellValue(sheet, ref, value2) {
|
|
5711
|
-
|
|
5815
|
+
this.#rememberSheet(sheet);
|
|
5712
5816
|
const s = normSheet(sheet);
|
|
5713
5817
|
const r = normRef(ref);
|
|
5714
5818
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5715
|
-
|
|
5716
|
-
return
|
|
5819
|
+
this.#writeCell(sheet, r, value2, row2, column2);
|
|
5820
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }]);
|
|
5717
5821
|
}
|
|
5718
5822
|
/**
|
|
5719
5823
|
* Signals that a cell changed by some external means (the model was already
|
|
5720
5824
|
* updated) and recalculates its dependents.
|
|
5721
5825
|
*/
|
|
5722
5826
|
onCellChanged(sheet, ref) {
|
|
5723
|
-
|
|
5827
|
+
this.#rememberSheet(sheet);
|
|
5724
5828
|
const s = normSheet(sheet);
|
|
5725
5829
|
const r = normRef(ref);
|
|
5726
5830
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5727
|
-
|
|
5728
|
-
return
|
|
5831
|
+
this.#rangeCache.invalidateCell(s, row2, column2);
|
|
5832
|
+
return this.#recalcFrom([{ sheet: s, row: row2, col: column2 }]);
|
|
5729
5833
|
}
|
|
5730
5834
|
/**
|
|
5731
5835
|
* Recalculates every registered formula.
|
|
@@ -5758,18 +5862,61 @@ var RecalcEngine = class {
|
|
|
5758
5862
|
recalcAll(options) {
|
|
5759
5863
|
const order = options?.order;
|
|
5760
5864
|
if (!order || order.length === 0) {
|
|
5761
|
-
return
|
|
5865
|
+
return this.#evaluatePass([...this.#formulas.values()]).results;
|
|
5762
5866
|
}
|
|
5763
|
-
const attempt =
|
|
5867
|
+
const attempt = this.#recalcInOrder(order);
|
|
5764
5868
|
if (attempt.conflict) {
|
|
5765
5869
|
options?.onOrderConflict?.(attempt.conflict);
|
|
5766
|
-
return
|
|
5870
|
+
return this.#evaluatePass([...this.#formulas.values()]).results;
|
|
5767
5871
|
}
|
|
5768
5872
|
return attempt.results;
|
|
5769
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
|
+
}
|
|
5770
5917
|
/** The precedents recorded for a formula cell (mainly for tests/inspection). */
|
|
5771
5918
|
getPrecedents(sheet, ref) {
|
|
5772
|
-
return
|
|
5919
|
+
return this.#formulas.get(keyOf(sheet, ref))?.refs ?? [];
|
|
5773
5920
|
}
|
|
5774
5921
|
// ── Structural edits ─────────────────────────────────────────────────────────
|
|
5775
5922
|
/**
|
|
@@ -5784,7 +5931,7 @@ var RecalcEngine = class {
|
|
|
5784
5931
|
* engine.onRowsInserted('S1', 3, 2);
|
|
5785
5932
|
*/
|
|
5786
5933
|
onRowsInserted(sheet, at, count2 = 1) {
|
|
5787
|
-
return
|
|
5934
|
+
return this.#applyShift({ dim: "row", kind: "insert", at, count: count2, sheetName: sheet });
|
|
5788
5935
|
}
|
|
5789
5936
|
/**
|
|
5790
5937
|
* Tells the engine rows were deleted on a sheet. Call AFTER the
|
|
@@ -5794,556 +5941,420 @@ var RecalcEngine = class {
|
|
|
5794
5941
|
* volatiles) are recomputed.
|
|
5795
5942
|
*/
|
|
5796
5943
|
onRowsDeleted(sheet, at, count2 = 1) {
|
|
5797
|
-
return
|
|
5944
|
+
return this.#applyShift({ dim: "row", kind: "delete", at, count: count2, sheetName: sheet });
|
|
5798
5945
|
}
|
|
5799
5946
|
/** Column counterpart to {@link onRowsInserted}. */
|
|
5800
5947
|
onColumnsInserted(sheet, at, count2 = 1) {
|
|
5801
|
-
return
|
|
5948
|
+
return this.#applyShift({ dim: "col", kind: "insert", at, count: count2, sheetName: sheet });
|
|
5802
5949
|
}
|
|
5803
5950
|
/** Column counterpart to {@link onRowsDeleted}. */
|
|
5804
5951
|
onColumnsDeleted(sheet, at, count2 = 1) {
|
|
5805
|
-
return
|
|
5806
|
-
}
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
else __privateGet(this, _volatiles).delete(entry.key);
|
|
5841
|
-
};
|
|
5842
|
-
unregister_fn = function(key) {
|
|
5843
|
-
__privateGet(this, _formulas).delete(key);
|
|
5844
|
-
__privateGet(this, _dependents).remove(key);
|
|
5845
|
-
__privateGet(this, _volatiles).delete(key);
|
|
5846
|
-
};
|
|
5847
|
-
rememberSheet_fn = function(name) {
|
|
5848
|
-
__privateGet(this, _sheetNames).set(normSheet(name), name);
|
|
5849
|
-
};
|
|
5850
|
-
/**
|
|
5851
|
-
* Takes ownership of the spill block a *file* already declared for an anchor.
|
|
5852
|
-
*
|
|
5853
|
-
* Excel stores a dynamic array as one formula on the anchor plus the spilled
|
|
5854
|
-
* results written as plain cached values in the cells below/right, and records
|
|
5855
|
-
* the extent in the anchor's `<f t="array" ref="…">`. Without this step the
|
|
5856
|
-
* engine meets those cached values on the first recalculation, sees literal
|
|
5857
|
-
* content inside the block it wants to spill into, and reports `#SPILL!` for
|
|
5858
|
-
* every dynamic array in every real file. Registering the declared block as
|
|
5859
|
-
* this anchor's existing spill makes them what they actually are — the anchor's
|
|
5860
|
-
* own territory, free to overwrite — and reuses the ordinary shrink path, so a
|
|
5861
|
-
* result smaller than the declared block clears the leftovers.
|
|
5862
|
-
*
|
|
5863
|
-
* Cost is one Set entry per already-spilled cell, paid once at load. A declared
|
|
5864
|
-
* block far larger than any real spill (a corrupt or hand-written `ref`) is
|
|
5865
|
-
* skipped rather than materialised.
|
|
5866
|
-
*/
|
|
5867
|
-
adoptDeclaredSpill_fn = function(s, sheetName, ref, anchorKey, row2, col) {
|
|
5868
|
-
const declared = __privateGet(this, _model).getSpillRange?.(sheetName, ref);
|
|
5869
|
-
if (!declared || !declared.includes(":")) return;
|
|
5870
|
-
let rect;
|
|
5871
|
-
try {
|
|
5872
|
-
rect = puredocsExcel.parseRangeRef(declared);
|
|
5873
|
-
} catch {
|
|
5874
|
-
return;
|
|
5875
|
-
}
|
|
5876
|
-
if (rect.startRow !== row2 || rect.startColumn !== col) return;
|
|
5877
|
-
const height = rect.endRow - rect.startRow + 1;
|
|
5878
|
-
const width = rect.endColumn - rect.startColumn + 1;
|
|
5879
|
-
if (height < 1 || width < 1) return;
|
|
5880
|
-
if (height * width > MAX_ADOPTED_SPILL_CELLS) return;
|
|
5881
|
-
if (height === 1 && width === 1) return;
|
|
5882
|
-
const members = /* @__PURE__ */ new Set();
|
|
5883
|
-
for (let r = rect.startRow; r <= rect.endRow; r++) {
|
|
5884
|
-
for (let c = rect.startColumn; c <= rect.endColumn; c++) {
|
|
5885
|
-
if (r === row2 && c === col) continue;
|
|
5886
|
-
const key = keyOf(s, puredocsExcel.cellRefFromRowCol(r, c));
|
|
5887
|
-
if (__privateGet(this, _formulas).has(key)) continue;
|
|
5888
|
-
if (__privateGet(this, _spillOwner).has(key)) continue;
|
|
5889
|
-
members.add(key);
|
|
5890
|
-
__privateGet(this, _spillOwner).set(key, anchorKey);
|
|
5891
|
-
}
|
|
5892
|
-
}
|
|
5893
|
-
if (members.size > 0) __privateGet(this, _spills).set(anchorKey, members);
|
|
5894
|
-
};
|
|
5895
|
-
/**
|
|
5896
|
-
* One pass in a caller-supplied order, watching for inversions.
|
|
5897
|
-
*
|
|
5898
|
-
* The check is the mirror image of the topological sort: instead of asking
|
|
5899
|
-
* "which of my precedents come first?", it asks, of each formula just
|
|
5900
|
-
* evaluated, "has anything that reads me already run?". The dependency index
|
|
5901
|
-
* answers that directly, and one positive answer is enough to know the order
|
|
5902
|
-
* cannot be trusted.
|
|
5903
|
-
*/
|
|
5904
|
-
recalcInOrder_fn = function(order) {
|
|
5905
|
-
const results = [];
|
|
5906
|
-
const changed = [];
|
|
5907
|
-
const done = /* @__PURE__ */ new Set();
|
|
5908
|
-
let spilled = false;
|
|
5909
|
-
for (const { sheet, ref } of order) {
|
|
5910
|
-
const key = keyOf(sheet, ref);
|
|
5911
|
-
if (done.has(key)) continue;
|
|
5912
|
-
const entry = __privateGet(this, _formulas).get(key);
|
|
5913
|
-
if (!entry) continue;
|
|
5914
|
-
done.add(key);
|
|
5915
|
-
const written = __privateMethod(this, _RecalcEngine_instances, writeResult_fn).call(this, entry);
|
|
5916
|
-
results.push(written.result);
|
|
5917
|
-
changed.push(...written.changed);
|
|
5918
|
-
if (written.result.spill) spilled = true;
|
|
5919
|
-
let conflict;
|
|
5920
|
-
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: entry.sheet, row: entry.row, col: entry.col }, (dep) => {
|
|
5921
|
-
if (conflict || dep.key === entry.key) return;
|
|
5922
|
-
if (done.has(dep.key)) conflict = { dependent: dep.key, precedent: entry.key };
|
|
5923
|
-
});
|
|
5924
|
-
if (conflict) return { results: [], conflict };
|
|
5925
|
-
}
|
|
5926
|
-
const rest = [...__privateGet(this, _formulas).values()].filter((e) => !done.has(e.key));
|
|
5927
|
-
if (rest.length > 0) {
|
|
5928
|
-
const pass = __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, rest);
|
|
5929
|
-
results.push(...pass.results);
|
|
5930
|
-
changed.push(...pass.changed);
|
|
5931
|
-
}
|
|
5932
|
-
if (spilled) {
|
|
5933
|
-
const evaluated = new Set(done);
|
|
5934
|
-
results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, changed, evaluated), evaluated));
|
|
5935
|
-
}
|
|
5936
|
-
return { results };
|
|
5937
|
-
};
|
|
5938
|
-
applyShift_fn = function(shift) {
|
|
5939
|
-
const editedSheet = normSheet(shift.sheetName);
|
|
5940
|
-
const isRowShift = shift.dim === "row";
|
|
5941
|
-
const bandEnd = shift.at + shift.count - 1;
|
|
5942
|
-
const rekeyed = [];
|
|
5943
|
-
const needsRecalc = /* @__PURE__ */ new Set();
|
|
5944
|
-
for (const e of __privateGet(this, _formulas).values()) {
|
|
5945
|
-
let row2 = e.row;
|
|
5946
|
-
let col = e.col;
|
|
5947
|
-
if (e.sheet === editedSheet) {
|
|
5948
|
-
const shifted = puredocsExcel.shiftIndex(isRowShift ? row2 : col, shift);
|
|
5949
|
-
if (shifted === null) continue;
|
|
5950
|
-
if (isRowShift) row2 = shifted;
|
|
5951
|
-
else col = shifted;
|
|
5952
|
-
}
|
|
5953
|
-
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));
|
|
5954
|
-
const rewritten = puredocsExcel.shiftFormulaRefs(e.formula, shift, e.sheetName);
|
|
5955
|
-
const moved = row2 !== e.row || col !== e.col;
|
|
5956
|
-
const ref = moved ? puredocsExcel.cellRefFromRowCol(row2, col) : e.ref;
|
|
5957
|
-
const key = moved ? keyOf(e.sheet, ref) : e.key;
|
|
5958
|
-
rekeyed.push({
|
|
5959
|
-
...e,
|
|
5960
|
-
ref,
|
|
5961
|
-
key,
|
|
5962
|
-
row: row2,
|
|
5963
|
-
col,
|
|
5964
|
-
formula: rewritten.changed ? rewritten.text : e.formula,
|
|
5965
|
-
// A rewritten formula's cached AST no longer matches its text; it is
|
|
5966
|
-
// re-parsed lazily, and only if the cell is actually evaluated again.
|
|
5967
|
-
ast: rewritten.changed ? void 0 : e.ast,
|
|
5968
|
-
refs: __privateMethod(this, _RecalcEngine_instances, shiftPrecedents_fn).call(this, e.refs, shift, editedSheet)
|
|
5969
|
-
// Volatility is a property of which *functions* a formula calls, and
|
|
5970
|
-
// rewriting references cannot change that.
|
|
5971
|
-
});
|
|
5972
|
-
if (touchedBand || rewritten.hasRefError) needsRecalc.add(key);
|
|
5973
|
-
}
|
|
5974
|
-
__privateSet(this, _formulas, /* @__PURE__ */ new Map());
|
|
5975
|
-
__privateGet(this, _dependents).clear();
|
|
5976
|
-
__privateGet(this, _volatiles).clear();
|
|
5977
|
-
for (const e of rekeyed) __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, e);
|
|
5978
|
-
__privateMethod(this, _RecalcEngine_instances, shiftSpillBookkeeping_fn).call(this, shift, editedSheet);
|
|
5979
|
-
__privateGet(this, _rangeCache).clear();
|
|
5980
|
-
const pending = /* @__PURE__ */ new Map();
|
|
5981
|
-
for (const k of needsRecalc) {
|
|
5982
|
-
const e = __privateGet(this, _formulas).get(k);
|
|
5983
|
-
if (e) pending.set(k, e);
|
|
5984
|
-
}
|
|
5985
|
-
for (const e of __privateGet(this, _formulas).values()) {
|
|
5986
|
-
if (e.volatile) pending.set(e.key, e);
|
|
5987
|
-
}
|
|
5988
|
-
return __privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, /* @__PURE__ */ new Set());
|
|
5989
|
-
};
|
|
5990
|
-
/**
|
|
5991
|
-
* Translates precedent rectangles across a structural edit.
|
|
5992
|
-
*
|
|
5993
|
-
* This is arithmetic rather than a re-parse of the rewritten formula, and the
|
|
5994
|
-
* two are equivalent by construction: `shiftFormulaRefs` derives the new
|
|
5995
|
-
* reference text by calling `shiftSpan`/`shiftIndex` on exactly these numbers,
|
|
5996
|
-
* so applying the same functions to the stored rectangle yields the same
|
|
5997
|
-
* region. A rectangle that is entirely deleted disappears, matching the
|
|
5998
|
-
* `#REF!` the text rewrite produces.
|
|
5999
|
-
*/
|
|
6000
|
-
shiftPrecedents_fn = function(refs, shift, editedSheet) {
|
|
6001
|
-
const out = [];
|
|
6002
|
-
for (const r of refs) {
|
|
6003
|
-
if (r.sheet !== editedSheet) {
|
|
6004
|
-
out.push(r);
|
|
6005
|
-
continue;
|
|
6006
|
-
}
|
|
6007
|
-
if (shift.dim === "row") {
|
|
6008
|
-
const span = puredocsExcel.shiftSpan(r.startRow, r.endRow, shift);
|
|
6009
|
-
if (!span) continue;
|
|
6010
|
-
out.push({
|
|
6011
|
-
sheet: r.sheet,
|
|
6012
|
-
startRow: span.start,
|
|
6013
|
-
endRow: Math.min(span.end, puredocsExcel.EXCEL_MAX_ROWS),
|
|
6014
|
-
startCol: r.startCol,
|
|
6015
|
-
endCol: r.endCol
|
|
6016
|
-
});
|
|
6017
|
-
} else {
|
|
6018
|
-
const span = puredocsExcel.shiftSpan(r.startCol, r.endCol, shift);
|
|
6019
|
-
if (!span) continue;
|
|
6020
|
-
out.push({
|
|
6021
|
-
sheet: r.sheet,
|
|
6022
|
-
startRow: r.startRow,
|
|
6023
|
-
endRow: r.endRow,
|
|
6024
|
-
startCol: span.start,
|
|
6025
|
-
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.
|
|
6026
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);
|
|
6027
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());
|
|
6028
6005
|
}
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
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();
|
|
6046
6071
|
for (const m of members) {
|
|
6047
6072
|
const shifted = shiftCellKey(m);
|
|
6048
6073
|
if (!shifted) continue;
|
|
6049
|
-
|
|
6050
|
-
|
|
6074
|
+
newMembers.add(shifted);
|
|
6075
|
+
this.#spillOwner.set(shifted, newAnchor);
|
|
6051
6076
|
}
|
|
6052
|
-
|
|
6077
|
+
newSpills.set(newAnchor, newMembers);
|
|
6053
6078
|
}
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
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);
|
|
6060
6122
|
}
|
|
6061
|
-
newSpills.set(newAnchor, newMembers);
|
|
6062
6123
|
}
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
__privateMethod(this, _RecalcEngine_instances, addVolatiles_fn).call(this, pending);
|
|
6081
|
-
if (includeKey) {
|
|
6082
|
-
const self = __privateGet(this, _formulas).get(includeKey);
|
|
6083
|
-
if (self) pending.set(includeKey, self);
|
|
6084
|
-
}
|
|
6085
|
-
results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, evaluated));
|
|
6086
|
-
return results;
|
|
6087
|
-
};
|
|
6088
|
-
/** Evaluates `pending` entries to a fixpoint, following spill-induced changes. */
|
|
6089
|
-
runFixpoint_fn = function(pending, evaluated) {
|
|
6090
|
-
const results = [];
|
|
6091
|
-
let guard = 0;
|
|
6092
|
-
const maxIterations = __privateGet(this, _formulas).size + 8;
|
|
6093
|
-
while (pending.size > 0 && guard++ <= maxIterations) {
|
|
6094
|
-
const { results: passResults, changed } = __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...pending.values()]);
|
|
6095
|
-
results.push(...passResults);
|
|
6096
|
-
for (const k of pending.keys()) evaluated.add(k);
|
|
6097
|
-
pending = __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, changed, evaluated);
|
|
6098
|
-
}
|
|
6099
|
-
return results;
|
|
6100
|
-
};
|
|
6101
|
-
/** Adds every volatile formula to a pending set. */
|
|
6102
|
-
addVolatiles_fn = function(pending) {
|
|
6103
|
-
for (const key of __privateGet(this, _volatiles)) {
|
|
6104
|
-
const e = __privateGet(this, _formulas).get(key);
|
|
6105
|
-
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;
|
|
6106
6141
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
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
|
+
}
|
|
6122
6157
|
});
|
|
6123
6158
|
}
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
*/
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
const
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
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);
|
|
6139
6192
|
}
|
|
6140
6193
|
}
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: pe.sheet, row: pe.row, col: pe.col }, (dep) => {
|
|
6160
|
-
if (dep.key === pe.key || !inSet.has(dep.key) || seen.has(dep.key)) return;
|
|
6161
|
-
seen.add(dep.key);
|
|
6162
|
-
dependents.get(pe.key).push(dep.key);
|
|
6163
|
-
indegree.set(dep.key, indegree.get(dep.key) + 1);
|
|
6164
|
-
});
|
|
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 };
|
|
6165
6212
|
}
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
};
|
|
6197
|
-
/**
|
|
6198
|
-
* Evaluates one formula and writes its result. Scalars go to the anchor cell.
|
|
6199
|
-
* Arrays spill across a block anchored at the formula cell: on a collision
|
|
6200
|
-
* with existing content the anchor becomes #SPILL! and nothing spills; when
|
|
6201
|
-
* an array shrinks, previously-spilled cells it no longer covers are cleared.
|
|
6202
|
-
*/
|
|
6203
|
-
writeResult_fn = function(e) {
|
|
6204
|
-
const anchorKey = e.key;
|
|
6205
|
-
const fv2 = __privateMethod(this, _RecalcEngine_instances, evaluateFormulaValue_fn).call(this, e);
|
|
6206
|
-
const changed = [];
|
|
6207
|
-
const prevSpill = __privateGet(this, _spills).get(anchorKey) ?? /* @__PURE__ */ new Set();
|
|
6208
|
-
if (!fv2.isArray) {
|
|
6209
|
-
__privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
|
|
6210
|
-
__privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
|
|
6211
|
-
const value2 = fv2.toObject();
|
|
6212
|
-
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
|
|
6213
|
-
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
6214
|
-
return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
|
|
6215
|
-
}
|
|
6216
|
-
const arr = fv2.arrayVal;
|
|
6217
|
-
const rows2 = arr.rows, cols = arr.columns;
|
|
6218
|
-
let collides = false;
|
|
6219
|
-
for (let dr = 0; dr < rows2 && !collides; dr++) {
|
|
6220
|
-
for (let dc = 0; dc < cols && !collides; dc++) {
|
|
6221
|
-
if (dr === 0 && dc === 0) continue;
|
|
6222
|
-
const r = e.row + dr, c = e.col + dc;
|
|
6223
|
-
const cellKey2 = keyOf(e.sheet, puredocsExcel.cellRefFromRowCol(r, c));
|
|
6224
|
-
if (__privateMethod(this, _RecalcEngine_instances, isOccupied_fn).call(this, cellKey2, e.sheetName, puredocsExcel.cellRefFromRowCol(r, c), anchorKey, prevSpill)) {
|
|
6225
|
-
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
|
+
}
|
|
6226
6243
|
}
|
|
6227
6244
|
}
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
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
|
+
}
|
|
6248
6265
|
}
|
|
6249
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
|
+
};
|
|
6250
6281
|
}
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
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
|
+
});
|
|
6257
6317
|
}
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
if (__privateGet(this, _formulas).has(cellKey2) && cellKey2 !== anchorKey) return true;
|
|
6269
|
-
const owner = __privateGet(this, _spillOwner).get(cellKey2);
|
|
6270
|
-
if (owner && owner !== anchorKey) return true;
|
|
6271
|
-
if (prevSpill.has(cellKey2)) return false;
|
|
6272
|
-
const v = __privateGet(this, _model).getCellValue(sheetName, ref);
|
|
6273
|
-
return v !== null && v !== void 0 && v !== "";
|
|
6274
|
-
};
|
|
6275
|
-
/** Clears an anchor's spilled cells (not the anchor itself) and forgets them. */
|
|
6276
|
-
clearSpill_fn = function(anchorKey, changed) {
|
|
6277
|
-
const spill = __privateGet(this, _spills).get(anchorKey);
|
|
6278
|
-
if (!spill) return;
|
|
6279
|
-
for (const cellKey2 of spill) {
|
|
6280
|
-
const cell = parseKey(cellKey2);
|
|
6281
|
-
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
6282
|
-
__privateGet(this, _spillOwner).delete(cellKey2);
|
|
6283
|
-
changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
|
|
6284
|
-
}
|
|
6285
|
-
__privateGet(this, _spills).delete(anchorKey);
|
|
6286
|
-
};
|
|
6287
|
-
evaluateFormulaValue_fn = function(e) {
|
|
6288
|
-
if (!e.ast) {
|
|
6289
|
-
try {
|
|
6290
|
-
e.ast = parseFormulaUncached(e.formula);
|
|
6291
|
-
} catch {
|
|
6292
|
-
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);
|
|
6293
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
|
+
};
|
|
6294
6357
|
}
|
|
6295
|
-
return evaluateAst(e.ast, __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName), {
|
|
6296
|
-
workbook: __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this),
|
|
6297
|
-
formulaRow: e.row,
|
|
6298
|
-
formulaCol: e.col,
|
|
6299
|
-
rangeCache: __privateGet(this, _rangeCache)
|
|
6300
|
-
});
|
|
6301
|
-
};
|
|
6302
|
-
/**
|
|
6303
|
-
* Per-sheet evaluator adapter. Memoised: these hold no per-formula state, and
|
|
6304
|
-
* rebuilding one (with its four closures) for every formula evaluated made a
|
|
6305
|
-
* full recalc allocate several objects per cell.
|
|
6306
|
-
*/
|
|
6307
|
-
sheetLike_fn = function(sheetName) {
|
|
6308
|
-
let cached = __privateGet(this, _sheetLikes).get(sheetName);
|
|
6309
|
-
if (!cached) {
|
|
6310
|
-
cached = __privateMethod(this, _RecalcEngine_instances, buildSheetLike_fn).call(this, sheetName);
|
|
6311
|
-
__privateGet(this, _sheetLikes).set(sheetName, cached);
|
|
6312
|
-
}
|
|
6313
|
-
return cached;
|
|
6314
|
-
};
|
|
6315
|
-
buildSheetLike_fn = function(sheetName) {
|
|
6316
|
-
const model = __privateGet(this, _model);
|
|
6317
|
-
return {
|
|
6318
|
-
name: sheetName,
|
|
6319
|
-
getCellValue: (ref) => __privateGet(this, _model).getCellValue(sheetName, ref),
|
|
6320
|
-
getCellValueAt: model.getCellValueAt ? (row2, column2) => model.getCellValueAt(sheetName, row2, column2) : void 0,
|
|
6321
|
-
// A getter, not a snapshot: the extent grows as formulas write results,
|
|
6322
|
-
// and a whole-column reference read later in the same pass must see it.
|
|
6323
|
-
get usedBounds() {
|
|
6324
|
-
return model.getUsedBounds?.(sheetName);
|
|
6325
|
-
},
|
|
6326
|
-
isRowHidden: __privateGet(this, _model).isRowHidden ? (row2) => __privateGet(this, _model).isRowHidden(sheetName, row2) : void 0,
|
|
6327
|
-
getCellFormula: __privateGet(this, _model).getCellFormula ? (ref) => __privateGet(this, _model).getCellFormula(sheetName, ref) : void 0,
|
|
6328
|
-
getSpillRange: __privateGet(this, _model).getSpillRange ? (ref) => __privateGet(this, _model).getSpillRange(sheetName, ref) : void 0
|
|
6329
|
-
};
|
|
6330
|
-
};
|
|
6331
|
-
buildWorkbookLike_fn = function() {
|
|
6332
|
-
if (__privateGet(this, _workbookLike)) return __privateGet(this, _workbookLike);
|
|
6333
|
-
const self = this;
|
|
6334
|
-
return __privateSet(this, _workbookLike, {
|
|
6335
|
-
get worksheets() {
|
|
6336
|
-
return [...__privateGet(self, _sheetNames).values()].map((n) => {
|
|
6337
|
-
var _a;
|
|
6338
|
-
return __privateMethod(_a = self, _RecalcEngine_instances, sheetLike_fn).call(_a, n);
|
|
6339
|
-
});
|
|
6340
|
-
},
|
|
6341
|
-
getWorksheet: (name) => {
|
|
6342
|
-
var _a;
|
|
6343
|
-
return __privateMethod(_a = self, _RecalcEngine_instances, sheetLike_fn).call(_a, name);
|
|
6344
|
-
},
|
|
6345
|
-
getDefinedName: __privateGet(this, _model).getDefinedName ? (name, sheet) => __privateGet(this, _model).getDefinedName(name, sheet) : void 0
|
|
6346
|
-
});
|
|
6347
6358
|
};
|
|
6348
6359
|
|
|
6349
6360
|
exports.ArrayValue = ArrayValue;
|