@devmm/puredocs-excel-formula 1.0.6 → 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
+ }
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
+ }
4284
4540
  }
4285
4541
  }
4286
- return FormulaValue.array(arr);
4542
+ const value2 = FormulaValue.array(arr);
4543
+ if (cache && rows2 > 0 && cols > 0) {
4544
+ cache.set(sheetKey, startRow, startCol, endRow, endCol, value2, rows2 * cols);
4545
+ }
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
@@ -4476,6 +4775,7 @@ function buildContext(sheet, opts) {
4476
4775
  evaluating
4477
4776
  );
4478
4777
  ctx.recursionGuard = opts.recursionGuard ?? { depth: 0, max: DEFAULT_LAMBDA_DEPTH };
4778
+ ctx.rangeCache = opts.rangeCache;
4479
4779
  const wb = opts.workbook;
4480
4780
  if (wb) {
4481
4781
  ctx.workbook = wb;
@@ -4527,7 +4827,8 @@ function evaluateAst(ast, sheet, options) {
4527
4827
  const ctx = buildContext(sheet, {
4528
4828
  workbook: options?.workbook,
4529
4829
  formulaRow: options?.formulaRow,
4530
- formulaCol: options?.formulaCol
4830
+ formulaCol: options?.formulaCol,
4831
+ rangeCache: options?.rangeCache
4531
4832
  });
4532
4833
  return ast.evaluate(ctx);
4533
4834
  } catch {
@@ -4548,6 +4849,149 @@ function getAstCacheStats() {
4548
4849
  function setAstCacheCapacity(capacity) {
4549
4850
  astCache.resize(capacity);
4550
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;
4551
4995
  function extractReferences(ast, formulaSheet, registry = FunctionRegistry.default, resolveName2) {
4552
4996
  const refs = [];
4553
4997
  let volatile = false;
@@ -4576,6 +5020,14 @@ function extractReferences(ast, formulaSheet, registry = FunctionRegistry.defaul
4576
5020
  refs.push(cell(node.sheetName, node.cellReference));
4577
5021
  } else if (node instanceof SheetRangeReferenceNode) {
4578
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
+ });
4579
5031
  } else if (node instanceof NamedRangeNode) {
4580
5032
  expandName(node.name);
4581
5033
  } else if (node instanceof SpillReferenceNode) {
@@ -4612,19 +5064,30 @@ function refContains(ref, sheet, row2, col) {
4612
5064
  }
4613
5065
 
4614
5066
  // src/dependency-index.ts
4615
- var TILE_ROWS = 64;
4616
- var TILE_COLS = 64;
5067
+ var TILE_ROWS2 = 64;
5068
+ var TILE_COLS2 = 64;
4617
5069
  var MAX_TILES_PER_REF = 32;
4618
- var _tiles, _wide, _placements, _DependencyIndex_instances, fileIn_fn;
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;
4619
5075
  var DependencyIndex = class {
4620
5076
  constructor() {
4621
5077
  __privateAdd(this, _DependencyIndex_instances);
4622
- /** `sheet|tileRow|tileCol` → formula keys whose precedents overlap that tile. */
4623
- __privateAdd(this, _tiles, /* @__PURE__ */ new Map());
4624
- /** sheet → formula keys with a precedent too broad to tile. */
4625
- __privateAdd(this, _wide, /* @__PURE__ */ new Map());
4626
- /** formula key the bucket keys it was filed under, for cheap removal. */
4627
- __privateAdd(this, _placements, /* @__PURE__ */ new Map());
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());
4628
5091
  }
4629
5092
  /** Registers (or re-registers) a formula's precedent rectangles. */
4630
5093
  add(formulaKey, refs) {
@@ -4632,78 +5095,117 @@ var DependencyIndex = class {
4632
5095
  if (refs.length === 0) return;
4633
5096
  const buckets = [];
4634
5097
  for (const ref of refs) {
4635
- const tileRow0 = ref.startRow / TILE_ROWS | 0;
4636
- const tileRow1 = ref.endRow / TILE_ROWS | 0;
4637
- const tileCol0 = ref.startCol / TILE_COLS | 0;
4638
- const tileCol1 = ref.endCol / TILE_COLS | 0;
4639
- const tileCount = (tileRow1 - tileRow0 + 1) * (tileCol1 - tileCol0 + 1);
4640
- if (tileCount > MAX_TILES_PER_REF) {
4641
- __privateMethod(this, _DependencyIndex_instances, fileIn_fn).call(this, __privateGet(this, _wide), ref.sheet, formulaKey, buckets, `w ${ref.sheet}`);
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
+ }
4642
5116
  continue;
4643
5117
  }
4644
- for (let tr = tileRow0; tr <= tileRow1; tr++) {
4645
- for (let tc = tileCol0; tc <= tileCol1; tc++) {
4646
- const key = `${ref.sheet} ${tr} ${tc}`;
4647
- __privateMethod(this, _DependencyIndex_instances, fileIn_fn).call(this, __privateGet(this, _tiles), key, formulaKey, buckets, `t ${key}`);
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
+ }
4648
5129
  }
5130
+ continue;
4649
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);
4650
5141
  }
4651
- __privateGet(this, _placements).set(formulaKey, buckets);
5142
+ __privateGet(this, _placements2).set(formulaKey, buckets);
4652
5143
  }
4653
5144
  /** Forgets a formula. */
4654
5145
  remove(formulaKey) {
4655
- const buckets = __privateGet(this, _placements).get(formulaKey);
5146
+ const buckets = __privateGet(this, _placements2).get(formulaKey);
4656
5147
  if (!buckets) return;
4657
5148
  for (const bucket of buckets) {
4658
- const wide = bucket.charCodeAt(0) === 119;
4659
- const store = wide ? __privateGet(this, _wide) : __privateGet(this, _tiles);
4660
- const key = bucket.slice(2);
4661
- const set = store.get(key);
5149
+ const set = __privateGet(this, _buckets).get(bucket);
4662
5150
  if (!set) continue;
4663
5151
  set.delete(formulaKey);
4664
- if (set.size === 0) store.delete(key);
5152
+ if (set.size === 0) __privateGet(this, _buckets).delete(bucket);
4665
5153
  }
4666
- __privateGet(this, _placements).delete(formulaKey);
5154
+ __privateGet(this, _placements2).delete(formulaKey);
4667
5155
  }
4668
5156
  /** Drops every registration. */
4669
5157
  clear() {
4670
- __privateGet(this, _tiles).clear();
4671
- __privateGet(this, _wide).clear();
4672
- __privateGet(this, _placements).clear();
5158
+ __privateGet(this, _buckets).clear();
5159
+ __privateGet(this, _placements2).clear();
4673
5160
  }
4674
5161
  /**
4675
5162
  * Calls `visit` with the key of each formula that *may* read the given cell.
4676
- * Callers still confirm with {@link refContains}: a tile is coarser than a
4677
- * rectangle, so this over-approximates by at most a tile's worth.
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.
4678
5167
  */
4679
5168
  candidates(sheet, row2, col, visit) {
4680
- const tile = __privateGet(this, _tiles).get(
4681
- `${sheet} ${row2 / TILE_ROWS | 0} ${col / TILE_COLS | 0}`
4682
- );
4683
- if (tile) for (const key of tile) visit(key);
4684
- const wide = __privateGet(this, _wide).get(sheet);
4685
- if (wide) for (const key of wide) visit(key);
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);
4686
5177
  }
4687
5178
  };
4688
- _tiles = new WeakMap();
4689
- _wide = new WeakMap();
4690
- _placements = new WeakMap();
5179
+ _buckets = new WeakMap();
5180
+ _placements2 = new WeakMap();
4691
5181
  _DependencyIndex_instances = new WeakSet();
4692
- fileIn_fn = function(store, key, formulaKey, buckets, bucketId) {
4693
- let set = store.get(key);
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);
4694
5188
  if (!set) {
4695
5189
  set = /* @__PURE__ */ new Set();
4696
- store.set(key, set);
5190
+ __privateGet(this, _buckets).set(bucket, set);
4697
5191
  }
4698
5192
  if (set.has(formulaKey)) return;
4699
5193
  set.add(formulaKey);
4700
- buckets.push(bucketId);
5194
+ buckets.push(bucket);
4701
5195
  };
