@devmm/puredocs-excel-formula 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1130 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +270 -18
- package/dist/index.d.ts +270 -18
- package/dist/index.js +1128 -125
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -804,6 +804,19 @@ var SheetRangeReferenceNode = class extends FormulaNode {
|
|
|
804
804
|
return ctx.getSheetRangeValues(this.sheetName, this.startRef, this.endRef);
|
|
805
805
|
}
|
|
806
806
|
};
|
|
807
|
+
var FullRangeReferenceNode = class extends FormulaNode {
|
|
808
|
+
constructor(startCol, endCol, startRow, endRow, sheetName) {
|
|
809
|
+
super();
|
|
810
|
+
this.startCol = startCol;
|
|
811
|
+
this.endCol = endCol;
|
|
812
|
+
this.startRow = startRow;
|
|
813
|
+
this.endRow = endRow;
|
|
814
|
+
this.sheetName = sheetName;
|
|
815
|
+
}
|
|
816
|
+
evaluate(ctx) {
|
|
817
|
+
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);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
807
820
|
var NamedRangeNode = class extends FormulaNode {
|
|
808
821
|
constructor(name) {
|
|
809
822
|
super();
|
|
@@ -1023,7 +1036,7 @@ var CallNode = class extends FormulaNode {
|
|
|
1023
1036
|
};
|
|
1024
1037
|
|
|
1025
1038
|
// src/formula-parser.ts
|
|
1026
|
-
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, parseFunction_fn, parseArgList_fn;
|
|
1039
|
+
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, parseFunction_fn, parseArgList_fn;
|
|
1027
1040
|
var FormulaParser = class {
|
|
1028
1041
|
constructor(tokens) {
|
|
1029
1042
|
__privateAdd(this, _FormulaParser_instances);
|
|
@@ -1147,6 +1160,10 @@ parsePrimary_fn = function() {
|
|
|
1147
1160
|
switch (token.type) {
|
|
1148
1161
|
case 0 /* Number */: {
|
|
1149
1162
|
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1163
|
+
if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
|
|
1164
|
+
const full = __privateMethod(this, _FormulaParser_instances, tryParseFullRange_fn).call(this, token.value);
|
|
1165
|
+
if (full) return full;
|
|
1166
|
+
}
|
|
1150
1167
|
const n = parseFloat(token.value);
|
|
1151
1168
|
if (isNaN(n)) throw new FormulaException(`Invalid number: ${token.value}`);
|
|
1152
1169
|
return new NumberNode(n);
|
|
@@ -1175,15 +1192,22 @@ parsePrimary_fn = function() {
|
|
|
1175
1192
|
const sheetName = parts[0];
|
|
1176
1193
|
const cellRef = parts[1] ?? "";
|
|
1177
1194
|
if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
|
|
1195
|
+
const full = __privateMethod(this, _FormulaParser_instances, tryParseFullRange_fn).call(this, cellRef, sheetName);
|
|
1196
|
+
if (full) return full;
|
|
1178
1197
|
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1179
1198
|
const end = __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 3 /* CellReference */);
|
|
1180
1199
|
return new SheetRangeReferenceNode(sheetName, cellRef, end.value);
|
|
1181
1200
|
}
|
|
1182
1201
|
return new SheetCellReferenceNode(sheetName, cellRef);
|
|
1183
1202
|
}
|
|
1184
|
-
case 23 /* NamedRange */:
|
|
1203
|
+
case 23 /* NamedRange */: {
|
|
1185
1204
|
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1205
|
+
if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
|
|
1206
|
+
const full = __privateMethod(this, _FormulaParser_instances, tryParseFullRange_fn).call(this, token.value);
|
|
1207
|
+
if (full) return full;
|
|
1208
|
+
}
|
|
1186
1209
|
return new NamedRangeNode(token.value);
|
|
1210
|
+
}
|
|
1187
1211
|
case 24 /* SpillReference */:
|
|
1188
1212
|
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1189
1213
|
return new SpillReferenceNode(token.value);
|
|
@@ -1201,6 +1225,28 @@ parsePrimary_fn = function() {
|
|
|
1201
1225
|
);
|
|
1202
1226
|
}
|
|
1203
1227
|
};
|
|
1228
|
+
/**
|
|
1229
|
+
* Completes a whole-column or whole-row reference whose first half has already
|
|
1230
|
+
* been consumed and whose ':' is the current token. Returns null — consuming
|
|
1231
|
+
* nothing — when the two halves are not a matching pair of columns or rows, so
|
|
1232
|
+
* the caller can fall back to its normal interpretation.
|
|
1233
|
+
*
|
|
1234
|
+
* Both halves must be the same kind: `A:A` and `1:5` are ranges, `A:1` is not
|
|
1235
|
+
* a reference at all in Excel and must stay an error rather than becoming a
|
|
1236
|
+
* silently different region.
|
|
1237
|
+
*/
|
|
1238
|
+
tryParseFullRange_fn = function(startText, sheetName) {
|
|
1239
|
+
const endToken = __privateGet(this, _tokens)[__privateGet(this, _pos2) + 1];
|
|
1240
|
+
if (!endToken) return null;
|
|
1241
|
+
const start = classifyBound(startText);
|
|
1242
|
+
const end = classifyBound(endToken.value);
|
|
1243
|
+
if (!start || !end || start.kind !== end.kind) return null;
|
|
1244
|
+
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1245
|
+
__privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1246
|
+
const lo = Math.min(start.index, end.index);
|
|
1247
|
+
const hi = Math.max(start.index, end.index);
|
|
1248
|
+
return start.kind === "col" ? new FullRangeReferenceNode(lo, hi, null, null, sheetName) : new FullRangeReferenceNode(null, null, lo, hi, sheetName);
|
|
1249
|
+
};
|
|
1204
1250
|
parseFunction_fn = function() {
|
|
1205
1251
|
const nameToken = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
|
|
1206
1252
|
__privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 6 /* LeftParen */);
|
|
@@ -1227,6 +1273,18 @@ parseArgList_fn = function() {
|
|
|
1227
1273
|
__privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 7 /* RightParen */);
|
|
1228
1274
|
return args;
|
|
1229
1275
|
};
|
|
1276
|
+
function classifyBound(text) {
|
|
1277
|
+
const bare = text.replace(/\$/g, "");
|
|
1278
|
+
if (/^\d+$/.test(bare)) {
|
|
1279
|
+
const row2 = parseInt(bare, 10);
|
|
1280
|
+
return row2 >= 1 && row2 <= puredocsExcel.EXCEL_MAX_ROWS ? { kind: "row", index: row2 } : null;
|
|
1281
|
+
}
|
|
1282
|
+
if (/^[A-Za-z]{1,3}$/.test(bare)) {
|
|
1283
|
+
const col = puredocsExcel.columnNumber(bare.toUpperCase());
|
|
1284
|
+
return col >= 1 && col <= puredocsExcel.EXCEL_MAX_COLUMNS ? { kind: "col", index: col } : null;
|
|
1285
|
+
}
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1230
1288
|
function comparisonOp(type) {
|
|
1231
1289
|
switch (type) {
|
|
1232
1290
|
case 16 /* Equal */:
|
|
@@ -1246,6 +1304,137 @@ function comparisonOp(type) {
|
|
|
1246
1304
|
}
|
|
1247
1305
|
}
|
|
1248
1306
|
|
|
1307
|
+
// src/criteria-index.ts
|
|
1308
|
+
var EPSILON = 1e-10;
|
|
1309
|
+
var indexes = /* @__PURE__ */ new WeakMap();
|
|
1310
|
+
function build(arr) {
|
|
1311
|
+
const byText = /* @__PURE__ */ new Map();
|
|
1312
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
1313
|
+
const classes = /* @__PURE__ */ new Map();
|
|
1314
|
+
const addToClass = (kind, v, i2) => {
|
|
1315
|
+
const group = classes.get(kind);
|
|
1316
|
+
if (group) group.positions.push(i2);
|
|
1317
|
+
else classes.set(kind, { positions: [i2], representative: v });
|
|
1318
|
+
};
|
|
1319
|
+
let i = 0;
|
|
1320
|
+
for (const v of arr.values()) {
|
|
1321
|
+
const textKey = v.asText().toUpperCase();
|
|
1322
|
+
const atText = byText.get(textKey);
|
|
1323
|
+
if (atText) atText.push(i);
|
|
1324
|
+
else byText.set(textKey, [i]);
|
|
1325
|
+
if (v.isNumber) {
|
|
1326
|
+
const n = v.numberValue;
|
|
1327
|
+
const atNum = byNumber.get(n);
|
|
1328
|
+
if (atNum) atNum.push(i);
|
|
1329
|
+
else byNumber.set(n, [i]);
|
|
1330
|
+
} else {
|
|
1331
|
+
addToClass(v.kind, v, i);
|
|
1332
|
+
}
|
|
1333
|
+
i++;
|
|
1334
|
+
}
|
|
1335
|
+
const sortedNumbers = Float64Array.from(byNumber.keys()).sort();
|
|
1336
|
+
const cumulative = new Int32Array(sortedNumbers.length + 1);
|
|
1337
|
+
for (let k = 0; k < sortedNumbers.length; k++) {
|
|
1338
|
+
cumulative[k + 1] = cumulative[k] + byNumber.get(sortedNumbers[k]).length;
|
|
1339
|
+
}
|
|
1340
|
+
return { byText, byNumber, sortedNumbers, cumulative, classes: [...classes.values()] };
|
|
1341
|
+
}
|
|
1342
|
+
function indexOf(arr) {
|
|
1343
|
+
let idx = indexes.get(arr);
|
|
1344
|
+
if (!idx) {
|
|
1345
|
+
idx = build(arr);
|
|
1346
|
+
indexes.set(arr, idx);
|
|
1347
|
+
}
|
|
1348
|
+
return idx;
|
|
1349
|
+
}
|
|
1350
|
+
function lowerBound(sorted, target) {
|
|
1351
|
+
let lo = 0, hi = sorted.length;
|
|
1352
|
+
while (lo < hi) {
|
|
1353
|
+
const mid2 = lo + hi >> 1;
|
|
1354
|
+
if (sorted[mid2] < target) lo = mid2 + 1;
|
|
1355
|
+
else hi = mid2;
|
|
1356
|
+
}
|
|
1357
|
+
return lo;
|
|
1358
|
+
}
|
|
1359
|
+
function upperBound(sorted, target) {
|
|
1360
|
+
let lo = 0, hi = sorted.length;
|
|
1361
|
+
while (lo < hi) {
|
|
1362
|
+
const mid2 = lo + hi >> 1;
|
|
1363
|
+
if (sorted[mid2] <= target) lo = mid2 + 1;
|
|
1364
|
+
else hi = mid2;
|
|
1365
|
+
}
|
|
1366
|
+
return lo;
|
|
1367
|
+
}
|
|
1368
|
+
function matchingPositions(criteriaRange, criteria, minSize) {
|
|
1369
|
+
if (!criteriaRange.isArray) return null;
|
|
1370
|
+
const arr = criteriaRange.arrayVal;
|
|
1371
|
+
if (arr.length < minSize) return null;
|
|
1372
|
+
if (criteria.kind === "equalsText") {
|
|
1373
|
+
return indexOf(arr).byText.get(criteria.text) ?? EMPTY;
|
|
1374
|
+
}
|
|
1375
|
+
if (criteria.kind === "equalsNumber") {
|
|
1376
|
+
const target = criteria.number;
|
|
1377
|
+
const idx = indexOf(arr);
|
|
1378
|
+
const exact2 = idx.byNumber.get(target);
|
|
1379
|
+
let near;
|
|
1380
|
+
const { sortedNumbers } = idx;
|
|
1381
|
+
for (let k = lowerBound(sortedNumbers, target - EPSILON); k < sortedNumbers.length; k++) {
|
|
1382
|
+
const value2 = sortedNumbers[k];
|
|
1383
|
+
if (value2 > target + EPSILON) break;
|
|
1384
|
+
if (value2 === target) continue;
|
|
1385
|
+
if (!criteria.test(FormulaValue.number(value2))) continue;
|
|
1386
|
+
(near ?? (near = [])).push(...idx.byNumber.get(value2));
|
|
1387
|
+
}
|
|
1388
|
+
const blanks = criteria.test(FormulaValue.blank) ? blankPositions(idx) : void 0;
|
|
1389
|
+
if (!near && !blanks?.length) return exact2 ?? EMPTY;
|
|
1390
|
+
const merged = [...exact2 ?? EMPTY, ...near ?? EMPTY, ...blanks ?? EMPTY];
|
|
1391
|
+
merged.sort((a, b) => a - b);
|
|
1392
|
+
return merged;
|
|
1393
|
+
}
|
|
1394
|
+
if (criteria.kind === "compareNumber") {
|
|
1395
|
+
return comparePositions(indexOf(arr), criteria, arr.length);
|
|
1396
|
+
}
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
function comparePositions(idx, criteria, length) {
|
|
1400
|
+
const target = criteria.number;
|
|
1401
|
+
const { sortedNumbers } = idx;
|
|
1402
|
+
let from = 0;
|
|
1403
|
+
let to = sortedNumbers.length;
|
|
1404
|
+
switch (criteria.op) {
|
|
1405
|
+
case ">":
|
|
1406
|
+
from = upperBound(sortedNumbers, target);
|
|
1407
|
+
break;
|
|
1408
|
+
case ">=":
|
|
1409
|
+
from = lowerBound(sortedNumbers, target);
|
|
1410
|
+
break;
|
|
1411
|
+
case "<":
|
|
1412
|
+
to = lowerBound(sortedNumbers, target);
|
|
1413
|
+
break;
|
|
1414
|
+
default:
|
|
1415
|
+
to = upperBound(sortedNumbers, target);
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
const budget = length >> 1;
|
|
1419
|
+
const matchedClasses = idx.classes.filter((g) => criteria.test(g.representative));
|
|
1420
|
+
let total = idx.cumulative[to] - idx.cumulative[from];
|
|
1421
|
+
for (const group of matchedClasses) total += group.positions.length;
|
|
1422
|
+
if (total > budget) return null;
|
|
1423
|
+
const positions = [];
|
|
1424
|
+
for (let k = from; k < to; k++) {
|
|
1425
|
+
for (const i of idx.byNumber.get(sortedNumbers[k])) positions.push(i);
|
|
1426
|
+
}
|
|
1427
|
+
for (const group of matchedClasses) {
|
|
1428
|
+
for (const i of group.positions) positions.push(i);
|
|
1429
|
+
}
|
|
1430
|
+
positions.sort((a, b) => a - b);
|
|
1431
|
+
return positions;
|
|
1432
|
+
}
|
|
1433
|
+
function blankPositions(idx) {
|
|
1434
|
+
return idx.classes.find((g) => g.representative.isBlank)?.positions;
|
|
1435
|
+
}
|
|
1436
|
+
var EMPTY = [];
|
|
1437
|
+
|
|
1249
1438
|
// src/functions/math-functions.ts
|
|
1250
1439
|
function registerMath(r) {
|
|
1251
1440
|
r.register("SUM", sum, 1);
|
|
@@ -1307,34 +1496,72 @@ function valueAt(v, i) {
|
|
|
1307
1496
|
function lengthOf(v) {
|
|
1308
1497
|
return v.isArray ? v.arrayVal.length : 1;
|
|
1309
1498
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1499
|
+
var INDEX_MIN_RANGE = 32;
|
|
1500
|
+
function resolveIfsSelection(a, c, startIdx, len2) {
|
|
1501
|
+
const ranges = [];
|
|
1502
|
+
const criteria = [];
|
|
1503
|
+
let seed;
|
|
1504
|
+
let seedPair = -1;
|
|
1312
1505
|
for (let p = startIdx; p + 1 < a.length; p += 2) {
|
|
1313
1506
|
const cr = a[p].evaluate(c);
|
|
1314
1507
|
if (cr.isError) return { ok: false, error: cr };
|
|
1315
1508
|
const crit = a[p + 1].evaluate(c);
|
|
1316
1509
|
if (crit.isError) return { ok: false, error: crit };
|
|
1317
|
-
const
|
|
1510
|
+
const compiled = exports.FormulaHelper.compileCriteria(crit.asText());
|
|
1511
|
+
ranges.push(cr);
|
|
1512
|
+
criteria.push(compiled);
|
|
1513
|
+
if (seed === void 0) {
|
|
1514
|
+
const positions = matchingPositions(cr, compiled, INDEX_MIN_RANGE);
|
|
1515
|
+
if (positions) {
|
|
1516
|
+
seed = positions;
|
|
1517
|
+
seedPair = ranges.length - 1;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (seed !== void 0) {
|
|
1522
|
+
let candidates = [];
|
|
1523
|
+
for (const i of seed) if (i < len2) candidates.push(i);
|
|
1524
|
+
for (let k = 0; k < ranges.length && candidates.length > 0; k++) {
|
|
1525
|
+
if (k === seedPair) continue;
|
|
1526
|
+
const range = ranges[k];
|
|
1527
|
+
const test = criteria[k].test;
|
|
1528
|
+
const next = [];
|
|
1529
|
+
for (const i of candidates) if (test(valueAt(range, i))) next.push(i);
|
|
1530
|
+
candidates = next;
|
|
1531
|
+
}
|
|
1532
|
+
return { ok: true, positions: candidates };
|
|
1533
|
+
}
|
|
1534
|
+
const mask = new Uint8Array(len2).fill(1);
|
|
1535
|
+
for (let k = 0; k < ranges.length; k++) {
|
|
1536
|
+
const range = ranges[k];
|
|
1537
|
+
const test = criteria[k].test;
|
|
1318
1538
|
for (let i = 0; i < len2; i++) {
|
|
1319
1539
|
if (!mask[i]) continue;
|
|
1320
|
-
if (!
|
|
1540
|
+
if (!test(valueAt(range, i))) mask[i] = 0;
|
|
1321
1541
|
}
|
|
1322
1542
|
}
|
|
1323
1543
|
return { ok: true, mask };
|
|
1324
1544
|
}
|
|
1545
|
+
function forEachSelected(sel, len2, visit) {
|
|
1546
|
+
if ("positions" in sel) {
|
|
1547
|
+
for (const i of sel.positions) visit(i);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const { mask } = sel;
|
|
1551
|
+
for (let i = 0; i < len2; i++) if (mask[i]) visit(i);
|
|
1552
|
+
}
|
|
1325
1553
|
function sumIfs(a, c) {
|
|
1326
1554
|
if (a.length < 3 || a.length % 2 === 0) return FormulaValue.errorValue;
|
|
1327
1555
|
const sumRange = a[0].evaluate(c);
|
|
1328
1556
|
if (sumRange.isError) return sumRange;
|
|
1329
1557
|
const len2 = lengthOf(sumRange);
|
|
1330
|
-
const
|
|
1331
|
-
if (!
|
|
1558
|
+
const sel = resolveIfsSelection(a, c, 1, len2);
|
|
1559
|
+
if (!sel.ok) return sel.error;
|
|
1332
1560
|
let total = 0;
|
|
1333
|
-
|
|
1334
|
-
if (!m.mask[i]) continue;
|
|
1561
|
+
forEachSelected(sel, len2, (i) => {
|
|
1335
1562
|
const r = valueAt(sumRange, i).tryAsDouble();
|
|
1336
1563
|
if (r.ok) total += r.value;
|
|
1337
|
-
}
|
|
1564
|
+
});
|
|
1338
1565
|
return FormulaValue.number(total);
|
|
1339
1566
|
}
|
|
1340
1567
|
function countIfs(a, c) {
|
|
@@ -1342,10 +1569,12 @@ function countIfs(a, c) {
|
|
|
1342
1569
|
const first = a[0].evaluate(c);
|
|
1343
1570
|
if (first.isError) return first;
|
|
1344
1571
|
const len2 = lengthOf(first);
|
|
1345
|
-
const
|
|
1346
|
-
if (!
|
|
1572
|
+
const sel = resolveIfsSelection(a, c, 0, len2);
|
|
1573
|
+
if (!sel.ok) return sel.error;
|
|
1347
1574
|
let n = 0;
|
|
1348
|
-
|
|
1575
|
+
forEachSelected(sel, len2, () => {
|
|
1576
|
+
n++;
|
|
1577
|
+
});
|
|
1349
1578
|
return FormulaValue.number(n);
|
|
1350
1579
|
}
|
|
1351
1580
|
function averageIfs(a, c) {
|
|
@@ -1353,17 +1582,16 @@ function averageIfs(a, c) {
|
|
|
1353
1582
|
const avgRange = a[0].evaluate(c);
|
|
1354
1583
|
if (avgRange.isError) return avgRange;
|
|
1355
1584
|
const len2 = lengthOf(avgRange);
|
|
1356
|
-
const
|
|
1357
|
-
if (!
|
|
1585
|
+
const sel = resolveIfsSelection(a, c, 1, len2);
|
|
1586
|
+
if (!sel.ok) return sel.error;
|
|
1358
1587
|
let total = 0, cnt = 0;
|
|
1359
|
-
|
|
1360
|
-
if (!m.mask[i]) continue;
|
|
1588
|
+
forEachSelected(sel, len2, (i) => {
|
|
1361
1589
|
const r = valueAt(avgRange, i).tryAsDouble();
|
|
1362
1590
|
if (r.ok) {
|
|
1363
1591
|
total += r.value;
|
|
1364
1592
|
cnt++;
|
|
1365
1593
|
}
|
|
1366
|
-
}
|
|
1594
|
+
});
|
|
1367
1595
|
return cnt === 0 ? FormulaValue.errorDiv0 : FormulaValue.number(total / cnt);
|
|
1368
1596
|
}
|
|
1369
1597
|
function sumSq(a, c) {
|
|
@@ -4187,39 +4415,43 @@ exports.FormulaHelper = void 0;
|
|
|
4187
4415
|
return result;
|
|
4188
4416
|
}
|
|
4189
4417
|
FormulaHelper2.flattenArgs = flattenArgs;
|
|
4190
|
-
function
|
|
4191
|
-
|
|
4192
|
-
const opMatch = /^(>=|<=|<>|>|<|=)(.*)$/.exec(
|
|
4418
|
+
function compileCriteria(criteria) {
|
|
4419
|
+
const trimmed = criteria.trim();
|
|
4420
|
+
const opMatch = /^(>=|<=|<>|>|<|=)(.*)$/.exec(trimmed);
|
|
4193
4421
|
if (opMatch) {
|
|
4194
4422
|
const op = opMatch[1];
|
|
4195
4423
|
const rhs = opMatch[2];
|
|
4196
4424
|
const rhsVal = parseFloat(rhs);
|
|
4197
4425
|
const rhsNum = isNaN(rhsVal) ? null : rhsVal;
|
|
4198
|
-
const
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
return cmp > 0;
|
|
4202
|
-
case ">=":
|
|
4203
|
-
return cmp >= 0;
|
|
4204
|
-
case "<":
|
|
4205
|
-
return cmp < 0;
|
|
4206
|
-
case "<=":
|
|
4207
|
-
return cmp <= 0;
|
|
4208
|
-
case "<>":
|
|
4209
|
-
return !FormulaValue.areEqual(value2, rhsNum !== null ? FormulaValue.number(rhsNum) : FormulaValue.text(rhs));
|
|
4210
|
-
case "=":
|
|
4211
|
-
return FormulaValue.areEqual(value2, rhsNum !== null ? FormulaValue.number(rhsNum) : FormulaValue.text(rhs));
|
|
4426
|
+
const rhsValue = rhsNum !== null ? FormulaValue.number(rhsNum) : FormulaValue.text(rhs);
|
|
4427
|
+
if (op === "=") {
|
|
4428
|
+
return rhsNum !== null ? numberEquality(rhsNum) : textEquality(rhs);
|
|
4212
4429
|
}
|
|
4430
|
+
if (op === "<>") {
|
|
4431
|
+
return { kind: "other", test: (v) => !FormulaValue.areEqual(v, rhsValue) };
|
|
4432
|
+
}
|
|
4433
|
+
const wanted = op === ">" ? (c) => c > 0 : op === ">=" ? (c) => c >= 0 : op === "<" ? (c) => c < 0 : (c) => c <= 0;
|
|
4434
|
+
const test = (v) => wanted(FormulaValue.compare(v, rhsValue));
|
|
4435
|
+
return rhsNum !== null ? { kind: "compareNumber", number: rhsNum, op, test } : { kind: "other", test };
|
|
4213
4436
|
}
|
|
4214
|
-
if (
|
|
4215
|
-
const pattern = wildcardToRegex(
|
|
4216
|
-
return pattern.test(
|
|
4217
|
-
}
|
|
4218
|
-
const num5 = parseFloat(criteria);
|
|
4219
|
-
if (!isNaN(num5)) {
|
|
4220
|
-
return FormulaValue.areEqual(value2, FormulaValue.number(num5));
|
|
4437
|
+
if (trimmed.includes("*") || trimmed.includes("?")) {
|
|
4438
|
+
const pattern = wildcardToRegex(trimmed);
|
|
4439
|
+
return { kind: "other", test: (v) => pattern.test(v.asText()) };
|
|
4221
4440
|
}
|
|
4222
|
-
|
|
4441
|
+
const num5 = parseFloat(trimmed);
|
|
4442
|
+
return isNaN(num5) ? textEquality(trimmed) : numberEquality(num5);
|
|
4443
|
+
}
|
|
4444
|
+
FormulaHelper2.compileCriteria = compileCriteria;
|
|
4445
|
+
function numberEquality(n) {
|
|
4446
|
+
const target = FormulaValue.number(n);
|
|
4447
|
+
return { kind: "equalsNumber", number: n, test: (v) => FormulaValue.areEqual(v, target) };
|
|
4448
|
+
}
|
|
4449
|
+
function textEquality(s) {
|
|
4450
|
+
const upper2 = s.toUpperCase();
|
|
4451
|
+
return { kind: "equalsText", text: upper2, test: (v) => v.asText().toUpperCase() === upper2 };
|
|
4452
|
+
}
|
|
4453
|
+
function matchesCriteria(value2, criteria) {
|
|
4454
|
+
return compileCriteria(criteria).test(value2);
|
|
4223
4455
|
}
|
|
4224
4456
|
FormulaHelper2.matchesCriteria = matchesCriteria;
|
|
4225
4457
|
function wildcardToRegex(pattern) {
|
|
@@ -4229,7 +4461,7 @@ exports.FormulaHelper = void 0;
|
|
|
4229
4461
|
})(exports.FormulaHelper || (exports.FormulaHelper = {}));
|
|
4230
4462
|
|
|
4231
4463
|
// src/formula-context.ts
|
|
4232
|
-
var _sheet, _functions2, _evaluating, _scopes, _FormulaContext_instances, lookupScope_fn;
|
|
4464
|
+
var _sheet, _functions2, _evaluating, _scopes, _FormulaContext_instances, lookupScope_fn, forSheet_fn;
|
|
4233
4465
|
var _FormulaContext = class _FormulaContext {
|
|
4234
4466
|
constructor(sheet, formulaRow = 0, formulaCol = 0, evaluating, registry) {
|
|
4235
4467
|
__privateAdd(this, _FormulaContext_instances);
|
|
@@ -4274,24 +4506,70 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4274
4506
|
getRangeValues(startRef, endRef) {
|
|
4275
4507
|
const s = puredocsExcel.parseCellRef(startRef);
|
|
4276
4508
|
const e = puredocsExcel.parseCellRef(endRef);
|
|
4277
|
-
|
|
4278
|
-
|
|
4509
|
+
return this.getRangeValuesByIndex(s.row, s.column, e.row, e.column);
|
|
4510
|
+
}
|
|
4511
|
+
/**
|
|
4512
|
+
* Resolves a rectangle already known in coordinates. Split out from
|
|
4513
|
+
* {@link getRangeValues} because whole-column references and the range memo
|
|
4514
|
+
* both work in numbers, and formatting them back into `A1` strings only to
|
|
4515
|
+
* re-parse them is measurable at range scale.
|
|
4516
|
+
*/
|
|
4517
|
+
getRangeValuesByIndex(startRow, startCol, endRow, endCol) {
|
|
4518
|
+
const rows2 = endRow - startRow + 1;
|
|
4519
|
+
const cols = endCol - startCol + 1;
|
|
4520
|
+
const cache = rows2 * cols > 1 ? this.rangeCache : void 0;
|
|
4521
|
+
const sheetKey = cache ? __privateGet(this, _sheet).name.toUpperCase() : "";
|
|
4522
|
+
if (cache) {
|
|
4523
|
+
const hit = cache.get(sheetKey, startRow, startCol, endRow, endCol);
|
|
4524
|
+
if (hit !== void 0) return hit;
|
|
4525
|
+
}
|
|
4279
4526
|
const arr = new ArrayValue(rows2, cols);
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4527
|
+
const at = __privateGet(this, _sheet).getCellValueAt;
|
|
4528
|
+
if (at) {
|
|
4529
|
+
for (let r = 0; r < rows2; r++) {
|
|
4530
|
+
for (let c = 0; c < cols; c++) {
|
|
4531
|
+
arr.set(r, c, FormulaValue.fromObjectSync(at.call(__privateGet(this, _sheet), startRow + r, startCol + c)));
|
|
4532
|
+
}
|
|
4284
4533
|
}
|
|
4534
|
+
} else {
|
|
4535
|
+
for (let r = 0; r < rows2; r++) {
|
|
4536
|
+
for (let c = 0; c < cols; c++) {
|
|
4537
|
+
const ref = puredocsExcel.cellRefFromRowCol(startRow + r, startCol + c);
|
|
4538
|
+
arr.set(r, c, this.getCellValue(ref));
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
const value2 = FormulaValue.array(arr);
|
|
4543
|
+
if (cache && rows2 > 0 && cols > 0) {
|
|
4544
|
+
cache.set(sheetKey, startRow, startCol, endRow, endCol, value2, rows2 * cols);
|
|
4285
4545
|
}
|
|
4286
|
-
return
|
|
4546
|
+
return value2;
|
|
4547
|
+
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Resolves a whole-column (`A:A`) or whole-row (`1:1`) reference, bounded by
|
|
4550
|
+
* the sheet's used range the way Excel does. Without a bound this would
|
|
4551
|
+
* materialise a million-cell array for a sheet with twelve rows in it.
|
|
4552
|
+
*
|
|
4553
|
+
* A sheet that cannot report its extent yields `#REF!` rather than a guess:
|
|
4554
|
+
* silently reading nothing would make SUM(A:A) return 0 on a full column.
|
|
4555
|
+
*/
|
|
4556
|
+
getFullRangeValues(startCol, endCol, startRow, endRow) {
|
|
4557
|
+
const bounds = __privateGet(this, _sheet).usedBounds;
|
|
4558
|
+
if (!bounds) return FormulaValue.errorRef;
|
|
4559
|
+
if (bounds.rowCount === 0 || bounds.columnCount === 0) return FormulaValue.blank;
|
|
4560
|
+
const r0 = startRow ?? 1;
|
|
4561
|
+
const r1 = endRow ?? bounds.rowCount;
|
|
4562
|
+
const c0 = startCol ?? 1;
|
|
4563
|
+
const c1 = endCol ?? bounds.columnCount;
|
|
4564
|
+
const lastRow = Math.min(r1, bounds.rowCount);
|
|
4565
|
+
const lastCol = Math.min(c1, bounds.columnCount);
|
|
4566
|
+
if (lastRow < r0 || lastCol < c0) return FormulaValue.blank;
|
|
4567
|
+
return this.getRangeValuesByIndex(r0, c0, lastRow, lastCol);
|
|
4287
4568
|
}
|
|
4288
4569
|
getSheetCellValue(sheetName, cellReference) {
|
|
4289
4570
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4290
4571
|
try {
|
|
4291
|
-
|
|
4292
|
-
const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, __privateGet(this, _evaluating), __privateGet(this, _functions2));
|
|
4293
|
-
ctx.workbook = this.workbook;
|
|
4294
|
-
return ctx.getCellValue(cellReference);
|
|
4572
|
+
return __privateMethod(this, _FormulaContext_instances, forSheet_fn).call(this, sheetName).getCellValue(cellReference);
|
|
4295
4573
|
} catch {
|
|
4296
4574
|
return FormulaValue.errorRef;
|
|
4297
4575
|
}
|
|
@@ -4299,10 +4577,16 @@ var _FormulaContext = class _FormulaContext {
|
|
|
4299
4577
|
getSheetRangeValues(sheetName, startRef, endRef) {
|
|
4300
4578
|
if (!this.workbook) return FormulaValue.errorRef;
|
|
4301
4579
|
try {
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4580
|
+
return __privateMethod(this, _FormulaContext_instances, forSheet_fn).call(this, sheetName).getRangeValues(startRef, endRef);
|
|
4581
|
+
} catch {
|
|
4582
|
+
return FormulaValue.errorRef;
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
/** Whole-column/whole-row reference qualified by a sheet name. */
|
|
4586
|
+
getSheetFullRangeValues(sheetName, startCol, endCol, startRow, endRow) {
|
|
4587
|
+
if (!this.workbook) return FormulaValue.errorRef;
|
|
4588
|
+
try {
|
|
4589
|
+
return __privateMethod(this, _FormulaContext_instances, forSheet_fn).call(this, sheetName).getFullRangeValues(startCol, endCol, startRow, endRow);
|
|
4306
4590
|
} catch {
|
|
4307
4591
|
return FormulaValue.errorRef;
|
|
4308
4592
|
}
|
|
@@ -4357,6 +4641,21 @@ lookupScope_fn = function(name) {
|
|
|
4357
4641
|
}
|
|
4358
4642
|
return void 0;
|
|
4359
4643
|
};
|
|
4644
|
+
/**
|
|
4645
|
+
* A sibling context bound to another sheet, sharing this one's circular-
|
|
4646
|
+
* reference guard and range memo. These contexts only read values — they
|
|
4647
|
+
* never evaluate an AST — so the name resolver and recursion budget are
|
|
4648
|
+
* deliberately not carried over; passing them would suggest a scoping
|
|
4649
|
+
* relationship that does not exist. The memo, on the other hand, must be
|
|
4650
|
+
* carried: cross-sheet ranges are the most expensive reads in a workbook.
|
|
4651
|
+
*/
|
|
4652
|
+
forSheet_fn = function(sheetName) {
|
|
4653
|
+
const sheet = this.workbook.getWorksheet(sheetName);
|
|
4654
|
+
const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, __privateGet(this, _evaluating), __privateGet(this, _functions2));
|
|
4655
|
+
ctx.workbook = this.workbook;
|
|
4656
|
+
ctx.rangeCache = this.rangeCache;
|
|
4657
|
+
return ctx;
|
|
4658
|
+
};
|
|
4360
4659
|
var FormulaContext = _FormulaContext;
|
|
4361
4660
|
|
|
4362
4661
|
// src/lru-cache.ts
|
|
@@ -4457,12 +4756,15 @@ function parseFormula(formula) {
|
|
|
4457
4756
|
const stripped = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
4458
4757
|
const cached = astCache.get(stripped);
|
|
4459
4758
|
if (cached !== void 0) return cached;
|
|
4460
|
-
const
|
|
4461
|
-
const tokens = lexer.tokenize();
|
|
4462
|
-
const ast = new FormulaParser(tokens).parse();
|
|
4759
|
+
const ast = parseFormulaUncached(stripped);
|
|
4463
4760
|
astCache.set(stripped, ast);
|
|
4464
4761
|
return ast;
|
|
4465
4762
|
}
|
|
4763
|
+
function parseFormulaUncached(formula) {
|
|
4764
|
+
const stripped = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
4765
|
+
const tokens = new FormulaLexer(stripped).tokenize();
|
|
4766
|
+
return new FormulaParser(tokens).parse();
|
|
4767
|
+
}
|
|
4466
4768
|
var DEFAULT_LAMBDA_DEPTH = 1024;
|
|
4467
4769
|
function buildContext(sheet, opts) {
|
|
4468
4770
|
const evaluating = opts.evaluating ?? /* @__PURE__ */ new Set();
|
|
@@ -4473,6 +4775,7 @@ function buildContext(sheet, opts) {
|
|
|
4473
4775
|
evaluating
|
|
4474
4776
|
);
|
|
4475
4777
|
ctx.recursionGuard = opts.recursionGuard ?? { depth: 0, max: DEFAULT_LAMBDA_DEPTH };
|
|
4778
|
+
ctx.rangeCache = opts.rangeCache;
|
|
4476
4779
|
const wb = opts.workbook;
|
|
4477
4780
|
if (wb) {
|
|
4478
4781
|
ctx.workbook = wb;
|
|
@@ -4486,7 +4789,7 @@ function buildContext(sheet, opts) {
|
|
|
4486
4789
|
return ctx;
|
|
4487
4790
|
}
|
|
4488
4791
|
function resolveName(name, sheet, workbook, evaluating, resolving, resolved, recursionGuard) {
|
|
4489
|
-
const key = `${sheet.name}
|
|
4792
|
+
const key = `${sheet.name} ${name.toUpperCase()}`;
|
|
4490
4793
|
const cached = resolved.get(key);
|
|
4491
4794
|
if (cached !== void 0) return cached;
|
|
4492
4795
|
if (resolving.has(key)) return FormulaValue.errorRef;
|
|
@@ -4514,11 +4817,18 @@ function resolveName(name, sheet, workbook, evaluating, resolving, resolved, rec
|
|
|
4514
4817
|
function evaluateFormula(formula, sheet, options) {
|
|
4515
4818
|
if (!formula) return FormulaValue.blank;
|
|
4516
4819
|
try {
|
|
4517
|
-
|
|
4820
|
+
return evaluateAst(parseFormula(formula), sheet, options);
|
|
4821
|
+
} catch {
|
|
4822
|
+
return FormulaValue.errorValue;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
function evaluateAst(ast, sheet, options) {
|
|
4826
|
+
try {
|
|
4518
4827
|
const ctx = buildContext(sheet, {
|
|
4519
4828
|
workbook: options?.workbook,
|
|
4520
4829
|
formulaRow: options?.formulaRow,
|
|
4521
|
-
formulaCol: options?.formulaCol
|
|
4830
|
+
formulaCol: options?.formulaCol,
|
|
4831
|
+
rangeCache: options?.rangeCache
|
|
4522
4832
|
});
|
|
4523
4833
|
return ast.evaluate(ctx);
|
|
4524
4834
|
} catch {
|
|
@@ -4539,6 +4849,149 @@ function getAstCacheStats() {
|
|
|
4539
4849
|
function setAstCacheCapacity(capacity) {
|
|
4540
4850
|
astCache.resize(capacity);
|
|
4541
4851
|
}
|
|
4852
|
+
|
|
4853
|
+
// src/range-cache.ts
|
|
4854
|
+
var TILE_ROWS = 64;
|
|
4855
|
+
var TILE_COLS = 64;
|
|
4856
|
+
var MAX_TILES_PER_RANGE = 32;
|
|
4857
|
+
var DEFAULT_MAX_CELLS = 2e6;
|
|
4858
|
+
var _entries, _tiles, _wide, _placements, _sizes, _cells, _maxCells, _RangeCache_static, contains_fn, _RangeCache_instances, drop_fn, fileIn_fn;
|
|
4859
|
+
var _RangeCache = class _RangeCache {
|
|
4860
|
+
constructor(maxCells = DEFAULT_MAX_CELLS) {
|
|
4861
|
+
__privateAdd(this, _RangeCache_instances);
|
|
4862
|
+
/** `SHEET r0 c0 r1 c1` → the memoised array value. Insertion-ordered (FIFO eviction). */
|
|
4863
|
+
__privateAdd(this, _entries, /* @__PURE__ */ new Map());
|
|
4864
|
+
/** `sheet tr tc` → entry keys whose rectangle overlaps that tile. */
|
|
4865
|
+
__privateAdd(this, _tiles, /* @__PURE__ */ new Map());
|
|
4866
|
+
/** sheet → entry keys whose rectangle is too broad to tile. */
|
|
4867
|
+
__privateAdd(this, _wide, /* @__PURE__ */ new Map());
|
|
4868
|
+
/** entry key → buckets it was filed under, for cheap removal. */
|
|
4869
|
+
__privateAdd(this, _placements, /* @__PURE__ */ new Map());
|
|
4870
|
+
/** entry key → its area, so eviction can give the budget back. */
|
|
4871
|
+
__privateAdd(this, _sizes, /* @__PURE__ */ new Map());
|
|
4872
|
+
/** Number of cells across all cached rectangles, kept under {@link #maxCells}. */
|
|
4873
|
+
__privateAdd(this, _cells, 0);
|
|
4874
|
+
__privateAdd(this, _maxCells);
|
|
4875
|
+
this.hits = 0;
|
|
4876
|
+
this.misses = 0;
|
|
4877
|
+
__privateSet(this, _maxCells, maxCells);
|
|
4878
|
+
}
|
|
4879
|
+
get size() {
|
|
4880
|
+
return __privateGet(this, _entries).size;
|
|
4881
|
+
}
|
|
4882
|
+
get cachedCells() {
|
|
4883
|
+
return __privateGet(this, _cells);
|
|
4884
|
+
}
|
|
4885
|
+
static key(sheet, r0, c0, r1, c1) {
|
|
4886
|
+
return `${sheet} ${r0} ${c0} ${r1} ${c1}`;
|
|
4887
|
+
}
|
|
4888
|
+
get(sheet, r0, c0, r1, c1) {
|
|
4889
|
+
const hit = __privateGet(this, _entries).get(_RangeCache.key(sheet, r0, c0, r1, c1));
|
|
4890
|
+
if (hit === void 0) this.misses++;
|
|
4891
|
+
else this.hits++;
|
|
4892
|
+
return hit;
|
|
4893
|
+
}
|
|
4894
|
+
/** Memoises a rectangle's value. `cells` is its area, for the budget. */
|
|
4895
|
+
set(sheet, r0, c0, r1, c1, value2, cells) {
|
|
4896
|
+
if (cells <= 0 || cells > __privateGet(this, _maxCells)) return;
|
|
4897
|
+
const key = _RangeCache.key(sheet, r0, c0, r1, c1);
|
|
4898
|
+
if (__privateGet(this, _entries).has(key)) __privateMethod(this, _RangeCache_instances, drop_fn).call(this, key);
|
|
4899
|
+
__privateGet(this, _entries).set(key, value2);
|
|
4900
|
+
__privateSet(this, _cells, __privateGet(this, _cells) + cells);
|
|
4901
|
+
const buckets = [];
|
|
4902
|
+
const tileRow0 = r0 / TILE_ROWS | 0, tileRow1 = r1 / TILE_ROWS | 0;
|
|
4903
|
+
const tileCol0 = c0 / TILE_COLS | 0, tileCol1 = c1 / TILE_COLS | 0;
|
|
4904
|
+
const tileCount = (tileRow1 - tileRow0 + 1) * (tileCol1 - tileCol0 + 1);
|
|
4905
|
+
if (tileCount > MAX_TILES_PER_RANGE) {
|
|
4906
|
+
__privateMethod(this, _RangeCache_instances, fileIn_fn).call(this, __privateGet(this, _wide), sheet, key, buckets, `w ${sheet}`);
|
|
4907
|
+
} else {
|
|
4908
|
+
for (let tr = tileRow0; tr <= tileRow1; tr++) {
|
|
4909
|
+
for (let tc = tileCol0; tc <= tileCol1; tc++) {
|
|
4910
|
+
const bucket = `${sheet} ${tr} ${tc}`;
|
|
4911
|
+
__privateMethod(this, _RangeCache_instances, fileIn_fn).call(this, __privateGet(this, _tiles), bucket, key, buckets, `t ${bucket}`);
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
__privateGet(this, _placements).set(key, buckets);
|
|
4916
|
+
__privateGet(this, _sizes).set(key, cells);
|
|
4917
|
+
if (__privateGet(this, _cells) > __privateGet(this, _maxCells)) {
|
|
4918
|
+
for (const oldest of __privateGet(this, _entries).keys()) {
|
|
4919
|
+
if (__privateGet(this, _cells) <= __privateGet(this, _maxCells)) break;
|
|
4920
|
+
if (oldest === key) continue;
|
|
4921
|
+
__privateMethod(this, _RangeCache_instances, drop_fn).call(this, oldest);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
/** Drops every cached rectangle containing the given cell. */
|
|
4926
|
+
invalidateCell(sheet, row2, col) {
|
|
4927
|
+
var _a, _b;
|
|
4928
|
+
const tile = __privateGet(this, _tiles).get(`${sheet} ${row2 / TILE_ROWS | 0} ${col / TILE_COLS | 0}`);
|
|
4929
|
+
if (tile) {
|
|
4930
|
+
for (const key of [...tile]) {
|
|
4931
|
+
if (__privateMethod(_a = _RangeCache, _RangeCache_static, contains_fn).call(_a, key, sheet, row2, col)) __privateMethod(this, _RangeCache_instances, drop_fn).call(this, key);
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
const wide = __privateGet(this, _wide).get(sheet);
|
|
4935
|
+
if (wide) {
|
|
4936
|
+
for (const key of [...wide]) {
|
|
4937
|
+
if (__privateMethod(_b = _RangeCache, _RangeCache_static, contains_fn).call(_b, key, sheet, row2, col)) __privateMethod(this, _RangeCache_instances, drop_fn).call(this, key);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
clear() {
|
|
4942
|
+
__privateGet(this, _entries).clear();
|
|
4943
|
+
__privateGet(this, _tiles).clear();
|
|
4944
|
+
__privateGet(this, _wide).clear();
|
|
4945
|
+
__privateGet(this, _placements).clear();
|
|
4946
|
+
__privateGet(this, _sizes).clear();
|
|
4947
|
+
__privateSet(this, _cells, 0);
|
|
4948
|
+
}
|
|
4949
|
+
};
|
|
4950
|
+
_entries = new WeakMap();
|
|
4951
|
+
_tiles = new WeakMap();
|
|
4952
|
+
_wide = new WeakMap();
|
|
4953
|
+
_placements = new WeakMap();
|
|
4954
|
+
_sizes = new WeakMap();
|
|
4955
|
+
_cells = new WeakMap();
|
|
4956
|
+
_maxCells = new WeakMap();
|
|
4957
|
+
_RangeCache_static = new WeakSet();
|
|
4958
|
+
contains_fn = function(key, sheet, row2, col) {
|
|
4959
|
+
const parts = key.split(" ");
|
|
4960
|
+
const n = parts.length;
|
|
4961
|
+
const c1 = +parts[n - 1], r1 = +parts[n - 2], c0 = +parts[n - 3], r0 = +parts[n - 4];
|
|
4962
|
+
if (parts.slice(0, n - 4).join(" ") !== sheet) return false;
|
|
4963
|
+
return row2 >= r0 && row2 <= r1 && col >= c0 && col <= c1;
|
|
4964
|
+
};
|
|
4965
|
+
_RangeCache_instances = new WeakSet();
|
|
4966
|
+
drop_fn = function(key) {
|
|
4967
|
+
if (!__privateGet(this, _entries).delete(key)) return;
|
|
4968
|
+
__privateSet(this, _cells, __privateGet(this, _cells) - (__privateGet(this, _sizes).get(key) ?? 0));
|
|
4969
|
+
__privateGet(this, _sizes).delete(key);
|
|
4970
|
+
const buckets = __privateGet(this, _placements).get(key);
|
|
4971
|
+
if (buckets) {
|
|
4972
|
+
for (const bucket of buckets) {
|
|
4973
|
+
const store = bucket.charCodeAt(0) === 119 ? __privateGet(this, _wide) : __privateGet(this, _tiles);
|
|
4974
|
+
const bucketKey = bucket.slice(2);
|
|
4975
|
+
const set = store.get(bucketKey);
|
|
4976
|
+
if (!set) continue;
|
|
4977
|
+
set.delete(key);
|
|
4978
|
+
if (set.size === 0) store.delete(bucketKey);
|
|
4979
|
+
}
|
|
4980
|
+
__privateGet(this, _placements).delete(key);
|
|
4981
|
+
}
|
|
4982
|
+
};
|
|
4983
|
+
fileIn_fn = function(store, bucketKey, entryKey, buckets, bucketId) {
|
|
4984
|
+
let set = store.get(bucketKey);
|
|
4985
|
+
if (!set) {
|
|
4986
|
+
set = /* @__PURE__ */ new Set();
|
|
4987
|
+
store.set(bucketKey, set);
|
|
4988
|
+
}
|
|
4989
|
+
if (set.has(entryKey)) return;
|
|
4990
|
+
set.add(entryKey);
|
|
4991
|
+
buckets.push(bucketId);
|
|
4992
|
+
};
|
|
4993
|
+
__privateAdd(_RangeCache, _RangeCache_static);
|
|
4994
|
+
var RangeCache = _RangeCache;
|
|
4542
4995
|
function extractReferences(ast, formulaSheet, registry = FunctionRegistry.default, resolveName2) {
|
|
4543
4996
|
const refs = [];
|
|
4544
4997
|
let volatile = false;
|
|
@@ -4567,6 +5020,14 @@ function extractReferences(ast, formulaSheet, registry = FunctionRegistry.defaul
|
|
|
4567
5020
|
refs.push(cell(node.sheetName, node.cellReference));
|
|
4568
5021
|
} else if (node instanceof SheetRangeReferenceNode) {
|
|
4569
5022
|
refs.push(range(node.sheetName, node.startRef, node.endRef));
|
|
5023
|
+
} else if (node instanceof FullRangeReferenceNode) {
|
|
5024
|
+
refs.push({
|
|
5025
|
+
sheet: (node.sheetName ?? formulaSheet).toUpperCase(),
|
|
5026
|
+
startRow: node.startRow ?? 1,
|
|
5027
|
+
endRow: node.endRow ?? puredocsExcel.EXCEL_MAX_ROWS,
|
|
5028
|
+
startCol: node.startCol ?? 1,
|
|
5029
|
+
endCol: node.endCol ?? puredocsExcel.EXCEL_MAX_COLUMNS
|
|
5030
|
+
});
|
|
4570
5031
|
} else if (node instanceof NamedRangeNode) {
|
|
4571
5032
|
expandName(node.name);
|
|
4572
5033
|
} else if (node instanceof SpillReferenceNode) {
|
|
@@ -4602,10 +5063,149 @@ function refContains(ref, sheet, row2, col) {
|
|
|
4602
5063
|
return ref.sheet === sheet && row2 >= ref.startRow && row2 <= ref.endRow && col >= ref.startCol && col <= ref.endCol;
|
|
4603
5064
|
}
|
|
4604
5065
|
|
|
5066
|
+
// src/dependency-index.ts
|
|
5067
|
+
var TILE_ROWS2 = 64;
|
|
5068
|
+
var TILE_COLS2 = 64;
|
|
5069
|
+
var MAX_TILES_PER_REF = 32;
|
|
5070
|
+
var MAX_EXACT_SPAN = 16;
|
|
5071
|
+
var MAX_BUCKETS_PER_REF = 64;
|
|
5072
|
+
var tileOfRow = (row2) => row2 / TILE_ROWS2 | 0;
|
|
5073
|
+
var tileOfCol = (col) => col / TILE_COLS2 | 0;
|
|
5074
|
+
var _buckets, _placements2, _DependencyIndex_instances, visit_fn, file_fn;
|
|
5075
|
+
var DependencyIndex = class {
|
|
5076
|
+
constructor() {
|
|
5077
|
+
__privateAdd(this, _DependencyIndex_instances);
|
|
5078
|
+
/**
|
|
5079
|
+
* All buckets in one map, keyed by a discriminating prefix:
|
|
5080
|
+
*
|
|
5081
|
+
* `t s tr tc` rectangle compact in both dimensions
|
|
5082
|
+
* `c s col tr` narrow columns, row tiles (the common case)
|
|
5083
|
+
* `C s col` narrow columns, every row (`A:A`)
|
|
5084
|
+
* `r s row tc` narrow rows, column tiles
|
|
5085
|
+
* `R s row` narrow rows, every column (`1:1`)
|
|
5086
|
+
* `w s` broad in both dimensions
|
|
5087
|
+
*/
|
|
5088
|
+
__privateAdd(this, _buckets, /* @__PURE__ */ new Map());
|
|
5089
|
+
/** formula key → the buckets it was filed under, for cheap removal. */
|
|
5090
|
+
__privateAdd(this, _placements2, /* @__PURE__ */ new Map());
|
|
5091
|
+
}
|
|
5092
|
+
/** Registers (or re-registers) a formula's precedent rectangles. */
|
|
5093
|
+
add(formulaKey, refs) {
|
|
5094
|
+
this.remove(formulaKey);
|
|
5095
|
+
if (refs.length === 0) return;
|
|
5096
|
+
const buckets = [];
|
|
5097
|
+
for (const ref of refs) {
|
|
5098
|
+
const rowSpan = ref.endRow - ref.startRow + 1;
|
|
5099
|
+
const colSpan = ref.endCol - ref.startCol + 1;
|
|
5100
|
+
const rowTile0 = tileOfRow(ref.startRow), rowTile1 = tileOfRow(ref.endRow);
|
|
5101
|
+
const colTile0 = tileOfCol(ref.startCol), colTile1 = tileOfCol(ref.endCol);
|
|
5102
|
+
const rowTiles = rowTile1 - rowTile0 + 1;
|
|
5103
|
+
const colTiles = colTile1 - colTile0 + 1;
|
|
5104
|
+
if (colSpan <= MAX_EXACT_SPAN && colSpan <= rowSpan) {
|
|
5105
|
+
if (colSpan * rowTiles <= MAX_BUCKETS_PER_REF && rowTiles <= MAX_TILES_PER_REF) {
|
|
5106
|
+
for (let col = ref.startCol; col <= ref.endCol; col++) {
|
|
5107
|
+
for (let tr = rowTile0; tr <= rowTile1; tr++) {
|
|
5108
|
+
__privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `c ${ref.sheet} ${col} ${tr}`, formulaKey, buckets);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
} else {
|
|
5112
|
+
for (let col = ref.startCol; col <= ref.endCol; col++) {
|
|
5113
|
+
__privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `C ${ref.sheet} ${col}`, formulaKey, buckets);
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
continue;
|
|
5117
|
+
}
|
|
5118
|
+
if (rowSpan <= MAX_EXACT_SPAN) {
|
|
5119
|
+
if (rowSpan * colTiles <= MAX_BUCKETS_PER_REF && colTiles <= MAX_TILES_PER_REF) {
|
|
5120
|
+
for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
|
|
5121
|
+
for (let tc = colTile0; tc <= colTile1; tc++) {
|
|
5122
|
+
__privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `r ${ref.sheet} ${row2} ${tc}`, formulaKey, buckets);
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
} else {
|
|
5126
|
+
for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
|
|
5127
|
+
__privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `R ${ref.sheet} ${row2}`, formulaKey, buckets);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
continue;
|
|
5131
|
+
}
|
|
5132
|
+
if (rowTiles * colTiles <= MAX_TILES_PER_REF) {
|
|
5133
|
+
for (let tr = rowTile0; tr <= rowTile1; tr++) {
|
|
5134
|
+
for (let tc = colTile0; tc <= colTile1; tc++) {
|
|
5135
|
+
__privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `t ${ref.sheet} ${tr} ${tc}`, formulaKey, buckets);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
__privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `w ${ref.sheet}`, formulaKey, buckets);
|
|
5141
|
+
}
|
|
5142
|
+
__privateGet(this, _placements2).set(formulaKey, buckets);
|
|
5143
|
+
}
|
|
5144
|
+
/** Forgets a formula. */
|
|
5145
|
+
remove(formulaKey) {
|
|
5146
|
+
const buckets = __privateGet(this, _placements2).get(formulaKey);
|
|
5147
|
+
if (!buckets) return;
|
|
5148
|
+
for (const bucket of buckets) {
|
|
5149
|
+
const set = __privateGet(this, _buckets).get(bucket);
|
|
5150
|
+
if (!set) continue;
|
|
5151
|
+
set.delete(formulaKey);
|
|
5152
|
+
if (set.size === 0) __privateGet(this, _buckets).delete(bucket);
|
|
5153
|
+
}
|
|
5154
|
+
__privateGet(this, _placements2).delete(formulaKey);
|
|
5155
|
+
}
|
|
5156
|
+
/** Drops every registration. */
|
|
5157
|
+
clear() {
|
|
5158
|
+
__privateGet(this, _buckets).clear();
|
|
5159
|
+
__privateGet(this, _placements2).clear();
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* Calls `visit` with the key of each formula that *may* read the given cell.
|
|
5163
|
+
* Callers still confirm with {@link refContains}: buckets are coarser than
|
|
5164
|
+
* rectangles in the broad dimension, so this over-approximates.
|
|
5165
|
+
*
|
|
5166
|
+
* A fixed six lookups, whatever the workbook contains.
|
|
5167
|
+
*/
|
|
5168
|
+
candidates(sheet, row2, col, visit) {
|
|
5169
|
+
const tr = tileOfRow(row2);
|
|
5170
|
+
const tc = tileOfCol(col);
|
|
5171
|
+
__privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `c ${sheet} ${col} ${tr}`, visit);
|
|
5172
|
+
__privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `C ${sheet} ${col}`, visit);
|
|
5173
|
+
__privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `r ${sheet} ${row2} ${tc}`, visit);
|
|
5174
|
+
__privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `R ${sheet} ${row2}`, visit);
|
|
5175
|
+
__privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `t ${sheet} ${tr} ${tc}`, visit);
|
|
5176
|
+
__privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `w ${sheet}`, visit);
|
|
5177
|
+
}
|
|
5178
|
+
};
|
|
5179
|
+
_buckets = new WeakMap();
|
|
5180
|
+
_placements2 = new WeakMap();
|
|
5181
|
+
_DependencyIndex_instances = new WeakSet();
|
|
5182
|
+
visit_fn = function(bucket, visit) {
|
|
5183
|
+
const set = __privateGet(this, _buckets).get(bucket);
|
|
5184
|
+
if (set) for (const key of set) visit(key);
|
|
5185
|
+
};
|
|
5186
|
+
file_fn = function(bucket, formulaKey, buckets) {
|
|
5187
|
+
let set = __privateGet(this, _buckets).get(bucket);
|
|
5188
|
+
if (!set) {
|
|
5189
|
+
set = /* @__PURE__ */ new Set();
|
|
5190
|
+
__privateGet(this, _buckets).set(bucket, set);
|
|
5191
|
+
}
|
|
5192
|
+
if (set.has(formulaKey)) return;
|
|
5193
|
+
set.add(formulaKey);
|
|
5194
|
+
buckets.push(bucket);
|
|
5195
|
+
};
|
|
5196
|
+
|
|
4605
5197
|
// src/recalc-engine.ts
|
|
4606
5198
|
function createWorkbookRecalcModel(workbook) {
|
|
4607
5199
|
return {
|
|
4608
5200
|
getCellValue: (sheet, ref) => workbook.getWorksheet(sheet).getCellValue(ref),
|
|
5201
|
+
// Falls back to the reference-string read rather than reporting "no value":
|
|
5202
|
+
// an older core without getCellValueAt must still return the real value,
|
|
5203
|
+
// because a silent undefined here reads as a blank cell.
|
|
5204
|
+
getCellValueAt: (sheet, row2, column2) => {
|
|
5205
|
+
const ws = workbook.getWorksheet(sheet);
|
|
5206
|
+
return ws.getCellValueAt ? ws.getCellValueAt(row2, column2) : ws.getCellValue(puredocsExcel.cellRefFromRowCol(row2, column2));
|
|
5207
|
+
},
|
|
5208
|
+
getUsedBounds: (sheet) => workbook.getWorksheet(sheet).usedBounds,
|
|
4609
5209
|
setCellValue: (sheet, ref, value2) => {
|
|
4610
5210
|
workbook.getWorksheet(sheet).getCell(ref).setComputedValue(value2);
|
|
4611
5211
|
},
|
|
@@ -4634,7 +5234,7 @@ function parseKey(key) {
|
|
|
4634
5234
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(ref);
|
|
4635
5235
|
return { sheet, ref, row: row2, col: column2 };
|
|
4636
5236
|
}
|
|
4637
|
-
var _model, _registry, _formulas, _sheetNames, _spills, _spillOwner, _RecalcEngine_instances, rememberSheet_fn, recalcFrom_fn, dependentClosure_fn,
|
|
5237
|
+
var _model, _registry, _formulas, _sheetNames, _spills, _spillOwner, _dependents, _volatiles, _sheetLikes, _workbookLike, _rangeCache, _RecalcEngine_instances, writeCell_fn, register_fn, unregister_fn, rememberSheet_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;
|
|
4638
5238
|
var RecalcEngine = class {
|
|
4639
5239
|
constructor(model, registry = FunctionRegistry.default) {
|
|
4640
5240
|
__privateAdd(this, _RecalcEngine_instances);
|
|
@@ -4647,6 +5247,27 @@ var RecalcEngine = class {
|
|
|
4647
5247
|
__privateAdd(this, _spills, /* @__PURE__ */ new Map());
|
|
4648
5248
|
/** spilled cellKey → the anchor cellKey that owns it. */
|
|
4649
5249
|
__privateAdd(this, _spillOwner, /* @__PURE__ */ new Map());
|
|
5250
|
+
/**
|
|
5251
|
+
* Reverse dependency index, so finding the formulas that read a cell does not
|
|
5252
|
+
* mean testing every registered formula.
|
|
5253
|
+
*/
|
|
5254
|
+
__privateAdd(this, _dependents, new DependencyIndex());
|
|
5255
|
+
/**
|
|
5256
|
+
* Keys of the volatile formulas. Maintained incrementally: scanning every
|
|
5257
|
+
* formula for volatility on each edit is O(F) per keystroke on its own.
|
|
5258
|
+
*/
|
|
5259
|
+
__privateAdd(this, _volatiles, /* @__PURE__ */ new Set());
|
|
5260
|
+
/** Memoised per-sheet adapters — these are stateless w.r.t. the formula. */
|
|
5261
|
+
__privateAdd(this, _sheetLikes, /* @__PURE__ */ new Map());
|
|
5262
|
+
__privateAdd(this, _workbookLike);
|
|
5263
|
+
/**
|
|
5264
|
+
* Memo for range reads. The engine owns it because it owns the writes: every
|
|
5265
|
+
* value written goes through {@link #writeCell}, which drops the cached
|
|
5266
|
+
* rectangles containing that cell. Nothing else may write to the model behind
|
|
5267
|
+
* the engine's back without calling {@link onCellChanged} — that was already
|
|
5268
|
+
* true for dependency tracking, and now also keeps this memo honest.
|
|
5269
|
+
*/
|
|
5270
|
+
__privateAdd(this, _rangeCache, new RangeCache());
|
|
4650
5271
|
__privateSet(this, _model, model);
|
|
4651
5272
|
__privateSet(this, _registry, registry);
|
|
4652
5273
|
}
|
|
@@ -4662,7 +5283,7 @@ var RecalcEngine = class {
|
|
|
4662
5283
|
const s = normSheet(sheet);
|
|
4663
5284
|
const r = normRef(ref);
|
|
4664
5285
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
4665
|
-
const ast =
|
|
5286
|
+
const ast = parseFormulaUncached(text);
|
|
4666
5287
|
const { refs, volatile } = extractReferences(
|
|
4667
5288
|
ast,
|
|
4668
5289
|
s,
|
|
@@ -4670,18 +5291,76 @@ var RecalcEngine = class {
|
|
|
4670
5291
|
__privateGet(this, _model).getDefinedName ? (n, sh) => __privateGet(this, _model).getDefinedName(n, sh) : void 0
|
|
4671
5292
|
);
|
|
4672
5293
|
__privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
|
|
4673
|
-
|
|
5294
|
+
const key = keyOf(s, r);
|
|
5295
|
+
__privateMethod(this, _RecalcEngine_instances, register_fn).call(this, {
|
|
4674
5296
|
sheet: s,
|
|
4675
5297
|
sheetName: sheet,
|
|
4676
5298
|
ref: r,
|
|
5299
|
+
key,
|
|
4677
5300
|
row: row2,
|
|
4678
5301
|
col: column2,
|
|
4679
5302
|
formula: text,
|
|
5303
|
+
ast,
|
|
4680
5304
|
refs,
|
|
4681
5305
|
volatile
|
|
4682
5306
|
});
|
|
4683
5307
|
__privateGet(this, _model).setCellFormula?.(sheet, r, text);
|
|
4684
|
-
|
|
5308
|
+
__privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
|
|
5309
|
+
return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }], key);
|
|
5310
|
+
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Indexes many formulas **without evaluating anything**.
|
|
5313
|
+
*
|
|
5314
|
+
* This is the path for loading a workbook. `setCellFormula` is built for an
|
|
5315
|
+
* edit: it recalculates the new cell and everything downstream of it, which is
|
|
5316
|
+
* right for one keystroke and quadratic for a file — registering N formulas one
|
|
5317
|
+
* by one evaluates the whole workbook N times. Here the formulas are only
|
|
5318
|
+
* parsed and filed into the dependency index; call {@link recalcAll} once
|
|
5319
|
+
* afterwards, or nothing at all if the cached values from the file will do.
|
|
5320
|
+
*
|
|
5321
|
+
* The model is deliberately **not** written to. These formulas are assumed to
|
|
5322
|
+
* be the ones the model already holds (they came from the file), and persisting
|
|
5323
|
+
* them again would clear each cell's cached value — leaving every formula cell
|
|
5324
|
+
* blank until a full recalculation had run. Use `setCellFormula` for formulas
|
|
5325
|
+
* the user is actually introducing.
|
|
5326
|
+
*
|
|
5327
|
+
* A formula that fails to parse is reported rather than thrown: one unsupported
|
|
5328
|
+
* construct in a large workbook should not abort the load.
|
|
5329
|
+
*/
|
|
5330
|
+
registerBulk(entries) {
|
|
5331
|
+
const resolveName2 = __privateGet(this, _model).getDefinedName ? (n, sh) => __privateGet(this, _model).getDefinedName(n, sh) : void 0;
|
|
5332
|
+
let registered = 0;
|
|
5333
|
+
const failed = [];
|
|
5334
|
+
for (const entry of entries) {
|
|
5335
|
+
const { sheet, ref, formula } = entry;
|
|
5336
|
+
if (!formula) continue;
|
|
5337
|
+
const text = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
5338
|
+
const s = normSheet(sheet);
|
|
5339
|
+
const r = normRef(ref);
|
|
5340
|
+
try {
|
|
5341
|
+
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5342
|
+
const ast = parseFormulaUncached(text);
|
|
5343
|
+
const { refs, volatile } = extractReferences(ast, s, __privateGet(this, _registry), resolveName2);
|
|
5344
|
+
__privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
|
|
5345
|
+
__privateMethod(this, _RecalcEngine_instances, register_fn).call(this, {
|
|
5346
|
+
sheet: s,
|
|
5347
|
+
sheetName: sheet,
|
|
5348
|
+
ref: r,
|
|
5349
|
+
key: keyOf(s, r),
|
|
5350
|
+
row: row2,
|
|
5351
|
+
col: column2,
|
|
5352
|
+
formula: text,
|
|
5353
|
+
ast,
|
|
5354
|
+
refs,
|
|
5355
|
+
volatile
|
|
5356
|
+
});
|
|
5357
|
+
registered++;
|
|
5358
|
+
} catch (err) {
|
|
5359
|
+
failed.push({ sheet, ref, error: err instanceof Error ? err.message : String(err) });
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
if (registered > 0) __privateGet(this, _rangeCache).clear();
|
|
5363
|
+
return { registered, failed };
|
|
4685
5364
|
}
|
|
4686
5365
|
/**
|
|
4687
5366
|
* Removes a formula from a cell and recalculates its dependents (which may now
|
|
@@ -4690,10 +5369,12 @@ var RecalcEngine = class {
|
|
|
4690
5369
|
clearCellFormula(sheet, ref) {
|
|
4691
5370
|
const s = normSheet(sheet);
|
|
4692
5371
|
const r = normRef(ref);
|
|
4693
|
-
const
|
|
4694
|
-
if (!
|
|
5372
|
+
const key = keyOf(s, r);
|
|
5373
|
+
if (!__privateGet(this, _formulas).has(key)) return [];
|
|
5374
|
+
__privateMethod(this, _RecalcEngine_instances, unregister_fn).call(this, key);
|
|
4695
5375
|
__privateGet(this, _model).clearCell?.(sheet, r);
|
|
4696
5376
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5377
|
+
__privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
|
|
4697
5378
|
return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
|
|
4698
5379
|
}
|
|
4699
5380
|
/**
|
|
@@ -4704,8 +5385,8 @@ var RecalcEngine = class {
|
|
|
4704
5385
|
__privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
|
|
4705
5386
|
const s = normSheet(sheet);
|
|
4706
5387
|
const r = normRef(ref);
|
|
4707
|
-
__privateGet(this, _model).setCellValue(sheet, r, value2);
|
|
4708
5388
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5389
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, sheet, r, value2, row2, column2);
|
|
4709
5390
|
return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
|
|
4710
5391
|
}
|
|
4711
5392
|
/**
|
|
@@ -4717,16 +5398,86 @@ var RecalcEngine = class {
|
|
|
4717
5398
|
const s = normSheet(sheet);
|
|
4718
5399
|
const r = normRef(ref);
|
|
4719
5400
|
const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
|
|
5401
|
+
__privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
|
|
4720
5402
|
return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
|
|
4721
5403
|
}
|
|
4722
|
-
/**
|
|
4723
|
-
|
|
4724
|
-
|
|
5404
|
+
/**
|
|
5405
|
+
* Recalculates every registered formula.
|
|
5406
|
+
*
|
|
5407
|
+
* By default the order is derived here, with a topological sort over the
|
|
5408
|
+
* dependency index. `order` — typically `workbook.calcChain` — supplies one
|
|
5409
|
+
* instead, so the sort is not needed.
|
|
5410
|
+
*
|
|
5411
|
+
* **A supplied order is verified, not trusted.** Evaluating in a stale order
|
|
5412
|
+
* makes a formula read a precedent that has not been computed yet, and the
|
|
5413
|
+
* result is a plausible wrong number with nothing to distinguish it from a
|
|
5414
|
+
* right one. In a calculation engine that is the worst failure available, so
|
|
5415
|
+
* this checks as it goes: after each formula is written, the dependency index
|
|
5416
|
+
* says who reads it, and any reader that was already evaluated is an inversion.
|
|
5417
|
+
* On the first inversion the ordered attempt is abandoned and the whole
|
|
5418
|
+
* recalculation is redone through the sorted path, whose result is returned;
|
|
5419
|
+
* `onOrderConflict`, if given, is told which pair conflicted.
|
|
5420
|
+
*
|
|
5421
|
+
* The verification costs about what the sort it replaces costs, so `order` is
|
|
5422
|
+
* worth roughly nothing now (measured at ~9% of a full recalculation on a
|
|
5423
|
+
* 2 000-formula workbook, after the dependency index stopped being the
|
|
5424
|
+
* bottleneck). It is kept because reading an order Excel already computed is a
|
|
5425
|
+
* reasonable thing to want; prefer plain `recalcAll()` unless you have measured
|
|
5426
|
+
* a reason not to.
|
|
5427
|
+
*
|
|
5428
|
+
* Registered formulas the order does not mention are evaluated afterwards
|
|
5429
|
+
* through the sorted path, and a dynamic array that spills still triggers the
|
|
5430
|
+
* usual fixpoint, so neither silently misses cells.
|
|
5431
|
+
*/
|
|
5432
|
+
recalcAll(options) {
|
|
5433
|
+
const order = options?.order;
|
|
5434
|
+
if (!order || order.length === 0) {
|
|
5435
|
+
return __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...__privateGet(this, _formulas).values()]).results;
|
|
5436
|
+
}
|
|
5437
|
+
const attempt = __privateMethod(this, _RecalcEngine_instances, recalcInOrder_fn).call(this, order);
|
|
5438
|
+
if (attempt.conflict) {
|
|
5439
|
+
options?.onOrderConflict?.(attempt.conflict);
|
|
5440
|
+
return __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...__privateGet(this, _formulas).values()]).results;
|
|
5441
|
+
}
|
|
5442
|
+
return attempt.results;
|
|
4725
5443
|
}
|
|
4726
5444
|
/** The precedents recorded for a formula cell (mainly for tests/inspection). */
|
|
4727
5445
|
getPrecedents(sheet, ref) {
|
|
4728
5446
|
return __privateGet(this, _formulas).get(keyOf(sheet, ref))?.refs ?? [];
|
|
4729
5447
|
}
|
|
5448
|
+
// ── Structural edits ─────────────────────────────────────────────────────────
|
|
5449
|
+
/**
|
|
5450
|
+
* Tells the engine rows were inserted on a sheet. Call AFTER the
|
|
5451
|
+
* corresponding `Worksheet.insertRows` (the model must already reflect the
|
|
5452
|
+
* edit). The dependency graph is translated in place — registrations,
|
|
5453
|
+
* formula text and spill bookkeeping all move — and only volatile formulas
|
|
5454
|
+
* are recomputed, because a pure translation changes no values.
|
|
5455
|
+
*
|
|
5456
|
+
* @example
|
|
5457
|
+
* ws.insertRows(3, 2);
|
|
5458
|
+
* engine.onRowsInserted('S1', 3, 2);
|
|
5459
|
+
*/
|
|
5460
|
+
onRowsInserted(sheet, at, count2 = 1) {
|
|
5461
|
+
return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "row", kind: "insert", at, count: count2, sheetName: sheet });
|
|
5462
|
+
}
|
|
5463
|
+
/**
|
|
5464
|
+
* Tells the engine rows were deleted on a sheet. Call AFTER the
|
|
5465
|
+
* corresponding `Worksheet.deleteRows`. Registrations inside the deleted
|
|
5466
|
+
* band are dropped; the rest of the graph is translated, and only formulas
|
|
5467
|
+
* whose precedents touched the deleted band (plus their dependents and
|
|
5468
|
+
* volatiles) are recomputed.
|
|
5469
|
+
*/
|
|
5470
|
+
onRowsDeleted(sheet, at, count2 = 1) {
|
|
5471
|
+
return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "row", kind: "delete", at, count: count2, sheetName: sheet });
|
|
5472
|
+
}
|
|
5473
|
+
/** Column counterpart to {@link onRowsInserted}. */
|
|
5474
|
+
onColumnsInserted(sheet, at, count2 = 1) {
|
|
5475
|
+
return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "col", kind: "insert", at, count: count2, sheetName: sheet });
|
|
5476
|
+
}
|
|
5477
|
+
/** Column counterpart to {@link onRowsDeleted}. */
|
|
5478
|
+
onColumnsDeleted(sheet, at, count2 = 1) {
|
|
5479
|
+
return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "col", kind: "delete", at, count: count2, sheetName: sheet });
|
|
5480
|
+
}
|
|
4730
5481
|
};
|
|
4731
5482
|
_model = new WeakMap();
|
|
4732
5483
|
_registry = new WeakMap();
|
|
@@ -4734,10 +5485,212 @@ _formulas = new WeakMap();
|
|
|
4734
5485
|
_sheetNames = new WeakMap();
|
|
4735
5486
|
_spills = new WeakMap();
|
|
4736
5487
|
_spillOwner = new WeakMap();
|
|
5488
|
+
_dependents = new WeakMap();
|
|
5489
|
+
_volatiles = new WeakMap();
|
|
5490
|
+
_sheetLikes = new WeakMap();
|
|
5491
|
+
_workbookLike = new WeakMap();
|
|
5492
|
+
_rangeCache = new WeakMap();
|
|
4737
5493
|
_RecalcEngine_instances = new WeakSet();
|
|
5494
|
+
/**
|
|
5495
|
+
* The single write path into the model. Writing a value invalidates the
|
|
5496
|
+
* cached rectangles that contain the cell, so a formula evaluated later in the
|
|
5497
|
+
* same pass cannot read a pre-write copy of a range. Coordinates are passed in
|
|
5498
|
+
* where the caller already knows them, to avoid re-parsing the reference.
|
|
5499
|
+
*/
|
|
5500
|
+
writeCell_fn = function(sheetName, ref, value2, row2, col) {
|
|
5501
|
+
__privateGet(this, _model).setCellValue(sheetName, ref, value2);
|
|
5502
|
+
if (row2 === void 0 || col === void 0) {
|
|
5503
|
+
const parsed = puredocsExcel.parseCellRef(ref);
|
|
5504
|
+
row2 = parsed.row;
|
|
5505
|
+
col = parsed.column;
|
|
5506
|
+
}
|
|
5507
|
+
__privateGet(this, _rangeCache).invalidateCell(normSheet(sheetName), row2, col);
|
|
5508
|
+
};
|
|
5509
|
+
/** Registers an entry in the map, the reverse index and the volatile set. */
|
|
5510
|
+
register_fn = function(entry) {
|
|
5511
|
+
__privateGet(this, _formulas).set(entry.key, entry);
|
|
5512
|
+
__privateGet(this, _dependents).add(entry.key, entry.refs);
|
|
5513
|
+
if (entry.volatile) __privateGet(this, _volatiles).add(entry.key);
|
|
5514
|
+
else __privateGet(this, _volatiles).delete(entry.key);
|
|
5515
|
+
};
|
|
5516
|
+
unregister_fn = function(key) {
|
|
5517
|
+
__privateGet(this, _formulas).delete(key);
|
|
5518
|
+
__privateGet(this, _dependents).remove(key);
|
|
5519
|
+
__privateGet(this, _volatiles).delete(key);
|
|
5520
|
+
};
|
|
4738
5521
|
rememberSheet_fn = function(name) {
|
|
4739
5522
|
__privateGet(this, _sheetNames).set(normSheet(name), name);
|
|
4740
5523
|
};
|
|
5524
|
+
/**
|
|
5525
|
+
* One pass in a caller-supplied order, watching for inversions.
|
|
5526
|
+
*
|
|
5527
|
+
* The check is the mirror image of the topological sort: instead of asking
|
|
5528
|
+
* "which of my precedents come first?", it asks, of each formula just
|
|
5529
|
+
* evaluated, "has anything that reads me already run?". The dependency index
|
|
5530
|
+
* answers that directly, and one positive answer is enough to know the order
|
|
5531
|
+
* cannot be trusted.
|
|
5532
|
+
*/
|
|
5533
|
+
recalcInOrder_fn = function(order) {
|
|
5534
|
+
const results = [];
|
|
5535
|
+
const changed = [];
|
|
5536
|
+
const done = /* @__PURE__ */ new Set();
|
|
5537
|
+
let spilled = false;
|
|
5538
|
+
for (const { sheet, ref } of order) {
|
|
5539
|
+
const key = keyOf(sheet, ref);
|
|
5540
|
+
if (done.has(key)) continue;
|
|
5541
|
+
const entry = __privateGet(this, _formulas).get(key);
|
|
5542
|
+
if (!entry) continue;
|
|
5543
|
+
done.add(key);
|
|
5544
|
+
const written = __privateMethod(this, _RecalcEngine_instances, writeResult_fn).call(this, entry);
|
|
5545
|
+
results.push(written.result);
|
|
5546
|
+
changed.push(...written.changed);
|
|
5547
|
+
if (written.result.spill) spilled = true;
|
|
5548
|
+
let conflict;
|
|
5549
|
+
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: entry.sheet, row: entry.row, col: entry.col }, (dep) => {
|
|
5550
|
+
if (conflict || dep.key === entry.key) return;
|
|
5551
|
+
if (done.has(dep.key)) conflict = { dependent: dep.key, precedent: entry.key };
|
|
5552
|
+
});
|
|
5553
|
+
if (conflict) return { results: [], conflict };
|
|
5554
|
+
}
|
|
5555
|
+
const rest = [...__privateGet(this, _formulas).values()].filter((e) => !done.has(e.key));
|
|
5556
|
+
if (rest.length > 0) {
|
|
5557
|
+
const pass = __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, rest);
|
|
5558
|
+
results.push(...pass.results);
|
|
5559
|
+
changed.push(...pass.changed);
|
|
5560
|
+
}
|
|
5561
|
+
if (spilled) {
|
|
5562
|
+
const evaluated = new Set(done);
|
|
5563
|
+
results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, changed, evaluated), evaluated));
|
|
5564
|
+
}
|
|
5565
|
+
return { results };
|
|
5566
|
+
};
|
|
5567
|
+
applyShift_fn = function(shift) {
|
|
5568
|
+
const editedSheet = normSheet(shift.sheetName);
|
|
5569
|
+
const isRowShift = shift.dim === "row";
|
|
5570
|
+
const bandEnd = shift.at + shift.count - 1;
|
|
5571
|
+
const rekeyed = [];
|
|
5572
|
+
const needsRecalc = /* @__PURE__ */ new Set();
|
|
5573
|
+
for (const e of __privateGet(this, _formulas).values()) {
|
|
5574
|
+
let row2 = e.row;
|
|
5575
|
+
let col = e.col;
|
|
5576
|
+
if (e.sheet === editedSheet) {
|
|
5577
|
+
const shifted = puredocsExcel.shiftIndex(isRowShift ? row2 : col, shift);
|
|
5578
|
+
if (shifted === null) continue;
|
|
5579
|
+
if (isRowShift) row2 = shifted;
|
|
5580
|
+
else col = shifted;
|
|
5581
|
+
}
|
|
5582
|
+
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));
|
|
5583
|
+
const rewritten = puredocsExcel.shiftFormulaRefs(e.formula, shift, e.sheetName);
|
|
5584
|
+
const moved = row2 !== e.row || col !== e.col;
|
|
5585
|
+
const ref = moved ? puredocsExcel.cellRefFromRowCol(row2, col) : e.ref;
|
|
5586
|
+
const key = moved ? keyOf(e.sheet, ref) : e.key;
|
|
5587
|
+
rekeyed.push({
|
|
5588
|
+
...e,
|
|
5589
|
+
ref,
|
|
5590
|
+
key,
|
|
5591
|
+
row: row2,
|
|
5592
|
+
col,
|
|
5593
|
+
formula: rewritten.changed ? rewritten.text : e.formula,
|
|
5594
|
+
// A rewritten formula's cached AST no longer matches its text; it is
|
|
5595
|
+
// re-parsed lazily, and only if the cell is actually evaluated again.
|
|
5596
|
+
ast: rewritten.changed ? void 0 : e.ast,
|
|
5597
|
+
refs: __privateMethod(this, _RecalcEngine_instances, shiftPrecedents_fn).call(this, e.refs, shift, editedSheet)
|
|
5598
|
+
// Volatility is a property of which *functions* a formula calls, and
|
|
5599
|
+
// rewriting references cannot change that.
|
|
5600
|
+
});
|
|
5601
|
+
if (touchedBand || rewritten.hasRefError) needsRecalc.add(key);
|
|
5602
|
+
}
|
|
5603
|
+
__privateSet(this, _formulas, /* @__PURE__ */ new Map());
|
|
5604
|
+
__privateGet(this, _dependents).clear();
|
|
5605
|
+
__privateGet(this, _volatiles).clear();
|
|
5606
|
+
for (const e of rekeyed) __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, e);
|
|
5607
|
+
__privateMethod(this, _RecalcEngine_instances, shiftSpillBookkeeping_fn).call(this, shift, editedSheet);
|
|
5608
|
+
__privateGet(this, _rangeCache).clear();
|
|
5609
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5610
|
+
for (const k of needsRecalc) {
|
|
5611
|
+
const e = __privateGet(this, _formulas).get(k);
|
|
5612
|
+
if (e) pending.set(k, e);
|
|
5613
|
+
}
|
|
5614
|
+
for (const e of __privateGet(this, _formulas).values()) {
|
|
5615
|
+
if (e.volatile) pending.set(e.key, e);
|
|
5616
|
+
}
|
|
5617
|
+
return __privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, /* @__PURE__ */ new Set());
|
|
5618
|
+
};
|
|
5619
|
+
/**
|
|
5620
|
+
* Translates precedent rectangles across a structural edit.
|
|
5621
|
+
*
|
|
5622
|
+
* This is arithmetic rather than a re-parse of the rewritten formula, and the
|
|
5623
|
+
* two are equivalent by construction: `shiftFormulaRefs` derives the new
|
|
5624
|
+
* reference text by calling `shiftSpan`/`shiftIndex` on exactly these numbers,
|
|
5625
|
+
* so applying the same functions to the stored rectangle yields the same
|
|
5626
|
+
* region. A rectangle that is entirely deleted disappears, matching the
|
|
5627
|
+
* `#REF!` the text rewrite produces.
|
|
5628
|
+
*/
|
|
5629
|
+
shiftPrecedents_fn = function(refs, shift, editedSheet) {
|
|
5630
|
+
const out = [];
|
|
5631
|
+
for (const r of refs) {
|
|
5632
|
+
if (r.sheet !== editedSheet) {
|
|
5633
|
+
out.push(r);
|
|
5634
|
+
continue;
|
|
5635
|
+
}
|
|
5636
|
+
if (shift.dim === "row") {
|
|
5637
|
+
const span = puredocsExcel.shiftSpan(r.startRow, r.endRow, shift);
|
|
5638
|
+
if (!span) continue;
|
|
5639
|
+
out.push({
|
|
5640
|
+
sheet: r.sheet,
|
|
5641
|
+
startRow: span.start,
|
|
5642
|
+
endRow: Math.min(span.end, puredocsExcel.EXCEL_MAX_ROWS),
|
|
5643
|
+
startCol: r.startCol,
|
|
5644
|
+
endCol: r.endCol
|
|
5645
|
+
});
|
|
5646
|
+
} else {
|
|
5647
|
+
const span = puredocsExcel.shiftSpan(r.startCol, r.endCol, shift);
|
|
5648
|
+
if (!span) continue;
|
|
5649
|
+
out.push({
|
|
5650
|
+
sheet: r.sheet,
|
|
5651
|
+
startRow: r.startRow,
|
|
5652
|
+
endRow: r.endRow,
|
|
5653
|
+
startCol: span.start,
|
|
5654
|
+
endCol: Math.min(span.end, puredocsExcel.EXCEL_MAX_COLUMNS)
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
return out;
|
|
5659
|
+
};
|
|
5660
|
+
/** Translates spill ownership across a structural edit on `editedSheet`. */
|
|
5661
|
+
shiftSpillBookkeeping_fn = function(shift, editedSheet) {
|
|
5662
|
+
const shiftCellKey = (key) => {
|
|
5663
|
+
const { sheet, row: row2, col } = parseKey(key);
|
|
5664
|
+
if (sheet !== editedSheet) return key;
|
|
5665
|
+
const r = shift.dim === "row" ? puredocsExcel.shiftIndex(row2, shift) : row2;
|
|
5666
|
+
const c = shift.dim === "col" ? puredocsExcel.shiftIndex(col, shift) : col;
|
|
5667
|
+
if (r === null || c === null) return null;
|
|
5668
|
+
return keyOf(sheet, puredocsExcel.cellRefFromRowCol(r, c));
|
|
5669
|
+
};
|
|
5670
|
+
const newSpills = /* @__PURE__ */ new Map();
|
|
5671
|
+
__privateGet(this, _spillOwner).clear();
|
|
5672
|
+
for (const [anchor, members] of __privateGet(this, _spills)) {
|
|
5673
|
+
const newAnchor = shiftCellKey(anchor);
|
|
5674
|
+
if (newAnchor === null) {
|
|
5675
|
+
for (const m of members) {
|
|
5676
|
+
const shifted = shiftCellKey(m);
|
|
5677
|
+
if (!shifted) continue;
|
|
5678
|
+
const cell = parseKey(shifted);
|
|
5679
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
5680
|
+
}
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
const newMembers = /* @__PURE__ */ new Set();
|
|
5684
|
+
for (const m of members) {
|
|
5685
|
+
const shifted = shiftCellKey(m);
|
|
5686
|
+
if (!shifted) continue;
|
|
5687
|
+
newMembers.add(shifted);
|
|
5688
|
+
__privateGet(this, _spillOwner).set(shifted, newAnchor);
|
|
5689
|
+
}
|
|
5690
|
+
newSpills.set(newAnchor, newMembers);
|
|
5691
|
+
}
|
|
5692
|
+
__privateSet(this, _spills, newSpills);
|
|
5693
|
+
};
|
|
4741
5694
|
// ── Internals ────────────────────────────────────────────────────────────────
|
|
4742
5695
|
/**
|
|
4743
5696
|
* Recomputes formulas affected by the given changed cells, to a fixpoint.
|
|
@@ -4753,13 +5706,17 @@ recalcFrom_fn = function(seeds, includeKey) {
|
|
|
4753
5706
|
const results = [];
|
|
4754
5707
|
const evaluated = /* @__PURE__ */ new Set();
|
|
4755
5708
|
let pending = __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, seeds, evaluated);
|
|
4756
|
-
|
|
4757
|
-
if (e.volatile) pending.set(keyOf(e.sheet, e.ref), e);
|
|
4758
|
-
}
|
|
5709
|
+
__privateMethod(this, _RecalcEngine_instances, addVolatiles_fn).call(this, pending);
|
|
4759
5710
|
if (includeKey) {
|
|
4760
5711
|
const self = __privateGet(this, _formulas).get(includeKey);
|
|
4761
5712
|
if (self) pending.set(includeKey, self);
|
|
4762
5713
|
}
|
|
5714
|
+
results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, evaluated));
|
|
5715
|
+
return results;
|
|
5716
|
+
};
|
|
5717
|
+
/** Evaluates `pending` entries to a fixpoint, following spill-induced changes. */
|
|
5718
|
+
runFixpoint_fn = function(pending, evaluated) {
|
|
5719
|
+
const results = [];
|
|
4763
5720
|
let guard = 0;
|
|
4764
5721
|
const maxIterations = __privateGet(this, _formulas).size + 8;
|
|
4765
5722
|
while (pending.size > 0 && guard++ <= maxIterations) {
|
|
@@ -4770,28 +5727,47 @@ recalcFrom_fn = function(seeds, includeKey) {
|
|
|
4770
5727
|
}
|
|
4771
5728
|
return results;
|
|
4772
5729
|
};
|
|
4773
|
-
/**
|
|
5730
|
+
/** Adds every volatile formula to a pending set. */
|
|
5731
|
+
addVolatiles_fn = function(pending) {
|
|
5732
|
+
for (const key of __privateGet(this, _volatiles)) {
|
|
5733
|
+
const e = __privateGet(this, _formulas).get(key);
|
|
5734
|
+
if (e) pending.set(key, e);
|
|
5735
|
+
}
|
|
5736
|
+
};
|
|
5737
|
+
/**
|
|
5738
|
+
* Formula entries (not yet evaluated) whose precedents include one of the
|
|
5739
|
+
* cells, transitively. The queue is walked with a head index — Array.shift()
|
|
5740
|
+
* is O(n) per dequeue, which turns a long dependency chain quadratic.
|
|
5741
|
+
*/
|
|
4774
5742
|
dependentClosure_fn = function(cells, evaluated) {
|
|
4775
5743
|
const out = /* @__PURE__ */ new Map();
|
|
4776
5744
|
const queue = [...cells];
|
|
4777
|
-
|
|
4778
|
-
const cell = queue
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
out.set(k, dep);
|
|
5745
|
+
for (let head = 0; head < queue.length; head++) {
|
|
5746
|
+
const cell = queue[head];
|
|
5747
|
+
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, cell, (dep) => {
|
|
5748
|
+
if (out.has(dep.key) || evaluated.has(dep.key)) return;
|
|
5749
|
+
out.set(dep.key, dep);
|
|
4783
5750
|
queue.push({ sheet: dep.sheet, row: dep.row, col: dep.col });
|
|
4784
|
-
}
|
|
5751
|
+
});
|
|
4785
5752
|
}
|
|
4786
5753
|
return out;
|
|
4787
5754
|
};
|
|
4788
|
-
/**
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
5755
|
+
/**
|
|
5756
|
+
* Visits the formula entries whose precedents include the given cell, via the
|
|
5757
|
+
* reverse index. The index over-approximates (it buckets rectangles into
|
|
5758
|
+
* tiles), so each candidate is still confirmed against its real rectangles.
|
|
5759
|
+
*/
|
|
5760
|
+
forEachDirectDependent_fn = function(cell, visit) {
|
|
5761
|
+
__privateGet(this, _dependents).candidates(cell.sheet, cell.row, cell.col, (key) => {
|
|
5762
|
+
const e = __privateGet(this, _formulas).get(key);
|
|
5763
|
+
if (!e) return;
|
|
5764
|
+
for (const ref of e.refs) {
|
|
5765
|
+
if (refContains(ref, cell.sheet, cell.row, cell.col)) {
|
|
5766
|
+
visit(e);
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
});
|
|
4795
5771
|
};
|
|
4796
5772
|
/**
|
|
4797
5773
|
* Topologically sorts the given entries and evaluates them, writing results
|
|
@@ -4800,32 +5776,32 @@ directDependents_fn = function(cell) {
|
|
|
4800
5776
|
*/
|
|
4801
5777
|
evaluatePass_fn = function(entries) {
|
|
4802
5778
|
const inSet = /* @__PURE__ */ new Map();
|
|
4803
|
-
for (const e of entries) inSet.set(
|
|
5779
|
+
for (const e of entries) inSet.set(e.key, e);
|
|
4804
5780
|
const indegree = /* @__PURE__ */ new Map();
|
|
4805
5781
|
const dependents = /* @__PURE__ */ new Map();
|
|
4806
5782
|
for (const k of inSet.keys()) {
|
|
4807
5783
|
indegree.set(k, 0);
|
|
4808
5784
|
dependents.set(k, []);
|
|
4809
5785
|
}
|
|
4810
|
-
for (const
|
|
4811
|
-
const
|
|
4812
|
-
|
|
4813
|
-
if (
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
}
|
|
5786
|
+
for (const pe of entries) {
|
|
5787
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5788
|
+
__privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: pe.sheet, row: pe.row, col: pe.col }, (dep) => {
|
|
5789
|
+
if (dep.key === pe.key || !inSet.has(dep.key) || seen.has(dep.key)) return;
|
|
5790
|
+
seen.add(dep.key);
|
|
5791
|
+
dependents.get(pe.key).push(dep.key);
|
|
5792
|
+
indegree.set(dep.key, indegree.get(dep.key) + 1);
|
|
5793
|
+
});
|
|
4819
5794
|
}
|
|
4820
5795
|
const ready = [];
|
|
4821
5796
|
for (const [k, d] of indegree) if (d === 0) ready.push(k);
|
|
4822
5797
|
const order = [];
|
|
4823
|
-
|
|
4824
|
-
const k = ready
|
|
5798
|
+
for (let head = 0; head < ready.length; head++) {
|
|
5799
|
+
const k = ready[head];
|
|
4825
5800
|
order.push(k);
|
|
4826
5801
|
for (const d of dependents.get(k)) {
|
|
4827
|
-
|
|
4828
|
-
|
|
5802
|
+
const left2 = indegree.get(d) - 1;
|
|
5803
|
+
indegree.set(d, left2);
|
|
5804
|
+
if (left2 === 0) ready.push(d);
|
|
4829
5805
|
}
|
|
4830
5806
|
}
|
|
4831
5807
|
const results = [];
|
|
@@ -4839,9 +5815,9 @@ evaluatePass_fn = function(entries) {
|
|
|
4839
5815
|
}
|
|
4840
5816
|
for (const [k, e] of inSet) {
|
|
4841
5817
|
if (emitted.has(k)) continue;
|
|
4842
|
-
__privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this,
|
|
5818
|
+
__privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, e.key, changed);
|
|
4843
5819
|
const value2 = FormulaValue.errorRef.toObject();
|
|
4844
|
-
|
|
5820
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
|
|
4845
5821
|
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
4846
5822
|
results.push({ sheet: e.sheetName, ref: e.ref, value: value2, circular: true });
|
|
4847
5823
|
}
|
|
@@ -4854,7 +5830,7 @@ evaluatePass_fn = function(entries) {
|
|
|
4854
5830
|
* an array shrinks, previously-spilled cells it no longer covers are cleared.
|
|
4855
5831
|
*/
|
|
4856
5832
|
writeResult_fn = function(e) {
|
|
4857
|
-
const anchorKey =
|
|
5833
|
+
const anchorKey = e.key;
|
|
4858
5834
|
const fv2 = __privateMethod(this, _RecalcEngine_instances, evaluateFormulaValue_fn).call(this, e);
|
|
4859
5835
|
const changed = [];
|
|
4860
5836
|
const prevSpill = __privateGet(this, _spills).get(anchorKey) ?? /* @__PURE__ */ new Set();
|
|
@@ -4862,7 +5838,7 @@ writeResult_fn = function(e) {
|
|
|
4862
5838
|
__privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
|
|
4863
5839
|
__privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
|
|
4864
5840
|
const value2 = fv2.toObject();
|
|
4865
|
-
|
|
5841
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
|
|
4866
5842
|
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
4867
5843
|
return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
|
|
4868
5844
|
}
|
|
@@ -4883,7 +5859,7 @@ writeResult_fn = function(e) {
|
|
|
4883
5859
|
__privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
|
|
4884
5860
|
__privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
|
|
4885
5861
|
const value2 = FormulaValue.error(9 /* Spill */).toObject();
|
|
4886
|
-
|
|
5862
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
|
|
4887
5863
|
changed.push({ sheet: e.sheet, row: e.row, col: e.col });
|
|
4888
5864
|
return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
|
|
4889
5865
|
}
|
|
@@ -4892,7 +5868,7 @@ writeResult_fn = function(e) {
|
|
|
4892
5868
|
for (let dc = 0; dc < cols; dc++) {
|
|
4893
5869
|
const r = e.row + dr, c = e.col + dc;
|
|
4894
5870
|
const ref = puredocsExcel.cellRefFromRowCol(r, c);
|
|
4895
|
-
|
|
5871
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, ref, arr.get(dr, dc).toObject(), r, c);
|
|
4896
5872
|
changed.push({ sheet: e.sheet, row: r, col: c });
|
|
4897
5873
|
if (dr !== 0 || dc !== 0) {
|
|
4898
5874
|
const cellKey2 = keyOf(e.sheet, ref);
|
|
@@ -4904,7 +5880,7 @@ writeResult_fn = function(e) {
|
|
|
4904
5880
|
for (const oldKey of prevSpill) {
|
|
4905
5881
|
if (newSpill.has(oldKey)) continue;
|
|
4906
5882
|
const cell = parseKey(oldKey);
|
|
4907
|
-
|
|
5883
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
4908
5884
|
__privateGet(this, _spillOwner).delete(oldKey);
|
|
4909
5885
|
changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
|
|
4910
5886
|
}
|
|
@@ -4931,33 +5907,60 @@ clearSpill_fn = function(anchorKey, changed) {
|
|
|
4931
5907
|
if (!spill) return;
|
|
4932
5908
|
for (const cellKey2 of spill) {
|
|
4933
5909
|
const cell = parseKey(cellKey2);
|
|
4934
|
-
|
|
5910
|
+
__privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
|
|
4935
5911
|
__privateGet(this, _spillOwner).delete(cellKey2);
|
|
4936
5912
|
changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
|
|
4937
5913
|
}
|
|
4938
5914
|
__privateGet(this, _spills).delete(anchorKey);
|
|
4939
5915
|
};
|
|
4940
5916
|
evaluateFormulaValue_fn = function(e) {
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
5917
|
+
if (!e.ast) {
|
|
5918
|
+
try {
|
|
5919
|
+
e.ast = parseFormulaUncached(e.formula);
|
|
5920
|
+
} catch {
|
|
5921
|
+
return FormulaValue.errorValue;
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
return evaluateAst(e.ast, __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName), {
|
|
5925
|
+
workbook: __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this),
|
|
4945
5926
|
formulaRow: e.row,
|
|
4946
|
-
formulaCol: e.col
|
|
5927
|
+
formulaCol: e.col,
|
|
5928
|
+
rangeCache: __privateGet(this, _rangeCache)
|
|
4947
5929
|
});
|
|
4948
5930
|
};
|
|
5931
|
+
/**
|
|
5932
|
+
* Per-sheet evaluator adapter. Memoised: these hold no per-formula state, and
|
|
5933
|
+
* rebuilding one (with its four closures) for every formula evaluated made a
|
|
5934
|
+
* full recalc allocate several objects per cell.
|
|
5935
|
+
*/
|
|
4949
5936
|
sheetLike_fn = function(sheetName) {
|
|
5937
|
+
let cached = __privateGet(this, _sheetLikes).get(sheetName);
|
|
5938
|
+
if (!cached) {
|
|
5939
|
+
cached = __privateMethod(this, _RecalcEngine_instances, buildSheetLike_fn).call(this, sheetName);
|
|
5940
|
+
__privateGet(this, _sheetLikes).set(sheetName, cached);
|
|
5941
|
+
}
|
|
5942
|
+
return cached;
|
|
5943
|
+
};
|
|
5944
|
+
buildSheetLike_fn = function(sheetName) {
|
|
5945
|
+
const model = __privateGet(this, _model);
|
|
4950
5946
|
return {
|
|
4951
5947
|
name: sheetName,
|
|
4952
5948
|
getCellValue: (ref) => __privateGet(this, _model).getCellValue(sheetName, ref),
|
|
5949
|
+
getCellValueAt: model.getCellValueAt ? (row2, column2) => model.getCellValueAt(sheetName, row2, column2) : void 0,
|
|
5950
|
+
// A getter, not a snapshot: the extent grows as formulas write results,
|
|
5951
|
+
// and a whole-column reference read later in the same pass must see it.
|
|
5952
|
+
get usedBounds() {
|
|
5953
|
+
return model.getUsedBounds?.(sheetName);
|
|
5954
|
+
},
|
|
4953
5955
|
isRowHidden: __privateGet(this, _model).isRowHidden ? (row2) => __privateGet(this, _model).isRowHidden(sheetName, row2) : void 0,
|
|
4954
5956
|
getCellFormula: __privateGet(this, _model).getCellFormula ? (ref) => __privateGet(this, _model).getCellFormula(sheetName, ref) : void 0,
|
|
4955
5957
|
getSpillRange: __privateGet(this, _model).getSpillRange ? (ref) => __privateGet(this, _model).getSpillRange(sheetName, ref) : void 0
|
|
4956
5958
|
};
|
|
4957
5959
|
};
|
|
4958
5960
|
buildWorkbookLike_fn = function() {
|
|
5961
|
+
if (__privateGet(this, _workbookLike)) return __privateGet(this, _workbookLike);
|
|
4959
5962
|
const self = this;
|
|
4960
|
-
return {
|
|
5963
|
+
return __privateSet(this, _workbookLike, {
|
|
4961
5964
|
get worksheets() {
|
|
4962
5965
|
return [...__privateGet(self, _sheetNames).values()].map((n) => {
|
|
4963
5966
|
var _a;
|
|
@@ -4969,7 +5972,7 @@ buildWorkbookLike_fn = function() {
|
|
|
4969
5972
|
return __privateMethod(_a = self, _RecalcEngine_instances, sheetLike_fn).call(_a, name);
|
|
4970
5973
|
},
|
|
4971
5974
|
getDefinedName: __privateGet(this, _model).getDefinedName ? (name, sheet) => __privateGet(this, _model).getDefinedName(name, sheet) : void 0
|
|
4972
|
-
};
|
|
5975
|
+
});
|
|
4973
5976
|
};
|
|
4974
5977
|
|
|
4975
5978
|
exports.ArrayValue = ArrayValue;
|
|
@@ -4987,12 +5990,14 @@ exports.FormulaLexer = FormulaLexer;
|
|
|
4987
5990
|
exports.FormulaNode = FormulaNode;
|
|
4988
5991
|
exports.FormulaParser = FormulaParser;
|
|
4989
5992
|
exports.FormulaValue = FormulaValue;
|
|
5993
|
+
exports.FullRangeReferenceNode = FullRangeReferenceNode;
|
|
4990
5994
|
exports.FunctionCallNode = FunctionCallNode;
|
|
4991
5995
|
exports.FunctionRegistry = FunctionRegistry;
|
|
4992
5996
|
exports.ImplicitIntersectionNode = ImplicitIntersectionNode;
|
|
4993
5997
|
exports.LruCache = LruCache;
|
|
4994
5998
|
exports.NamedRangeNode = NamedRangeNode;
|
|
4995
5999
|
exports.NumberNode = NumberNode;
|
|
6000
|
+
exports.RangeCache = RangeCache;
|
|
4996
6001
|
exports.RangeReferenceNode = RangeReferenceNode;
|
|
4997
6002
|
exports.RecalcEngine = RecalcEngine;
|
|
4998
6003
|
exports.SheetCellReferenceNode = SheetCellReferenceNode;
|
|
@@ -5004,10 +6009,12 @@ exports.UnaryOpNode = UnaryOpNode;
|
|
|
5004
6009
|
exports.UnaryOperator = UnaryOperator;
|
|
5005
6010
|
exports.clearAstCache = clearAstCache;
|
|
5006
6011
|
exports.createWorkbookRecalcModel = createWorkbookRecalcModel;
|
|
6012
|
+
exports.evaluateAst = evaluateAst;
|
|
5007
6013
|
exports.evaluateFormula = evaluateFormula;
|
|
5008
6014
|
exports.extractReferences = extractReferences;
|
|
5009
6015
|
exports.getAstCacheStats = getAstCacheStats;
|
|
5010
6016
|
exports.parseFormula = parseFormula;
|
|
6017
|
+
exports.parseFormulaUncached = parseFormulaUncached;
|
|
5011
6018
|
exports.refContains = refContains;
|
|
5012
6019
|
exports.registerAggregate = registerAggregate;
|
|
5013
6020
|
exports.registerArray = registerArray;
|