@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 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
- function computeIfsMask(a, c, startIdx, len2) {
1311
- const mask = new Uint8Array(len2).fill(1);
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 critText = crit.asText();
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 (!exports.FormulaHelper.matchesCriteria(valueAt(cr, i), critText)) mask[i] = 0;
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 m = computeIfsMask(a, c, 1, len2);
1331
- if (!m.ok) return m.error;
1558
+ const sel = resolveIfsSelection(a, c, 1, len2);
1559
+ if (!sel.ok) return sel.error;
1332
1560
  let total = 0;
1333
- for (let i = 0; i < len2; i++) {
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 m = computeIfsMask(a, c, 0, len2);
1346
- if (!m.ok) return m.error;
1572
+ const sel = resolveIfsSelection(a, c, 0, len2);
1573
+ if (!sel.ok) return sel.error;
1347
1574
  let n = 0;
1348
- for (let i = 0; i < len2; i++) if (m.mask[i]) n++;
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 m = computeIfsMask(a, c, 1, len2);
1357
- if (!m.ok) return m.error;
1585
+ const sel = resolveIfsSelection(a, c, 1, len2);
1586
+ if (!sel.ok) return sel.error;
1358
1587
  let total = 0, cnt = 0;
1359
- for (let i = 0; i < len2; i++) {
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 matchesCriteria(value2, criteria) {
4191
- criteria = criteria.trim();
4192
- const opMatch = /^(>=|<=|<>|>|<|=)(.*)$/.exec(criteria);
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 cmp = rhsNum !== null ? FormulaValue.compare(value2, FormulaValue.number(rhsNum)) : FormulaValue.compare(value2, FormulaValue.text(rhs));
4199
- switch (op) {
4200
- case ">":
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 (criteria.includes("*") || criteria.includes("?")) {
4215
- const pattern = wildcardToRegex(criteria);
4216
- return pattern.test(value2.asText());
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
- return value2.asText().toUpperCase() === criteria.toUpperCase();
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
- const rows2 = e.row - s.row + 1;
4278
- const cols = e.column - s.column + 1;
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
- for (let r = 0; r < rows2; r++) {
4281
- for (let c = 0; c < cols; c++) {
4282
- const ref = puredocsExcel.cellRefFromRowCol(s.row + r, s.column + c);
4283
- arr.set(r, c, this.getCellValue(ref));
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 FormulaValue.array(arr);
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
- const sheet = this.workbook.getWorksheet(sheetName);
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
- const sheet = this.workbook.getWorksheet(sheetName);
4303
- const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, __privateGet(this, _evaluating), __privateGet(this, _functions2));
4304
- ctx.workbook = this.workbook;
4305
- return ctx.getRangeValues(startRef, endRef);
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 lexer = new FormulaLexer(stripped);
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}\0${name.toUpperCase()}`;
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
- const ast = parseFormula(formula);
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, directDependents_fn, evaluatePass_fn, writeResult_fn, isOccupied_fn, clearSpill_fn, evaluateFormulaValue_fn, sheetLike_fn, buildWorkbookLike_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 = parseFormula(text);
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
- __privateGet(this, _formulas).set(keyOf(s, r), {
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
- return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }], keyOf(s, r));
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 existed = __privateGet(this, _formulas).delete(keyOf(s, r));
4694
- if (!existed) return [];
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
- /** Recalculates every registered formula in dependency order. */
4723
- recalcAll() {
4724
- return __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...__privateGet(this, _formulas).values()]).results;
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
- for (const e of __privateGet(this, _formulas).values()) {
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
- /** Formula entries (not yet evaluated) whose precedents include one of the cells. */
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
- while (queue.length > 0) {
4778
- const cell = queue.shift();
4779
- for (const dep of __privateMethod(this, _RecalcEngine_instances, directDependents_fn).call(this, cell)) {
4780
- const k = keyOf(dep.sheet, dep.ref);
4781
- if (out.has(k) || evaluated.has(k)) continue;
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
- /** Formula entries whose precedents include the given cell. */
4789
- directDependents_fn = function(cell) {
4790
- const out = [];
4791
- for (const e of __privateGet(this, _formulas).values()) {
4792
- if (e.refs.some((ref) => refContains(ref, cell.sheet, cell.row, cell.col))) out.push(e);
4793
- }
4794
- return out;
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(keyOf(e.sheet, e.ref), e);
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 e of entries) {
4811
- const depKey = keyOf(e.sheet, e.ref);
4812
- for (const [pk, pe] of inSet) {
4813
- if (pk === depKey) continue;
4814
- if (e.refs.some((ref) => refContains(ref, pe.sheet, pe.row, pe.col))) {
4815
- dependents.get(pk).push(depKey);
4816
- indegree.set(depKey, indegree.get(depKey) + 1);
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
- while (ready.length > 0) {
4824
- const k = ready.shift();
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
- indegree.set(d, indegree.get(d) - 1);
4828
- if (indegree.get(d) === 0) ready.push(d);
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, keyOf(e.sheet, e.ref), changed);
5818
+ __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, e.key, changed);
4843
5819
  const value2 = FormulaValue.errorRef.toObject();
4844
- __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
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 = keyOf(e.sheet, e.ref);
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
- __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
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
- __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
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
- __privateGet(this, _model).setCellValue(e.sheetName, ref, arr.get(dr, dc).toObject());
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
- __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
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
- __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
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
- const wb = __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this);
4942
- const ws = __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName);
4943
- return evaluateFormula(e.formula, ws, {
4944
- workbook: wb,
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;