4702
5196
 
4703
5197
  // src/recalc-engine.ts
4704
5198
  function createWorkbookRecalcModel(workbook) {
4705
5199
  return {
4706
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,
4707
5209
  setCellValue: (sheet, ref, value2) => {
4708
5210
  workbook.getWorksheet(sheet).getCell(ref).setComputedValue(value2);
4709
5211
  },
@@ -4732,7 +5234,7 @@ function parseKey(key) {
4732
5234
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(ref);
4733
5235
  return { sheet, ref, row: row2, col: column2 };
4734
5236
  }
4735
- var _model, _registry, _formulas, _sheetNames, _spills, _spillOwner, _dependents, _volatiles, _sheetLikes, _workbookLike, _RecalcEngine_instances, register_fn, unregister_fn, rememberSheet_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;
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;
4736
5238
  var RecalcEngine = class {
4737
5239
  constructor(model, registry = FunctionRegistry.default) {
4738
5240
  __privateAdd(this, _RecalcEngine_instances);
@@ -4758,6 +5260,14 @@ var RecalcEngine = class {
4758
5260
  /** Memoised per-sheet adapters — these are stateless w.r.t. the formula. */
4759
5261
  __privateAdd(this, _sheetLikes, /* @__PURE__ */ new Map());
4760
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());
4761
5271
  __privateSet(this, _model, model);
4762
5272
  __privateSet(this, _registry, registry);
4763
5273
  }
@@ -4795,8 +5305,63 @@ var RecalcEngine = class {
4795
5305
  volatile
4796
5306
  });
4797
5307
  __privateGet(this, _model).setCellFormula?.(sheet, r, text);
5308
+ __privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
4798
5309
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }], key);
4799
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 };
5364
+ }
4800
5365
  /**
4801
5366
  * Removes a formula from a cell and recalculates its dependents (which may now
4802
5367
  * read a plain input value instead). Returns the recomputed dependent cells.
@@ -4809,6 +5374,7 @@ var RecalcEngine = class {
4809
5374
  __privateMethod(this, _RecalcEngine_instances, unregister_fn).call(this, key);
4810
5375
  __privateGet(this, _model).clearCell?.(sheet, r);
4811
5376
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5377
+ __privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
4812
5378
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
4813
5379
  }
4814
5380
  /**
@@ -4819,8 +5385,8 @@ var RecalcEngine = class {
4819
5385
  __privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
4820
5386
  const s = normSheet(sheet);
4821
5387
  const r = normRef(ref);
4822
- __privateGet(this, _model).setCellValue(sheet, r, value2);
4823
5388
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5389
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, sheet, r, value2, row2, column2);
4824
5390
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
4825
5391
  }
4826
5392
  /**
@@ -4832,11 +5398,48 @@ var RecalcEngine = class {
4832
5398
  const s = normSheet(sheet);
4833
5399
  const r = normRef(ref);
4834
5400
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5401
+ __privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
4835
5402
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
4836
5403
  }
4837
- /** Recalculates every registered formula in dependency order. */
4838
- recalcAll() {
4839
- 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;
4840
5443
  }
4841
5444
  /** The precedents recorded for a formula cell (mainly for tests/inspection). */
4842
5445
  getPrecedents(sheet, ref) {
@@ -4886,7 +5489,23 @@ _dependents = new WeakMap();
4886
5489
  _volatiles = new WeakMap();
4887
5490
  _sheetLikes = new WeakMap();
4888
5491
  _workbookLike = new WeakMap();
5492
+ _rangeCache = new WeakMap();
4889
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
+ };
4890
5509
  /** Registers an entry in the map, the reverse index and the volatile set. */
4891
5510
  register_fn = function(entry) {
4892
5511
  __privateGet(this, _formulas).set(entry.key, entry);
@@ -4902,6 +5521,49 @@ unregister_fn = function(key) {
4902
5521
  rememberSheet_fn = function(name) {
4903
5522
  __privateGet(this, _sheetNames).set(normSheet(name), name);
4904
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
+ };
4905
5567
  applyShift_fn = function(shift) {
4906
5568
  const editedSheet = normSheet(shift.sheetName);
4907
5569
  const isRowShift = shift.dim === "row";
@@ -4943,6 +5605,7 @@ applyShift_fn = function(shift) {
4943
5605
  __privateGet(this, _volatiles).clear();
4944
5606
  for (const e of rekeyed) __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, e);
4945
5607
  __privateMethod(this, _RecalcEngine_instances, shiftSpillBookkeeping_fn).call(this, shift, editedSheet);
5608
+ __privateGet(this, _rangeCache).clear();
4946
5609
  const pending = /* @__PURE__ */ new Map();
4947
5610
  for (const k of needsRecalc) {
4948
5611
  const e = __privateGet(this, _formulas).get(k);
@@ -5013,7 +5676,7 @@ shiftSpillBookkeeping_fn = function(shift, editedSheet) {
5013
5676
  const shifted = shiftCellKey(m);
5014
5677
  if (!shifted) continue;
5015
5678
  const cell = parseKey(shifted);
5016
- __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
5679
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
5017
5680
  }
5018
5681
  continue;
5019
5682
  }
@@ -5154,7 +5817,7 @@ evaluatePass_fn = function(entries) {
5154
5817
  if (emitted.has(k)) continue;
5155
5818
  __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, e.key, changed);
5156
5819
  const value2 = FormulaValue.errorRef.toObject();
5157
- __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);
5158
5821
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
5159
5822
  results.push({ sheet: e.sheetName, ref: e.ref, value: value2, circular: true });
5160
5823
  }
@@ -5175,7 +5838,7 @@ writeResult_fn = function(e) {
5175
5838
  __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
5176
5839
  __privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
5177
5840
  const value2 = fv2.toObject();
5178
- __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);
5179
5842
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
5180
5843
  return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
5181
5844
  }
@@ -5196,7 +5859,7 @@ writeResult_fn = function(e) {
5196
5859
  __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
5197
5860
  __privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
5198
5861
  const value2 = FormulaValue.error(9 /* Spill */).toObject();
5199
- __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);
5200
5863
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
5201
5864
  return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
5202
5865
  }
@@ -5205,7 +5868,7 @@ writeResult_fn = function(e) {
5205
5868
  for (let dc = 0; dc < cols; dc++) {
5206
5869
  const r = e.row + dr, c = e.col + dc;
5207
5870
  const ref = puredocsExcel.cellRefFromRowCol(r, c);
5208
- __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);
5209
5872
  changed.push({ sheet: e.sheet, row: r, col: c });
5210
5873
  if (dr !== 0 || dc !== 0) {
5211
5874
  const cellKey2 = keyOf(e.sheet, ref);
@@ -5217,7 +5880,7 @@ writeResult_fn = function(e) {
5217
5880
  for (const oldKey of prevSpill) {
5218
5881
  if (newSpill.has(oldKey)) continue;
5219
5882
  const cell = parseKey(oldKey);
5220
- __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);
5221
5884
  __privateGet(this, _spillOwner).delete(oldKey);
5222
5885
  changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
5223
5886
  }
@@ -5244,7 +5907,7 @@ clearSpill_fn = function(anchorKey, changed) {
5244
5907
  if (!spill) return;
5245
5908
  for (const cellKey2 of spill) {
5246
5909
  const cell = parseKey(cellKey2);
5247
- __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);
5248
5911
  __privateGet(this, _spillOwner).delete(cellKey2);
5249
5912
  changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
5250
5913
  }
@@ -5261,7 +5924,8 @@ evaluateFormulaValue_fn = function(e) {
5261
5924
  return evaluateAst(e.ast, __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName), {
5262
5925
  workbook: __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this),
5263
5926
  formulaRow: e.row,
5264
- formulaCol: e.col
5927
+ formulaCol: e.col,
5928
+ rangeCache: __privateGet(this, _rangeCache)
5265
5929
  });
5266
5930
  };
5267
5931
  /**
@@ -5278,9 +5942,16 @@ sheetLike_fn = function(sheetName) {
5278
5942
  return cached;
5279
5943
  };
5280
5944
  buildSheetLike_fn = function(sheetName) {
5945
+ const model = __privateGet(this, _model);
5281
5946
  return {
5282
5947
  name: sheetName,
5283
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
+ },
5284
5955
  isRowHidden: __privateGet(this, _model).isRowHidden ? (row2) => __privateGet(this, _model).isRowHidden(sheetName, row2) : void 0,
5285
5956
  getCellFormula: __privateGet(this, _model).getCellFormula ? (ref) => __privateGet(this, _model).getCellFormula(sheetName, ref) : void 0,
5286
5957
  getSpillRange: __privateGet(this, _model).getSpillRange ? (ref) => __privateGet(this, _model).getSpillRange(sheetName, ref) : void 0
@@ -5319,12 +5990,14 @@ exports.FormulaLexer = FormulaLexer;
5319
5990
  exports.FormulaNode = FormulaNode;
5320
5991
  exports.FormulaParser = FormulaParser;
5321
5992
  exports.FormulaValue = FormulaValue;
5993
+ exports.FullRangeReferenceNode = FullRangeReferenceNode;
5322
5994
  exports.FunctionCallNode = FunctionCallNode;
5323
5995
  exports.FunctionRegistry = FunctionRegistry;
5324
5996
  exports.ImplicitIntersectionNode = ImplicitIntersectionNode;
5325
5997
  exports.LruCache = LruCache;
5326
5998
  exports.NamedRangeNode = NamedRangeNode;
5327
5999
  exports.NumberNode = NumberNode;
6000
+ exports.RangeCache = RangeCache;
5328
6001
  exports.RangeReferenceNode = RangeReferenceNode;
5329
6002
  exports.RecalcEngine = RecalcEngine;
5330
6003
  exports.SheetCellReferenceNode = SheetCellReferenceNode;