@devmm/puredocs-excel-formula 1.0.6 → 1.0.8

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
@@ -448,8 +448,11 @@ var TokenType = /* @__PURE__ */ ((TokenType2) => {
448
448
  TokenType2[TokenType2["SpillReference"] = 24] = "SpillReference";
449
449
  TokenType2[TokenType2["Exclamation"] = 25] = "Exclamation";
450
450
  TokenType2[TokenType2["AtSign"] = 26] = "AtSign";
451
- TokenType2[TokenType2["ErrorLiteral"] = 27] = "ErrorLiteral";
452
- TokenType2[TokenType2["EOF"] = 28] = "EOF";
451
+ TokenType2[TokenType2["LeftBrace"] = 27] = "LeftBrace";
452
+ TokenType2[TokenType2["RightBrace"] = 28] = "RightBrace";
453
+ TokenType2[TokenType2["Semicolon"] = 29] = "Semicolon";
454
+ TokenType2[TokenType2["ErrorLiteral"] = 30] = "ErrorLiteral";
455
+ TokenType2[TokenType2["EOF"] = 31] = "EOF";
453
456
  return TokenType2;
454
457
  })(TokenType || {});
455
458
  function makeToken(type, value2, position) {
@@ -463,6 +466,8 @@ var FormulaException = class extends Error {
463
466
  this.name = "FormulaException";
464
467
  }
465
468
  };
469
+ var UNDERSCORE = 95;
470
+ var RESERVED_PREFIXES = ["_XLFN.", "_XLWS.", "_XLPM."];
466
471
  var KNOWN_ERRORS = /* @__PURE__ */ new Set([
467
472
  "#NULL!",
468
473
  "#DIV/0!",
@@ -474,7 +479,7 @@ var KNOWN_ERRORS = /* @__PURE__ */ new Set([
474
479
  "#CALC!",
475
480
  "#SPILL!"
476
481
  ]);
477
- var _formula, _pos, _FormulaLexer_instances, readNextToken_fn, readString_fn, readErrorLiteral_fn, readQuotedSheetRef_fn, readNumber_fn, readIdentifier_fn, readCellRefChars_fn, readOperator_fn, skipWhitespace_fn;
482
+ var _formula, _pos, _FormulaLexer_instances, readNextToken_fn, readString_fn, readErrorLiteral_fn, readQuotedSheetRef_fn, readNumber_fn, readIdentifier_fn, stripReservedPrefixes_fn, readCellRefChars_fn, readOperator_fn, skipWhitespace_fn;
478
483
  var FormulaLexer = class {
479
484
  constructor(formula) {
480
485
  __privateAdd(this, _FormulaLexer_instances);
@@ -492,7 +497,7 @@ var FormulaLexer = class {
492
497
  const token = __privateMethod(this, _FormulaLexer_instances, readNextToken_fn).call(this);
493
498
  if (token) tokens.push(token);
494
499
  }
495
- tokens.push(makeToken(28 /* EOF */, "", __privateGet(this, _pos)));
500
+ tokens.push(makeToken(31 /* EOF */, "", __privateGet(this, _pos)));
496
501
  return tokens;
497
502
  }
498
503
  };
@@ -546,7 +551,7 @@ readErrorLiteral_fn = function() {
546
551
  if (!KNOWN_ERRORS.has(upper2)) {
547
552
  throw new FormulaException(`Unknown error literal "${raw}" at position ${start}.`);
548
553
  }
549
- return makeToken(27 /* ErrorLiteral */, upper2, start);
554
+ return makeToken(30 /* ErrorLiteral */, upper2, start);
550
555
  };
551
556
  readQuotedSheetRef_fn = function() {
552
557
  const start = __privateWrapper(this, _pos)._++;
@@ -609,10 +614,8 @@ readIdentifier_fn = function() {
609
614
  } else break;
610
615
  }
611
616
  let upper2 = raw.toUpperCase();
612
- if (upper2.startsWith("_XLFN.")) {
613
- raw = raw.slice(6);
614
- upper2 = upper2.slice(6);
615
- }
617
+ raw = __privateMethod(this, _FormulaLexer_instances, stripReservedPrefixes_fn).call(this, raw, upper2);
618
+ if (raw.length !== upper2.length) upper2 = upper2.slice(upper2.length - raw.length);
616
619
  if (upper2 === "TRUE") return makeToken(2 /* Boolean */, "TRUE", start);
617
620
  if (upper2 === "FALSE") return makeToken(2 /* Boolean */, "FALSE", start);
618
621
  if (__privateGet(this, _formula)[__privateGet(this, _pos)] === "!") {
@@ -634,6 +637,30 @@ readIdentifier_fn = function() {
634
637
  }
635
638
  return makeToken(23 /* NamedRange */, raw, start);
636
639
  };
640
+ /**
641
+ * Drops Excel's storage-level name decorations. They carry no semantics — the
642
+ * writer stamps them so an older reader fails loudly instead of silently
643
+ * mis-evaluating — and they *stack*: `_xlfn._xlws.FILTER` wears two. Stripping
644
+ * one prefix once, as this used to, leaves `_XLWS.FILTER` unresolvable.
645
+ *
646
+ * Every identifier in every formula passes through here, so the common case is
647
+ * gated on a single char-code test: a name not starting with '_' returns
648
+ * untouched without so much as a comparison against the prefix table.
649
+ */
650
+ stripReservedPrefixes_fn = function(raw, upper2) {
651
+ if (raw.charCodeAt(0) !== UNDERSCORE) return raw;
652
+ let cut = 0;
653
+ outer: for (; ; ) {
654
+ for (const prefix of RESERVED_PREFIXES) {
655
+ if (upper2.startsWith(prefix, cut)) {
656
+ cut += prefix.length;
657
+ continue outer;
658
+ }
659
+ }
660
+ break;
661
+ }
662
+ return cut > 0 && cut < raw.length ? raw.slice(cut) : raw;
663
+ };
637
664
  readCellRefChars_fn = function() {
638
665
  let ref = "";
639
666
  while (__privateGet(this, _pos) < __privateGet(this, _formula).length) {
@@ -677,6 +704,14 @@ readOperator_fn = function() {
677
704
  return makeToken(26 /* AtSign */, "@", start);
678
705
  case "!":
679
706
  return makeToken(25 /* Exclamation */, "!", start);
707
+ case "{":
708
+ return makeToken(27 /* LeftBrace */, "{", start);
709
+ case "}":
710
+ return makeToken(28 /* RightBrace */, "}", start);
711
+ // Inside an array constant ';' separates rows. Excel always stores ',' as
712
+ // the argument separator regardless of locale, so ';' has no other role.
713
+ case ";":
714
+ return makeToken(29 /* Semicolon */, ";", start);
680
715
  case "<":
681
716
  if (__privateGet(this, _formula)[__privateGet(this, _pos)] === ">") {
682
717
  __privateWrapper(this, _pos)._++;
@@ -759,6 +794,19 @@ var ErrorNode = class extends FormulaNode {
759
794
  return this.errorValue;
760
795
  }
761
796
  };
797
+ var _value;
798
+ var ArrayLiteralNode = class extends FormulaNode {
799
+ constructor(array) {
800
+ super();
801
+ this.array = array;
802
+ __privateAdd(this, _value);
803
+ __privateSet(this, _value, FormulaValue.array(array));
804
+ }
805
+ evaluate() {
806
+ return __privateGet(this, _value);
807
+ }
808
+ };
809
+ _value = new WeakMap();
762
810
  var BlankNode = class extends FormulaNode {
763
811
  evaluate() {
764
812
  return FormulaValue.blank;
@@ -804,6 +852,19 @@ var SheetRangeReferenceNode = class extends FormulaNode {
804
852
  return ctx.getSheetRangeValues(this.sheetName, this.startRef, this.endRef);
805
853
  }
806
854
  };
855
+ var FullRangeReferenceNode = class extends FormulaNode {
856
+ constructor(startCol, endCol, startRow, endRow, sheetName) {
857
+ super();
858
+ this.startCol = startCol;
859
+ this.endCol = endCol;
860
+ this.startRow = startRow;
861
+ this.endRow = endRow;
862
+ this.sheetName = sheetName;
863
+ }
864
+ evaluate(ctx) {
865
+ 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);
866
+ }
867
+ };
807
868
  var NamedRangeNode = class extends FormulaNode {
808
869
  constructor(name) {
809
870
  super();
@@ -1023,7 +1084,7 @@ var CallNode = class extends FormulaNode {
1023
1084
  };
1024
1085
 
1025
1086
  // 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;
1087
+ var _tokens, _pos2, _FormulaParser_instances, current_get, advance_fn, expect_fn, parseComparison_fn, parseConcatenation_fn, parseAddSub_fn, parseMulDiv_fn, parseUnary_fn, parsePower_fn, parsePercent_fn, parsePostfix_fn, parsePrimary_fn, tryParseFullRange_fn, parseArrayLiteral_fn, parseArrayElement_fn, parseFunction_fn, parseArgList_fn;
1027
1088
  var FormulaParser = class {
1028
1089
  constructor(tokens) {
1029
1090
  __privateAdd(this, _FormulaParser_instances);
@@ -1034,7 +1095,7 @@ var FormulaParser = class {
1034
1095
  parse() {
1035
1096
  __privateSet(this, _pos2, 0);
1036
1097
  const node = __privateMethod(this, _FormulaParser_instances, parseComparison_fn).call(this);
1037
- if (__privateGet(this, _FormulaParser_instances, current_get).type !== 28 /* EOF */) {
1098
+ if (__privateGet(this, _FormulaParser_instances, current_get).type !== 31 /* EOF */) {
1038
1099
  throw new FormulaException(
1039
1100
  `Unexpected token '${__privateGet(this, _FormulaParser_instances, current_get).value}' at position ${__privateGet(this, _FormulaParser_instances, current_get).position}.`
1040
1101
  );
@@ -1046,7 +1107,7 @@ _tokens = new WeakMap();
1046
1107
  _pos2 = new WeakMap();
1047
1108
  _FormulaParser_instances = new WeakSet();
1048
1109
  current_get = function() {
1049
- return __privateGet(this, _tokens)[__privateGet(this, _pos2)] ?? { type: 28 /* EOF */, value: "", position: -1 };
1110
+ return __privateGet(this, _tokens)[__privateGet(this, _pos2)] ?? { type: 31 /* EOF */, value: "", position: -1 };
1050
1111
  };
1051
1112
  advance_fn = function() {
1052
1113
  const t = __privateGet(this, _FormulaParser_instances, current_get);
@@ -1147,6 +1208,10 @@ parsePrimary_fn = function() {
1147
1208
  switch (token.type) {
1148
1209
  case 0 /* Number */: {
1149
1210
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1211
+ if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
1212
+ const full = __privateMethod(this, _FormulaParser_instances, tryParseFullRange_fn).call(this, token.value);
1213
+ if (full) return full;
1214
+ }
1150
1215
  const n = parseFloat(token.value);
1151
1216
  if (isNaN(n)) throw new FormulaException(`Invalid number: ${token.value}`);
1152
1217
  return new NumberNode(n);
@@ -1157,7 +1222,7 @@ parsePrimary_fn = function() {
1157
1222
  case 2 /* Boolean */:
1158
1223
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1159
1224
  return new BooleanNode(token.value === "TRUE");
1160
- case 27 /* ErrorLiteral */:
1225
+ case 30 /* ErrorLiteral */:
1161
1226
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1162
1227
  return new ErrorNode(FormulaValue.error(FormulaValue.errorFromString(token.value)));
1163
1228
  case 3 /* CellReference */: {
@@ -1175,20 +1240,29 @@ parsePrimary_fn = function() {
1175
1240
  const sheetName = parts[0];
1176
1241
  const cellRef = parts[1] ?? "";
1177
1242
  if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
1243
+ const full = __privateMethod(this, _FormulaParser_instances, tryParseFullRange_fn).call(this, cellRef, sheetName);
1244
+ if (full) return full;
1178
1245
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1179
1246
  const end = __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 3 /* CellReference */);
1180
1247
  return new SheetRangeReferenceNode(sheetName, cellRef, end.value);
1181
1248
  }
1182
1249
  return new SheetCellReferenceNode(sheetName, cellRef);
1183
1250
  }
1184
- case 23 /* NamedRange */:
1251
+ case 23 /* NamedRange */: {
1185
1252
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1253
+ if (__privateGet(this, _FormulaParser_instances, current_get).type === 4 /* Colon */) {
1254
+ const full = __privateMethod(this, _FormulaParser_instances, tryParseFullRange_fn).call(this, token.value);
1255
+ if (full) return full;
1256
+ }
1186
1257
  return new NamedRangeNode(token.value);
1258
+ }
1187
1259
  case 24 /* SpillReference */:
1188
1260
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1189
1261
  return new SpillReferenceNode(token.value);
1190
1262
  case 5 /* Function */:
1191
1263
  return __privateMethod(this, _FormulaParser_instances, parseFunction_fn).call(this);
1264
+ case 27 /* LeftBrace */:
1265
+ return __privateMethod(this, _FormulaParser_instances, parseArrayLiteral_fn).call(this);
1192
1266
  case 6 /* LeftParen */: {
1193
1267
  __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1194
1268
  const expr = __privateMethod(this, _FormulaParser_instances, parseComparison_fn).call(this);
@@ -1201,6 +1275,101 @@ parsePrimary_fn = function() {
1201
1275
  );
1202
1276
  }
1203
1277
  };
1278
+ /**
1279
+ * Completes a whole-column or whole-row reference whose first half has already
1280
+ * been consumed and whose ':' is the current token. Returns null — consuming
1281
+ * nothing — when the two halves are not a matching pair of columns or rows, so
1282
+ * the caller can fall back to its normal interpretation.
1283
+ *
1284
+ * Both halves must be the same kind: `A:A` and `1:5` are ranges, `A:1` is not
1285
+ * a reference at all in Excel and must stay an error rather than becoming a
1286
+ * silently different region.
1287
+ */
1288
+ tryParseFullRange_fn = function(startText, sheetName) {
1289
+ const endToken = __privateGet(this, _tokens)[__privateGet(this, _pos2) + 1];
1290
+ if (!endToken) return null;
1291
+ const start = classifyBound(startText);
1292
+ const end = classifyBound(endToken.value);
1293
+ if (!start || !end || start.kind !== end.kind) return null;
1294
+ __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1295
+ __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1296
+ const lo = Math.min(start.index, end.index);
1297
+ const hi = Math.max(start.index, end.index);
1298
+ return start.kind === "col" ? new FullRangeReferenceNode(lo, hi, null, null, sheetName) : new FullRangeReferenceNode(null, null, lo, hi, sheetName);
1299
+ };
1300
+ /**
1301
+ * Parses an array constant: `{1,2;3,4}` — ',' separates columns, ';' rows.
1302
+ *
1303
+ * Excel permits only literals inside one (no references, no calls, not even
1304
+ * arithmetic beyond a leading sign), so the value is fully determined here and
1305
+ * is materialised once into the node instead of being rebuilt on every eval.
1306
+ *
1307
+ * A ragged constant is rejected rather than padded: Excel refuses to accept
1308
+ * one at entry, so a file cannot legitimately contain it, and silently filling
1309
+ * the gaps would invent data the author never wrote.
1310
+ */
1311
+ parseArrayLiteral_fn = function() {
1312
+ const open = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1313
+ const rows2 = [];
1314
+ let row2 = [];
1315
+ for (; ; ) {
1316
+ row2.push(__privateMethod(this, _FormulaParser_instances, parseArrayElement_fn).call(this));
1317
+ if (__privateGet(this, _FormulaParser_instances, current_get).type === 8 /* Comma */) {
1318
+ __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1319
+ continue;
1320
+ }
1321
+ if (__privateGet(this, _FormulaParser_instances, current_get).type === 29 /* Semicolon */) {
1322
+ __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1323
+ rows2.push(row2);
1324
+ row2 = [];
1325
+ continue;
1326
+ }
1327
+ break;
1328
+ }
1329
+ rows2.push(row2);
1330
+ __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 28 /* RightBrace */);
1331
+ const cols = rows2[0].length;
1332
+ if (rows2.some((r) => r.length !== cols)) {
1333
+ throw new FormulaException(
1334
+ `Array constant at position ${open.position} has rows of differing lengths.`
1335
+ );
1336
+ }
1337
+ const array = new ArrayValue(rows2.length, cols);
1338
+ for (let r = 0; r < rows2.length; r++) {
1339
+ for (let c = 0; c < cols; c++) array.set(r, c, rows2[r][c]);
1340
+ }
1341
+ return new ArrayLiteralNode(array);
1342
+ };
1343
+ /** One element of an array constant: a literal, optionally signed. */
1344
+ parseArrayElement_fn = function() {
1345
+ let negate = false;
1346
+ let signed = false;
1347
+ while (__privateGet(this, _FormulaParser_instances, current_get).type === 10 /* Minus */ || __privateGet(this, _FormulaParser_instances, current_get).type === 9 /* Plus */) {
1348
+ if (__privateGet(this, _FormulaParser_instances, current_get).type === 10 /* Minus */) negate = !negate;
1349
+ signed = true;
1350
+ __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1351
+ }
1352
+ const token = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1353
+ switch (token.type) {
1354
+ case 0 /* Number */: {
1355
+ const n = parseFloat(token.value);
1356
+ if (isNaN(n)) throw new FormulaException(`Invalid number: ${token.value}`);
1357
+ return FormulaValue.number(negate ? -n : n);
1358
+ }
1359
+ case 1 /* String */:
1360
+ if (signed) break;
1361
+ return FormulaValue.text(token.value);
1362
+ case 2 /* Boolean */:
1363
+ if (signed) break;
1364
+ return FormulaValue.boolean(token.value === "TRUE");
1365
+ case 30 /* ErrorLiteral */:
1366
+ if (signed) break;
1367
+ return FormulaValue.error(FormulaValue.errorFromString(token.value));
1368
+ }
1369
+ throw new FormulaException(
1370
+ `Array constants accept only literal values; got '${token.value}' at position ${token.position}.`
1371
+ );
1372
+ };
1204
1373
  parseFunction_fn = function() {
1205
1374
  const nameToken = __privateMethod(this, _FormulaParser_instances, advance_fn).call(this);
1206
1375
  __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 6 /* LeftParen */);
@@ -1227,6 +1396,18 @@ parseArgList_fn = function() {
1227
1396
  __privateMethod(this, _FormulaParser_instances, expect_fn).call(this, 7 /* RightParen */);
1228
1397
  return args;
1229
1398
  };
1399
+ function classifyBound(text) {
1400
+ const bare = text.replace(/\$/g, "");
1401
+ if (/^\d+$/.test(bare)) {
1402
+ const row2 = parseInt(bare, 10);
1403
+ return row2 >= 1 && row2 <= puredocsExcel.EXCEL_MAX_ROWS ? { kind: "row", index: row2 } : null;
1404
+ }
1405
+ if (/^[A-Za-z]{1,3}$/.test(bare)) {
1406
+ const col = puredocsExcel.columnNumber(bare.toUpperCase());
1407
+ return col >= 1 && col <= puredocsExcel.EXCEL_MAX_COLUMNS ? { kind: "col", index: col } : null;
1408
+ }
1409
+ return null;
1410
+ }
1230
1411
  function comparisonOp(type) {
1231
1412
  switch (type) {
1232
1413
  case 16 /* Equal */:
@@ -1246,6 +1427,137 @@ function comparisonOp(type) {
1246
1427
  }
1247
1428
  }
1248
1429
 
1430
+ // src/criteria-index.ts
1431
+ var EPSILON = 1e-10;
1432
+ var indexes = /* @__PURE__ */ new WeakMap();
1433
+ function build(arr) {
1434
+ const byText = /* @__PURE__ */ new Map();
1435
+ const byNumber = /* @__PURE__ */ new Map();
1436
+ const classes = /* @__PURE__ */ new Map();
1437
+ const addToClass = (kind, v, i2) => {
1438
+ const group = classes.get(kind);
1439
+ if (group) group.positions.push(i2);
1440
+ else classes.set(kind, { positions: [i2], representative: v });
1441
+ };
1442
+ let i = 0;
1443
+ for (const v of arr.values()) {
1444
+ const textKey = v.asText().toUpperCase();
1445
+ const atText = byText.get(textKey);
1446
+ if (atText) atText.push(i);
1447
+ else byText.set(textKey, [i]);
1448
+ if (v.isNumber) {
1449
+ const n = v.numberValue;
1450
+ const atNum = byNumber.get(n);
1451
+ if (atNum) atNum.push(i);
1452
+ else byNumber.set(n, [i]);
1453
+ } else {
1454
+ addToClass(v.kind, v, i);
1455
+ }
1456
+ i++;
1457
+ }
1458
+ const sortedNumbers = Float64Array.from(byNumber.keys()).sort();
1459
+ const cumulative = new Int32Array(sortedNumbers.length + 1);
1460
+ for (let k = 0; k < sortedNumbers.length; k++) {
1461
+ cumulative[k + 1] = cumulative[k] + byNumber.get(sortedNumbers[k]).length;
1462
+ }
1463
+ return { byText, byNumber, sortedNumbers, cumulative, classes: [...classes.values()] };
1464
+ }
1465
+ function indexOf(arr) {
1466
+ let idx = indexes.get(arr);
1467
+ if (!idx) {
1468
+ idx = build(arr);
1469
+ indexes.set(arr, idx);
1470
+ }
1471
+ return idx;
1472
+ }
1473
+ function lowerBound(sorted, target) {
1474
+ let lo = 0, hi = sorted.length;
1475
+ while (lo < hi) {
1476
+ const mid2 = lo + hi >> 1;
1477
+ if (sorted[mid2] < target) lo = mid2 + 1;
1478
+ else hi = mid2;
1479
+ }
1480
+ return lo;
1481
+ }
1482
+ function upperBound(sorted, target) {
1483
+ let lo = 0, hi = sorted.length;
1484
+ while (lo < hi) {
1485
+ const mid2 = lo + hi >> 1;
1486
+ if (sorted[mid2] <= target) lo = mid2 + 1;
1487
+ else hi = mid2;
1488
+ }
1489
+ return lo;
1490
+ }
1491
+ function matchingPositions(criteriaRange, criteria, minSize) {
1492
+ if (!criteriaRange.isArray) return null;
1493
+ const arr = criteriaRange.arrayVal;
1494
+ if (arr.length < minSize) return null;
1495
+ if (criteria.kind === "equalsText") {
1496
+ return indexOf(arr).byText.get(criteria.text) ?? EMPTY;
1497
+ }
1498
+ if (criteria.kind === "equalsNumber") {
1499
+ const target = criteria.number;
1500
+ const idx = indexOf(arr);
1501
+ const exact2 = idx.byNumber.get(target);
1502
+ let near;
1503
+ const { sortedNumbers } = idx;
1504
+ for (let k = lowerBound(sortedNumbers, target - EPSILON); k < sortedNumbers.length; k++) {
1505
+ const value2 = sortedNumbers[k];
1506
+ if (value2 > target + EPSILON) break;
1507
+ if (value2 === target) continue;
1508
+ if (!criteria.test(FormulaValue.number(value2))) continue;
1509
+ (near ?? (near = [])).push(...idx.byNumber.get(value2));
1510
+ }
1511
+ const blanks = criteria.test(FormulaValue.blank) ? blankPositions(idx) : void 0;
1512
+ if (!near && !blanks?.length) return exact2 ?? EMPTY;
1513
+ const merged = [...exact2 ?? EMPTY, ...near ?? EMPTY, ...blanks ?? EMPTY];
1514
+ merged.sort((a, b) => a - b);
1515
+ return merged;
1516
+ }
1517
+ if (criteria.kind === "compareNumber") {
1518
+ return comparePositions(indexOf(arr), criteria, arr.length);
1519
+ }
1520
+ return null;
1521
+ }
1522
+ function comparePositions(idx, criteria, length) {
1523
+ const target = criteria.number;
1524
+ const { sortedNumbers } = idx;
1525
+ let from = 0;
1526
+ let to = sortedNumbers.length;
1527
+ switch (criteria.op) {
1528
+ case ">":
1529
+ from = upperBound(sortedNumbers, target);
1530
+ break;
1531
+ case ">=":
1532
+ from = lowerBound(sortedNumbers, target);
1533
+ break;
1534
+ case "<":
1535
+ to = lowerBound(sortedNumbers, target);
1536
+ break;
1537
+ default:
1538
+ to = upperBound(sortedNumbers, target);
1539
+ break;
1540
+ }
1541
+ const budget = length >> 1;
1542
+ const matchedClasses = idx.classes.filter((g) => criteria.test(g.representative));
1543
+ let total = idx.cumulative[to] - idx.cumulative[from];
1544
+ for (const group of matchedClasses) total += group.positions.length;
1545
+ if (total > budget) return null;
1546
+ const positions = [];
1547
+ for (let k = from; k < to; k++) {
1548
+ for (const i of idx.byNumber.get(sortedNumbers[k])) positions.push(i);
1549
+ }
1550
+ for (const group of matchedClasses) {
1551
+ for (const i of group.positions) positions.push(i);
1552
+ }
1553
+ positions.sort((a, b) => a - b);
1554
+ return positions;
1555
+ }
1556
+ function blankPositions(idx) {
1557
+ return idx.classes.find((g) => g.representative.isBlank)?.positions;
1558
+ }
1559
+ var EMPTY = [];
1560
+
1249
1561
  // src/functions/math-functions.ts
1250
1562
  function registerMath(r) {
1251
1563
  r.register("SUM", sum, 1);
@@ -1307,34 +1619,72 @@ function valueAt(v, i) {
1307
1619
  function lengthOf(v) {
1308
1620
  return v.isArray ? v.arrayVal.length : 1;
1309
1621
  }
1310
- function computeIfsMask(a, c, startIdx, len2) {
1311
- const mask = new Uint8Array(len2).fill(1);
1622
+ var INDEX_MIN_RANGE = 32;
1623
+ function resolveIfsSelection(a, c, startIdx, len2) {
1624
+ const ranges = [];
1625
+ const criteria = [];
1626
+ let seed;
1627
+ let seedPair = -1;
1312
1628
  for (let p = startIdx; p + 1 < a.length; p += 2) {
1313
1629
  const cr = a[p].evaluate(c);
1314
1630
  if (cr.isError) return { ok: false, error: cr };
1315
1631
  const crit = a[p + 1].evaluate(c);
1316
1632
  if (crit.isError) return { ok: false, error: crit };
1317
- const critText = crit.asText();
1633
+ const compiled = exports.FormulaHelper.compileCriteria(crit.asText());
1634
+ ranges.push(cr);
1635
+ criteria.push(compiled);
1636
+ if (seed === void 0) {
1637
+ const positions = matchingPositions(cr, compiled, INDEX_MIN_RANGE);
1638
+ if (positions) {
1639
+ seed = positions;
1640
+ seedPair = ranges.length - 1;
1641
+ }
1642
+ }
1643
+ }
1644
+ if (seed !== void 0) {
1645
+ let candidates = [];
1646
+ for (const i of seed) if (i < len2) candidates.push(i);
1647
+ for (let k = 0; k < ranges.length && candidates.length > 0; k++) {
1648
+ if (k === seedPair) continue;
1649
+ const range = ranges[k];
1650
+ const test = criteria[k].test;
1651
+ const next = [];
1652
+ for (const i of candidates) if (test(valueAt(range, i))) next.push(i);
1653
+ candidates = next;
1654
+ }
1655
+ return { ok: true, positions: candidates };
1656
+ }
1657
+ const mask = new Uint8Array(len2).fill(1);
1658
+ for (let k = 0; k < ranges.length; k++) {
1659
+ const range = ranges[k];
1660
+ const test = criteria[k].test;
1318
1661
  for (let i = 0; i < len2; i++) {
1319
1662
  if (!mask[i]) continue;
1320
- if (!exports.FormulaHelper.matchesCriteria(valueAt(cr, i), critText)) mask[i] = 0;
1663
+ if (!test(valueAt(range, i))) mask[i] = 0;
1321
1664
  }
1322
1665
  }
1323
1666
  return { ok: true, mask };
1324
1667
  }
1668
+ function forEachSelected(sel, len2, visit) {
1669
+ if ("positions" in sel) {
1670
+ for (const i of sel.positions) visit(i);
1671
+ return;
1672
+ }
1673
+ const { mask } = sel;
1674
+ for (let i = 0; i < len2; i++) if (mask[i]) visit(i);
1675
+ }
1325
1676
  function sumIfs(a, c) {
1326
1677
  if (a.length < 3 || a.length % 2 === 0) return FormulaValue.errorValue;
1327
1678
  const sumRange = a[0].evaluate(c);
1328
1679
  if (sumRange.isError) return sumRange;
1329
1680
  const len2 = lengthOf(sumRange);
1330
- const m = computeIfsMask(a, c, 1, len2);
1331
- if (!m.ok) return m.error;
1681
+ const sel = resolveIfsSelection(a, c, 1, len2);
1682
+ if (!sel.ok) return sel.error;
1332
1683
  let total = 0;
1333
- for (let i = 0; i < len2; i++) {
1334
- if (!m.mask[i]) continue;
1684
+ forEachSelected(sel, len2, (i) => {
1335
1685
  const r = valueAt(sumRange, i).tryAsDouble();
1336
1686
  if (r.ok) total += r.value;
1337
- }
1687
+ });
1338
1688
  return FormulaValue.number(total);
1339
1689
  }
1340
1690
  function countIfs(a, c) {
@@ -1342,10 +1692,12 @@ function countIfs(a, c) {
1342
1692
  const first = a[0].evaluate(c);
1343
1693
  if (first.isError) return first;
1344
1694
  const len2 = lengthOf(first);
1345
- const m = computeIfsMask(a, c, 0, len2);
1346
- if (!m.ok) return m.error;
1695
+ const sel = resolveIfsSelection(a, c, 0, len2);
1696
+ if (!sel.ok) return sel.error;
1347
1697
  let n = 0;
1348
- for (let i = 0; i < len2; i++) if (m.mask[i]) n++;
1698
+ forEachSelected(sel, len2, () => {
1699
+ n++;
1700
+ });
1349
1701
  return FormulaValue.number(n);
1350
1702
  }
1351
1703
  function averageIfs(a, c) {
@@ -1353,17 +1705,16 @@ function averageIfs(a, c) {
1353
1705
  const avgRange = a[0].evaluate(c);
1354
1706
  if (avgRange.isError) return avgRange;
1355
1707
  const len2 = lengthOf(avgRange);
1356
- const m = computeIfsMask(a, c, 1, len2);
1357
- if (!m.ok) return m.error;
1708
+ const sel = resolveIfsSelection(a, c, 1, len2);
1709
+ if (!sel.ok) return sel.error;
1358
1710
  let total = 0, cnt = 0;
1359
- for (let i = 0; i < len2; i++) {
1360
- if (!m.mask[i]) continue;
1711
+ forEachSelected(sel, len2, (i) => {
1361
1712
  const r = valueAt(avgRange, i).tryAsDouble();
1362
1713
  if (r.ok) {
1363
1714
  total += r.value;
1364
1715
  cnt++;
1365
1716
  }
1366
- }
1717
+ });
1367
1718
  return cnt === 0 ? FormulaValue.errorDiv0 : FormulaValue.number(total / cnt);
1368
1719
  }
1369
1720
  function sumSq(a, c) {
@@ -3835,22 +4186,46 @@ function unique(a, c) {
3835
4186
  const out = fromRows(result);
3836
4187
  return byCol2 ? FormulaValue.array(out.arrayVal.transpose()) : out;
3837
4188
  }
4189
+ var numVector = (a, c, i, dflt) => {
4190
+ if (i >= a.length) return { ok: true, v: [dflt] };
4191
+ const val = a[i].evaluate(c);
4192
+ if (val.isError) return { ok: false, e: val };
4193
+ const out = [];
4194
+ for (const item of val.isArray ? val.arrayVal.values() : [val]) {
4195
+ if (item.isBlank) continue;
4196
+ const d = item.tryAsDouble();
4197
+ if (!d.ok) return { ok: false, e: FormulaValue.errorValue };
4198
+ out.push(d.value);
4199
+ }
4200
+ return out.length > 0 ? { ok: true, v: out } : { ok: true, v: [dflt] };
4201
+ };
3838
4202
  function sort(a, c) {
3839
4203
  const v = a[0].evaluate(c);
3840
4204
  if (v.isError) return v;
3841
- const sortIndex = num4(a, c, 1, 1);
3842
- if (!sortIndex.ok) return sortIndex.e;
3843
- const sortOrder = num4(a, c, 2, 1);
3844
- if (!sortOrder.ok) return sortOrder.e;
4205
+ const indices = numVector(a, c, 1, 1);
4206
+ if (!indices.ok) return indices.e;
4207
+ const orders = numVector(a, c, 2, 1);
4208
+ if (!orders.ok) return orders.e;
3845
4209
  const byCol2 = a.length > 3 ? a[3].evaluate(c).coerceToBool().booleanValue : false;
4210
+ if (orders.v.length !== 1 && orders.v.length !== indices.v.length) {
4211
+ return FormulaValue.errorValue;
4212
+ }
3846
4213
  const base = byCol2 ? asArray2(v).transpose() : asArray2(v);
3847
4214
  const rows2 = toRows(base);
3848
- const idx = Math.trunc(sortIndex.n) - 1;
3849
- const dir = sortOrder.n < 0 ? -1 : 1;
3850
- if (rows2.length > 0 && (idx < 0 || idx >= rows2[0].length)) return FormulaValue.errorValue;
4215
+ const keys = indices.v.map((n, i) => ({
4216
+ col: Math.trunc(n) - 1,
4217
+ dir: (orders.v.length === 1 ? orders.v[0] : orders.v[i]) < 0 ? -1 : 1
4218
+ }));
4219
+ const width = rows2.length > 0 ? rows2[0].length : 0;
4220
+ if (rows2.length > 0 && keys.some((k) => k.col < 0 || k.col >= width)) {
4221
+ return FormulaValue.errorValue;
4222
+ }
3851
4223
  const sorted = rows2.map((r, i) => ({ r, i })).sort((x, y) => {
3852
- const cmp = FormulaValue.compare(x.r[idx], y.r[idx]) * dir;
3853
- return cmp !== 0 ? cmp : x.i - y.i;
4224
+ for (const k of keys) {
4225
+ const cmp = FormulaValue.compare(x.r[k.col], y.r[k.col]) * k.dir;
4226
+ if (cmp !== 0) return cmp;
4227
+ }
4228
+ return x.i - y.i;
3854
4229
  }).map((o) => o.r);
3855
4230
  const out = fromRows(sorted);
3856
4231
  return byCol2 ? FormulaValue.array(out.arrayVal.transpose()) : out;
@@ -4187,39 +4562,43 @@ exports.FormulaHelper = void 0;
4187
4562
  return result;
4188
4563
  }
4189
4564
  FormulaHelper2.flattenArgs = flattenArgs;
4190
- function matchesCriteria(value2, criteria) {
4191
- criteria = criteria.trim();
4192
- const opMatch = /^(>=|<=|<>|>|<|=)(.*)$/.exec(criteria);
4565
+ function compileCriteria(criteria) {
4566
+ const trimmed = criteria.trim();
4567
+ const opMatch = /^(>=|<=|<>|>|<|=)(.*)$/.exec(trimmed);
4193
4568
  if (opMatch) {
4194
4569
  const op = opMatch[1];
4195
4570
  const rhs = opMatch[2];
4196
4571
  const rhsVal = parseFloat(rhs);
4197
4572
  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));
4573
+ const rhsValue = rhsNum !== null ? FormulaValue.number(rhsNum) : FormulaValue.text(rhs);
4574
+ if (op === "=") {
4575
+ return rhsNum !== null ? numberEquality(rhsNum) : textEquality(rhs);
4212
4576
  }
4577
+ if (op === "<>") {
4578
+ return { kind: "other", test: (v) => !FormulaValue.areEqual(v, rhsValue) };
4579
+ }
4580
+ const wanted = op === ">" ? (c) => c > 0 : op === ">=" ? (c) => c >= 0 : op === "<" ? (c) => c < 0 : (c) => c <= 0;
4581
+ const test = (v) => wanted(FormulaValue.compare(v, rhsValue));
4582
+ return rhsNum !== null ? { kind: "compareNumber", number: rhsNum, op, test } : { kind: "other", test };
4213
4583
  }
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));
4584
+ if (trimmed.includes("*") || trimmed.includes("?")) {
4585
+ const pattern = wildcardToRegex(trimmed);
4586
+ return { kind: "other", test: (v) => pattern.test(v.asText()) };
4221
4587
  }
4222
- return value2.asText().toUpperCase() === criteria.toUpperCase();
4588
+ const num5 = parseFloat(trimmed);
4589
+ return isNaN(num5) ? textEquality(trimmed) : numberEquality(num5);
4590
+ }
4591
+ FormulaHelper2.compileCriteria = compileCriteria;
4592
+ function numberEquality(n) {
4593
+ const target = FormulaValue.number(n);
4594
+ return { kind: "equalsNumber", number: n, test: (v) => FormulaValue.areEqual(v, target) };
4595
+ }
4596
+ function textEquality(s) {
4597
+ const upper2 = s.toUpperCase();
4598
+ return { kind: "equalsText", text: upper2, test: (v) => v.asText().toUpperCase() === upper2 };
4599
+ }
4600
+ function matchesCriteria(value2, criteria) {
4601
+ return compileCriteria(criteria).test(value2);
4223
4602
  }
4224
4603
  FormulaHelper2.matchesCriteria = matchesCriteria;
4225
4604
  function wildcardToRegex(pattern) {
@@ -4229,7 +4608,7 @@ exports.FormulaHelper = void 0;
4229
4608
  })(exports.FormulaHelper || (exports.FormulaHelper = {}));
4230
4609
 
4231
4610
  // src/formula-context.ts
4232
- var _sheet, _functions2, _evaluating, _scopes, _FormulaContext_instances, lookupScope_fn;
4611
+ var _sheet, _functions2, _evaluating, _scopes, _FormulaContext_instances, lookupScope_fn, forSheet_fn;
4233
4612
  var _FormulaContext = class _FormulaContext {
4234
4613
  constructor(sheet, formulaRow = 0, formulaCol = 0, evaluating, registry) {
4235
4614
  __privateAdd(this, _FormulaContext_instances);
@@ -4274,24 +4653,70 @@ var _FormulaContext = class _FormulaContext {
4274
4653
  getRangeValues(startRef, endRef) {
4275
4654
  const s = puredocsExcel.parseCellRef(startRef);
4276
4655
  const e = puredocsExcel.parseCellRef(endRef);
4277
- const rows2 = e.row - s.row + 1;
4278
- const cols = e.column - s.column + 1;
4656
+ return this.getRangeValuesByIndex(s.row, s.column, e.row, e.column);
4657
+ }
4658
+ /**
4659
+ * Resolves a rectangle already known in coordinates. Split out from
4660
+ * {@link getRangeValues} because whole-column references and the range memo
4661
+ * both work in numbers, and formatting them back into `A1` strings only to
4662
+ * re-parse them is measurable at range scale.
4663
+ */
4664
+ getRangeValuesByIndex(startRow, startCol, endRow, endCol) {
4665
+ const rows2 = endRow - startRow + 1;
4666
+ const cols = endCol - startCol + 1;
4667
+ const cache = rows2 * cols > 1 ? this.rangeCache : void 0;
4668
+ const sheetKey = cache ? __privateGet(this, _sheet).name.toUpperCase() : "";
4669
+ if (cache) {
4670
+ const hit = cache.get(sheetKey, startRow, startCol, endRow, endCol);
4671
+ if (hit !== void 0) return hit;
4672
+ }
4279
4673
  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));
4674
+ const at = __privateGet(this, _sheet).getCellValueAt;
4675
+ if (at) {
4676
+ for (let r = 0; r < rows2; r++) {
4677
+ for (let c = 0; c < cols; c++) {
4678
+ arr.set(r, c, FormulaValue.fromObjectSync(at.call(__privateGet(this, _sheet), startRow + r, startCol + c)));
4679
+ }
4680
+ }
4681
+ } else {
4682
+ for (let r = 0; r < rows2; r++) {
4683
+ for (let c = 0; c < cols; c++) {
4684
+ const ref = puredocsExcel.cellRefFromRowCol(startRow + r, startCol + c);
4685
+ arr.set(r, c, this.getCellValue(ref));
4686
+ }
4284
4687
  }
4285
4688
  }
4286
- return FormulaValue.array(arr);
4689
+ const value2 = FormulaValue.array(arr);
4690
+ if (cache && rows2 > 0 && cols > 0) {
4691
+ cache.set(sheetKey, startRow, startCol, endRow, endCol, value2, rows2 * cols);
4692
+ }
4693
+ return value2;
4694
+ }
4695
+ /**
4696
+ * Resolves a whole-column (`A:A`) or whole-row (`1:1`) reference, bounded by
4697
+ * the sheet's used range the way Excel does. Without a bound this would
4698
+ * materialise a million-cell array for a sheet with twelve rows in it.
4699
+ *
4700
+ * A sheet that cannot report its extent yields `#REF!` rather than a guess:
4701
+ * silently reading nothing would make SUM(A:A) return 0 on a full column.
4702
+ */
4703
+ getFullRangeValues(startCol, endCol, startRow, endRow) {
4704
+ const bounds = __privateGet(this, _sheet).usedBounds;
4705
+ if (!bounds) return FormulaValue.errorRef;
4706
+ if (bounds.rowCount === 0 || bounds.columnCount === 0) return FormulaValue.blank;
4707
+ const r0 = startRow ?? 1;
4708
+ const r1 = endRow ?? bounds.rowCount;
4709
+ const c0 = startCol ?? 1;
4710
+ const c1 = endCol ?? bounds.columnCount;
4711
+ const lastRow = Math.min(r1, bounds.rowCount);
4712
+ const lastCol = Math.min(c1, bounds.columnCount);
4713
+ if (lastRow < r0 || lastCol < c0) return FormulaValue.blank;
4714
+ return this.getRangeValuesByIndex(r0, c0, lastRow, lastCol);
4287
4715
  }
4288
4716
  getSheetCellValue(sheetName, cellReference) {
4289
4717
  if (!this.workbook) return FormulaValue.errorRef;
4290
4718
  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);
4719
+ return __privateMethod(this, _FormulaContext_instances, forSheet_fn).call(this, sheetName).getCellValue(cellReference);
4295
4720
  } catch {
4296
4721
  return FormulaValue.errorRef;
4297
4722
  }
@@ -4299,10 +4724,16 @@ var _FormulaContext = class _FormulaContext {
4299
4724
  getSheetRangeValues(sheetName, startRef, endRef) {
4300
4725
  if (!this.workbook) return FormulaValue.errorRef;
4301
4726
  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);
4727
+ return __privateMethod(this, _FormulaContext_instances, forSheet_fn).call(this, sheetName).getRangeValues(startRef, endRef);
4728
+ } catch {
4729
+ return FormulaValue.errorRef;
4730
+ }
4731
+ }
4732
+ /** Whole-column/whole-row reference qualified by a sheet name. */
4733
+ getSheetFullRangeValues(sheetName, startCol, endCol, startRow, endRow) {
4734
+ if (!this.workbook) return FormulaValue.errorRef;
4735
+ try {
4736
+ return __privateMethod(this, _FormulaContext_instances, forSheet_fn).call(this, sheetName).getFullRangeValues(startCol, endCol, startRow, endRow);
4306
4737
  } catch {
4307
4738
  return FormulaValue.errorRef;
4308
4739
  }
@@ -4357,6 +4788,21 @@ lookupScope_fn = function(name) {
4357
4788
  }
4358
4789
  return void 0;
4359
4790
  };
4791
+ /**
4792
+ * A sibling context bound to another sheet, sharing this one's circular-
4793
+ * reference guard and range memo. These contexts only read values — they
4794
+ * never evaluate an AST — so the name resolver and recursion budget are
4795
+ * deliberately not carried over; passing them would suggest a scoping
4796
+ * relationship that does not exist. The memo, on the other hand, must be
4797
+ * carried: cross-sheet ranges are the most expensive reads in a workbook.
4798
+ */
4799
+ forSheet_fn = function(sheetName) {
4800
+ const sheet = this.workbook.getWorksheet(sheetName);
4801
+ const ctx = new _FormulaContext(sheet, this.formulaRow, this.formulaCol, __privateGet(this, _evaluating), __privateGet(this, _functions2));
4802
+ ctx.workbook = this.workbook;
4803
+ ctx.rangeCache = this.rangeCache;
4804
+ return ctx;
4805
+ };
4360
4806
  var FormulaContext = _FormulaContext;
4361
4807
 
4362
4808
  // src/lru-cache.ts
@@ -4476,6 +4922,7 @@ function buildContext(sheet, opts) {
4476
4922
  evaluating
4477
4923
  );
4478
4924
  ctx.recursionGuard = opts.recursionGuard ?? { depth: 0, max: DEFAULT_LAMBDA_DEPTH };
4925
+ ctx.rangeCache = opts.rangeCache;
4479
4926
  const wb = opts.workbook;
4480
4927
  if (wb) {
4481
4928
  ctx.workbook = wb;
@@ -4527,7 +4974,8 @@ function evaluateAst(ast, sheet, options) {
4527
4974
  const ctx = buildContext(sheet, {
4528
4975
  workbook: options?.workbook,
4529
4976
  formulaRow: options?.formulaRow,
4530
- formulaCol: options?.formulaCol
4977
+ formulaCol: options?.formulaCol,
4978
+ rangeCache: options?.rangeCache
4531
4979
  });
4532
4980
  return ast.evaluate(ctx);
4533
4981
  } catch {
@@ -4548,6 +4996,149 @@ function getAstCacheStats() {
4548
4996
  function setAstCacheCapacity(capacity) {
4549
4997
  astCache.resize(capacity);
4550
4998
  }
4999
+
5000
+ // src/range-cache.ts
5001
+ var TILE_ROWS = 64;
5002
+ var TILE_COLS = 64;
5003
+ var MAX_TILES_PER_RANGE = 32;
5004
+ var DEFAULT_MAX_CELLS = 2e6;
5005
+ var _entries, _tiles, _wide, _placements, _sizes, _cells, _maxCells, _RangeCache_static, contains_fn, _RangeCache_instances, drop_fn, fileIn_fn;
5006
+ var _RangeCache = class _RangeCache {
5007
+ constructor(maxCells = DEFAULT_MAX_CELLS) {
5008
+ __privateAdd(this, _RangeCache_instances);
5009
+ /** `SHEET r0 c0 r1 c1` → the memoised array value. Insertion-ordered (FIFO eviction). */
5010
+ __privateAdd(this, _entries, /* @__PURE__ */ new Map());
5011
+ /** `sheet tr tc` → entry keys whose rectangle overlaps that tile. */
5012
+ __privateAdd(this, _tiles, /* @__PURE__ */ new Map());
5013
+ /** sheet → entry keys whose rectangle is too broad to tile. */
5014
+ __privateAdd(this, _wide, /* @__PURE__ */ new Map());
5015
+ /** entry key → buckets it was filed under, for cheap removal. */
5016
+ __privateAdd(this, _placements, /* @__PURE__ */ new Map());
5017
+ /** entry key → its area, so eviction can give the budget back. */
5018
+ __privateAdd(this, _sizes, /* @__PURE__ */ new Map());
5019
+ /** Number of cells across all cached rectangles, kept under {@link #maxCells}. */
5020
+ __privateAdd(this, _cells, 0);
5021
+ __privateAdd(this, _maxCells);
5022
+ this.hits = 0;
5023
+ this.misses = 0;
5024
+ __privateSet(this, _maxCells, maxCells);
5025
+ }
5026
+ get size() {
5027
+ return __privateGet(this, _entries).size;
5028
+ }
5029
+ get cachedCells() {
5030
+ return __privateGet(this, _cells);
5031
+ }
5032
+ static key(sheet, r0, c0, r1, c1) {
5033
+ return `${sheet} ${r0} ${c0} ${r1} ${c1}`;
5034
+ }
5035
+ get(sheet, r0, c0, r1, c1) {
5036
+ const hit = __privateGet(this, _entries).get(_RangeCache.key(sheet, r0, c0, r1, c1));
5037
+ if (hit === void 0) this.misses++;
5038
+ else this.hits++;
5039
+ return hit;
5040
+ }
5041
+ /** Memoises a rectangle's value. `cells` is its area, for the budget. */
5042
+ set(sheet, r0, c0, r1, c1, value2, cells) {
5043
+ if (cells <= 0 || cells > __privateGet(this, _maxCells)) return;
5044
+ const key = _RangeCache.key(sheet, r0, c0, r1, c1);
5045
+ if (__privateGet(this, _entries).has(key)) __privateMethod(this, _RangeCache_instances, drop_fn).call(this, key);
5046
+ __privateGet(this, _entries).set(key, value2);
5047
+ __privateSet(this, _cells, __privateGet(this, _cells) + cells);
5048
+ const buckets = [];
5049
+ const tileRow0 = r0 / TILE_ROWS | 0, tileRow1 = r1 / TILE_ROWS | 0;
5050
+ const tileCol0 = c0 / TILE_COLS | 0, tileCol1 = c1 / TILE_COLS | 0;
5051
+ const tileCount = (tileRow1 - tileRow0 + 1) * (tileCol1 - tileCol0 + 1);
5052
+ if (tileCount > MAX_TILES_PER_RANGE) {
5053
+ __privateMethod(this, _RangeCache_instances, fileIn_fn).call(this, __privateGet(this, _wide), sheet, key, buckets, `w ${sheet}`);
5054
+ } else {
5055
+ for (let tr = tileRow0; tr <= tileRow1; tr++) {
5056
+ for (let tc = tileCol0; tc <= tileCol1; tc++) {
5057
+ const bucket = `${sheet} ${tr} ${tc}`;
5058
+ __privateMethod(this, _RangeCache_instances, fileIn_fn).call(this, __privateGet(this, _tiles), bucket, key, buckets, `t ${bucket}`);
5059
+ }
5060
+ }
5061
+ }
5062
+ __privateGet(this, _placements).set(key, buckets);
5063
+ __privateGet(this, _sizes).set(key, cells);
5064
+ if (__privateGet(this, _cells) > __privateGet(this, _maxCells)) {
5065
+ for (const oldest of __privateGet(this, _entries).keys()) {
5066
+ if (__privateGet(this, _cells) <= __privateGet(this, _maxCells)) break;
5067
+ if (oldest === key) continue;
5068
+ __privateMethod(this, _RangeCache_instances, drop_fn).call(this, oldest);
5069
+ }
5070
+ }
5071
+ }
5072
+ /** Drops every cached rectangle containing the given cell. */
5073
+ invalidateCell(sheet, row2, col) {
5074
+ var _a, _b;
5075
+ const tile = __privateGet(this, _tiles).get(`${sheet} ${row2 / TILE_ROWS | 0} ${col / TILE_COLS | 0}`);
5076
+ if (tile) {
5077
+ for (const key of [...tile]) {
5078
+ if (__privateMethod(_a = _RangeCache, _RangeCache_static, contains_fn).call(_a, key, sheet, row2, col)) __privateMethod(this, _RangeCache_instances, drop_fn).call(this, key);
5079
+ }
5080
+ }
5081
+ const wide = __privateGet(this, _wide).get(sheet);
5082
+ if (wide) {
5083
+ for (const key of [...wide]) {
5084
+ if (__privateMethod(_b = _RangeCache, _RangeCache_static, contains_fn).call(_b, key, sheet, row2, col)) __privateMethod(this, _RangeCache_instances, drop_fn).call(this, key);
5085
+ }
5086
+ }
5087
+ }
5088
+ clear() {
5089
+ __privateGet(this, _entries).clear();
5090
+ __privateGet(this, _tiles).clear();
5091
+ __privateGet(this, _wide).clear();
5092
+ __privateGet(this, _placements).clear();
5093
+ __privateGet(this, _sizes).clear();
5094
+ __privateSet(this, _cells, 0);
5095
+ }
5096
+ };
5097
+ _entries = new WeakMap();
5098
+ _tiles = new WeakMap();
5099
+ _wide = new WeakMap();
5100
+ _placements = new WeakMap();
5101
+ _sizes = new WeakMap();
5102
+ _cells = new WeakMap();
5103
+ _maxCells = new WeakMap();
5104
+ _RangeCache_static = new WeakSet();
5105
+ contains_fn = function(key, sheet, row2, col) {
5106
+ const parts = key.split(" ");
5107
+ const n = parts.length;
5108
+ const c1 = +parts[n - 1], r1 = +parts[n - 2], c0 = +parts[n - 3], r0 = +parts[n - 4];
5109
+ if (parts.slice(0, n - 4).join(" ") !== sheet) return false;
5110
+ return row2 >= r0 && row2 <= r1 && col >= c0 && col <= c1;
5111
+ };
5112
+ _RangeCache_instances = new WeakSet();
5113
+ drop_fn = function(key) {
5114
+ if (!__privateGet(this, _entries).delete(key)) return;
5115
+ __privateSet(this, _cells, __privateGet(this, _cells) - (__privateGet(this, _sizes).get(key) ?? 0));
5116
+ __privateGet(this, _sizes).delete(key);
5117
+ const buckets = __privateGet(this, _placements).get(key);
5118
+ if (buckets) {
5119
+ for (const bucket of buckets) {
5120
+ const store = bucket.charCodeAt(0) === 119 ? __privateGet(this, _wide) : __privateGet(this, _tiles);
5121
+ const bucketKey = bucket.slice(2);
5122
+ const set = store.get(bucketKey);
5123
+ if (!set) continue;
5124
+ set.delete(key);
5125
+ if (set.size === 0) store.delete(bucketKey);
5126
+ }
5127
+ __privateGet(this, _placements).delete(key);
5128
+ }
5129
+ };
5130
+ fileIn_fn = function(store, bucketKey, entryKey, buckets, bucketId) {
5131
+ let set = store.get(bucketKey);
5132
+ if (!set) {
5133
+ set = /* @__PURE__ */ new Set();
5134
+ store.set(bucketKey, set);
5135
+ }
5136
+ if (set.has(entryKey)) return;
5137
+ set.add(entryKey);
5138
+ buckets.push(bucketId);
5139
+ };
5140
+ __privateAdd(_RangeCache, _RangeCache_static);
5141
+ var RangeCache = _RangeCache;
4551
5142
  function extractReferences(ast, formulaSheet, registry = FunctionRegistry.default, resolveName2) {
4552
5143
  const refs = [];
4553
5144
  let volatile = false;
@@ -4576,6 +5167,14 @@ function extractReferences(ast, formulaSheet, registry = FunctionRegistry.defaul
4576
5167
  refs.push(cell(node.sheetName, node.cellReference));
4577
5168
  } else if (node instanceof SheetRangeReferenceNode) {
4578
5169
  refs.push(range(node.sheetName, node.startRef, node.endRef));
5170
+ } else if (node instanceof FullRangeReferenceNode) {
5171
+ refs.push({
5172
+ sheet: (node.sheetName ?? formulaSheet).toUpperCase(),
5173
+ startRow: node.startRow ?? 1,
5174
+ endRow: node.endRow ?? puredocsExcel.EXCEL_MAX_ROWS,
5175
+ startCol: node.startCol ?? 1,
5176
+ endCol: node.endCol ?? puredocsExcel.EXCEL_MAX_COLUMNS
5177
+ });
4579
5178
  } else if (node instanceof NamedRangeNode) {
4580
5179
  expandName(node.name);
4581
5180
  } else if (node instanceof SpillReferenceNode) {
@@ -4612,19 +5211,30 @@ function refContains(ref, sheet, row2, col) {
4612
5211
  }
4613
5212
 
4614
5213
  // src/dependency-index.ts
4615
- var TILE_ROWS = 64;
4616
- var TILE_COLS = 64;
5214
+ var TILE_ROWS2 = 64;
5215
+ var TILE_COLS2 = 64;
4617
5216
  var MAX_TILES_PER_REF = 32;
4618
- var _tiles, _wide, _placements, _DependencyIndex_instances, fileIn_fn;
5217
+ var MAX_EXACT_SPAN = 16;
5218
+ var MAX_BUCKETS_PER_REF = 64;
5219
+ var tileOfRow = (row2) => row2 / TILE_ROWS2 | 0;
5220
+ var tileOfCol = (col) => col / TILE_COLS2 | 0;
5221
+ var _buckets, _placements2, _DependencyIndex_instances, visit_fn, file_fn;
4619
5222
  var DependencyIndex = class {
4620
5223
  constructor() {
4621
5224
  __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());
5225
+ /**
5226
+ * All buckets in one map, keyed by a discriminating prefix:
5227
+ *
5228
+ * `t s tr tc` rectangle compact in both dimensions
5229
+ * `c s col tr` narrow columns, row tiles (the common case)
5230
+ * `C s col` narrow columns, every row (`A:A`)
5231
+ * `r s row tc` narrow rows, column tiles
5232
+ * `R s row` narrow rows, every column (`1:1`)
5233
+ * `w s` broad in both dimensions
5234
+ */
5235
+ __privateAdd(this, _buckets, /* @__PURE__ */ new Map());
5236
+ /** formula key → the buckets it was filed under, for cheap removal. */
5237
+ __privateAdd(this, _placements2, /* @__PURE__ */ new Map());
4628
5238
  }
4629
5239
  /** Registers (or re-registers) a formula's precedent rectangles. */
4630
5240
  add(formulaKey, refs) {
@@ -4632,78 +5242,117 @@ var DependencyIndex = class {
4632
5242
  if (refs.length === 0) return;
4633
5243
  const buckets = [];
4634
5244
  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}`);
5245
+ const rowSpan = ref.endRow - ref.startRow + 1;
5246
+ const colSpan = ref.endCol - ref.startCol + 1;
5247
+ const rowTile0 = tileOfRow(ref.startRow), rowTile1 = tileOfRow(ref.endRow);
5248
+ const colTile0 = tileOfCol(ref.startCol), colTile1 = tileOfCol(ref.endCol);
5249
+ const rowTiles = rowTile1 - rowTile0 + 1;
5250
+ const colTiles = colTile1 - colTile0 + 1;
5251
+ if (colSpan <= MAX_EXACT_SPAN && colSpan <= rowSpan) {
5252
+ if (colSpan * rowTiles <= MAX_BUCKETS_PER_REF && rowTiles <= MAX_TILES_PER_REF) {
5253
+ for (let col = ref.startCol; col <= ref.endCol; col++) {
5254
+ for (let tr = rowTile0; tr <= rowTile1; tr++) {
5255
+ __privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `c ${ref.sheet} ${col} ${tr}`, formulaKey, buckets);
5256
+ }
5257
+ }
5258
+ } else {
5259
+ for (let col = ref.startCol; col <= ref.endCol; col++) {
5260
+ __privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `C ${ref.sheet} ${col}`, formulaKey, buckets);
5261
+ }
5262
+ }
4642
5263
  continue;
4643
5264
  }
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}`);
5265
+ if (rowSpan <= MAX_EXACT_SPAN) {
5266
+ if (rowSpan * colTiles <= MAX_BUCKETS_PER_REF && colTiles <= MAX_TILES_PER_REF) {
5267
+ for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
5268
+ for (let tc = colTile0; tc <= colTile1; tc++) {
5269
+ __privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `r ${ref.sheet} ${row2} ${tc}`, formulaKey, buckets);
5270
+ }
5271
+ }
5272
+ } else {
5273
+ for (let row2 = ref.startRow; row2 <= ref.endRow; row2++) {
5274
+ __privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `R ${ref.sheet} ${row2}`, formulaKey, buckets);
5275
+ }
4648
5276
  }
5277
+ continue;
4649
5278
  }
5279
+ if (rowTiles * colTiles <= MAX_TILES_PER_REF) {
5280
+ for (let tr = rowTile0; tr <= rowTile1; tr++) {
5281
+ for (let tc = colTile0; tc <= colTile1; tc++) {
5282
+ __privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `t ${ref.sheet} ${tr} ${tc}`, formulaKey, buckets);
5283
+ }
5284
+ }
5285
+ continue;
5286
+ }
5287
+ __privateMethod(this, _DependencyIndex_instances, file_fn).call(this, `w ${ref.sheet}`, formulaKey, buckets);
4650
5288
  }
4651
- __privateGet(this, _placements).set(formulaKey, buckets);
5289
+ __privateGet(this, _placements2).set(formulaKey, buckets);
4652
5290
  }
4653
5291
  /** Forgets a formula. */
4654
5292
  remove(formulaKey) {
4655
- const buckets = __privateGet(this, _placements).get(formulaKey);
5293
+ const buckets = __privateGet(this, _placements2).get(formulaKey);
4656
5294
  if (!buckets) return;
4657
5295
  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);
5296
+ const set = __privateGet(this, _buckets).get(bucket);
4662
5297
  if (!set) continue;
4663
5298
  set.delete(formulaKey);
4664
- if (set.size === 0) store.delete(key);
5299
+ if (set.size === 0) __privateGet(this, _buckets).delete(bucket);
4665
5300
  }
4666
- __privateGet(this, _placements).delete(formulaKey);
5301
+ __privateGet(this, _placements2).delete(formulaKey);
4667
5302
  }
4668
5303
  /** Drops every registration. */
4669
5304
  clear() {
4670
- __privateGet(this, _tiles).clear();
4671
- __privateGet(this, _wide).clear();
4672
- __privateGet(this, _placements).clear();
5305
+ __privateGet(this, _buckets).clear();
5306
+ __privateGet(this, _placements2).clear();
4673
5307
  }
4674
5308
  /**
4675
5309
  * 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.
5310
+ * Callers still confirm with {@link refContains}: buckets are coarser than
5311
+ * rectangles in the broad dimension, so this over-approximates.
5312
+ *
5313
+ * A fixed six lookups, whatever the workbook contains.
4678
5314
  */
4679
5315
  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);
5316
+ const tr = tileOfRow(row2);
5317
+ const tc = tileOfCol(col);
5318
+ __privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `c ${sheet} ${col} ${tr}`, visit);
5319
+ __privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `C ${sheet} ${col}`, visit);
5320
+ __privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `r ${sheet} ${row2} ${tc}`, visit);
5321
+ __privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `R ${sheet} ${row2}`, visit);
5322
+ __privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `t ${sheet} ${tr} ${tc}`, visit);
5323
+ __privateMethod(this, _DependencyIndex_instances, visit_fn).call(this, `w ${sheet}`, visit);
4686
5324
  }
4687
5325
  };
4688
- _tiles = new WeakMap();
4689
- _wide = new WeakMap();
4690
- _placements = new WeakMap();
5326
+ _buckets = new WeakMap();
5327
+ _placements2 = new WeakMap();
4691
5328
  _DependencyIndex_instances = new WeakSet();
4692
- fileIn_fn = function(store, key, formulaKey, buckets, bucketId) {
4693
- let set = store.get(key);
5329
+ visit_fn = function(bucket, visit) {
5330
+ const set = __privateGet(this, _buckets).get(bucket);
5331
+ if (set) for (const key of set) visit(key);
5332
+ };
5333
+ file_fn = function(bucket, formulaKey, buckets) {
5334
+ let set = __privateGet(this, _buckets).get(bucket);
4694
5335
  if (!set) {
4695
5336
  set = /* @__PURE__ */ new Set();
4696
- store.set(key, set);
5337
+ __privateGet(this, _buckets).set(bucket, set);
4697
5338
  }
4698
5339
  if (set.has(formulaKey)) return;
4699
5340
  set.add(formulaKey);
4700
- buckets.push(bucketId);
5341
+ buckets.push(bucket);
4701
5342
  };
4702
5343
 
4703
5344
  // src/recalc-engine.ts
4704
5345
  function createWorkbookRecalcModel(workbook) {
4705
5346
  return {
4706
5347
  getCellValue: (sheet, ref) => workbook.getWorksheet(sheet).getCellValue(ref),
5348
+ // Falls back to the reference-string read rather than reporting "no value":
5349
+ // an older core without getCellValueAt must still return the real value,
5350
+ // because a silent undefined here reads as a blank cell.
5351
+ getCellValueAt: (sheet, row2, column2) => {
5352
+ const ws = workbook.getWorksheet(sheet);
5353
+ return ws.getCellValueAt ? ws.getCellValueAt(row2, column2) : ws.getCellValue(puredocsExcel.cellRefFromRowCol(row2, column2));
5354
+ },
5355
+ getUsedBounds: (sheet) => workbook.getWorksheet(sheet).usedBounds,
4707
5356
  setCellValue: (sheet, ref, value2) => {
4708
5357
  workbook.getWorksheet(sheet).getCell(ref).setComputedValue(value2);
4709
5358
  },
@@ -4732,7 +5381,7 @@ function parseKey(key) {
4732
5381
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(ref);
4733
5382
  return { sheet, ref, row: row2, col: column2 };
4734
5383
  }
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;
5384
+ 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
5385
  var RecalcEngine = class {
4737
5386
  constructor(model, registry = FunctionRegistry.default) {
4738
5387
  __privateAdd(this, _RecalcEngine_instances);
@@ -4758,6 +5407,14 @@ var RecalcEngine = class {
4758
5407
  /** Memoised per-sheet adapters — these are stateless w.r.t. the formula. */
4759
5408
  __privateAdd(this, _sheetLikes, /* @__PURE__ */ new Map());
4760
5409
  __privateAdd(this, _workbookLike);
5410
+ /**
5411
+ * Memo for range reads. The engine owns it because it owns the writes: every
5412
+ * value written goes through {@link #writeCell}, which drops the cached
5413
+ * rectangles containing that cell. Nothing else may write to the model behind
5414
+ * the engine's back without calling {@link onCellChanged} — that was already
5415
+ * true for dependency tracking, and now also keeps this memo honest.
5416
+ */
5417
+ __privateAdd(this, _rangeCache, new RangeCache());
4761
5418
  __privateSet(this, _model, model);
4762
5419
  __privateSet(this, _registry, registry);
4763
5420
  }
@@ -4795,8 +5452,63 @@ var RecalcEngine = class {
4795
5452
  volatile
4796
5453
  });
4797
5454
  __privateGet(this, _model).setCellFormula?.(sheet, r, text);
5455
+ __privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
4798
5456
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }], key);
4799
5457
  }
5458
+ /**
5459
+ * Indexes many formulas **without evaluating anything**.
5460
+ *
5461
+ * This is the path for loading a workbook. `setCellFormula` is built for an
5462
+ * edit: it recalculates the new cell and everything downstream of it, which is
5463
+ * right for one keystroke and quadratic for a file — registering N formulas one
5464
+ * by one evaluates the whole workbook N times. Here the formulas are only
5465
+ * parsed and filed into the dependency index; call {@link recalcAll} once
5466
+ * afterwards, or nothing at all if the cached values from the file will do.
5467
+ *
5468
+ * The model is deliberately **not** written to. These formulas are assumed to
5469
+ * be the ones the model already holds (they came from the file), and persisting
5470
+ * them again would clear each cell's cached value — leaving every formula cell
5471
+ * blank until a full recalculation had run. Use `setCellFormula` for formulas
5472
+ * the user is actually introducing.
5473
+ *
5474
+ * A formula that fails to parse is reported rather than thrown: one unsupported
5475
+ * construct in a large workbook should not abort the load.
5476
+ */
5477
+ registerBulk(entries) {
5478
+ const resolveName2 = __privateGet(this, _model).getDefinedName ? (n, sh) => __privateGet(this, _model).getDefinedName(n, sh) : void 0;
5479
+ let registered = 0;
5480
+ const failed = [];
5481
+ for (const entry of entries) {
5482
+ const { sheet, ref, formula } = entry;
5483
+ if (!formula) continue;
5484
+ const text = formula.startsWith("=") ? formula.slice(1) : formula;
5485
+ const s = normSheet(sheet);
5486
+ const r = normRef(ref);
5487
+ try {
5488
+ const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5489
+ const ast = parseFormulaUncached(text);
5490
+ const { refs, volatile } = extractReferences(ast, s, __privateGet(this, _registry), resolveName2);
5491
+ __privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
5492
+ __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, {
5493
+ sheet: s,
5494
+ sheetName: sheet,
5495
+ ref: r,
5496
+ key: keyOf(s, r),
5497
+ row: row2,
5498
+ col: column2,
5499
+ formula: text,
5500
+ ast,
5501
+ refs,
5502
+ volatile
5503
+ });
5504
+ registered++;
5505
+ } catch (err) {
5506
+ failed.push({ sheet, ref, error: err instanceof Error ? err.message : String(err) });
5507
+ }
5508
+ }
5509
+ if (registered > 0) __privateGet(this, _rangeCache).clear();
5510
+ return { registered, failed };
5511
+ }
4800
5512
  /**
4801
5513
  * Removes a formula from a cell and recalculates its dependents (which may now
4802
5514
  * read a plain input value instead). Returns the recomputed dependent cells.
@@ -4809,6 +5521,7 @@ var RecalcEngine = class {
4809
5521
  __privateMethod(this, _RecalcEngine_instances, unregister_fn).call(this, key);
4810
5522
  __privateGet(this, _model).clearCell?.(sheet, r);
4811
5523
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5524
+ __privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
4812
5525
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
4813
5526
  }
4814
5527
  /**
@@ -4819,8 +5532,8 @@ var RecalcEngine = class {
4819
5532
  __privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
4820
5533
  const s = normSheet(sheet);
4821
5534
  const r = normRef(ref);
4822
- __privateGet(this, _model).setCellValue(sheet, r, value2);
4823
5535
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5536
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, sheet, r, value2, row2, column2);
4824
5537
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
4825
5538
  }
4826
5539
  /**
@@ -4832,11 +5545,48 @@ var RecalcEngine = class {
4832
5545
  const s = normSheet(sheet);
4833
5546
  const r = normRef(ref);
4834
5547
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
5548
+ __privateGet(this, _rangeCache).invalidateCell(s, row2, column2);
4835
5549
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
4836
5550
  }
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;
5551
+ /**
5552
+ * Recalculates every registered formula.
5553
+ *
5554
+ * By default the order is derived here, with a topological sort over the
5555
+ * dependency index. `order` — typically `workbook.calcChain` — supplies one
5556
+ * instead, so the sort is not needed.
5557
+ *
5558
+ * **A supplied order is verified, not trusted.** Evaluating in a stale order
5559
+ * makes a formula read a precedent that has not been computed yet, and the
5560
+ * result is a plausible wrong number with nothing to distinguish it from a
5561
+ * right one. In a calculation engine that is the worst failure available, so
5562
+ * this checks as it goes: after each formula is written, the dependency index
5563
+ * says who reads it, and any reader that was already evaluated is an inversion.
5564
+ * On the first inversion the ordered attempt is abandoned and the whole
5565
+ * recalculation is redone through the sorted path, whose result is returned;
5566
+ * `onOrderConflict`, if given, is told which pair conflicted.
5567
+ *
5568
+ * The verification costs about what the sort it replaces costs, so `order` is
5569
+ * worth roughly nothing now (measured at ~9% of a full recalculation on a
5570
+ * 2 000-formula workbook, after the dependency index stopped being the
5571
+ * bottleneck). It is kept because reading an order Excel already computed is a
5572
+ * reasonable thing to want; prefer plain `recalcAll()` unless you have measured
5573
+ * a reason not to.
5574
+ *
5575
+ * Registered formulas the order does not mention are evaluated afterwards
5576
+ * through the sorted path, and a dynamic array that spills still triggers the
5577
+ * usual fixpoint, so neither silently misses cells.
5578
+ */
5579
+ recalcAll(options) {
5580
+ const order = options?.order;
5581
+ if (!order || order.length === 0) {
5582
+ return __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...__privateGet(this, _formulas).values()]).results;
5583
+ }
5584
+ const attempt = __privateMethod(this, _RecalcEngine_instances, recalcInOrder_fn).call(this, order);
5585
+ if (attempt.conflict) {
5586
+ options?.onOrderConflict?.(attempt.conflict);
5587
+ return __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, [...__privateGet(this, _formulas).values()]).results;
5588
+ }
5589
+ return attempt.results;
4840
5590
  }
4841
5591
  /** The precedents recorded for a formula cell (mainly for tests/inspection). */
4842
5592
  getPrecedents(sheet, ref) {
@@ -4886,7 +5636,23 @@ _dependents = new WeakMap();
4886
5636
  _volatiles = new WeakMap();
4887
5637
  _sheetLikes = new WeakMap();
4888
5638
  _workbookLike = new WeakMap();
5639
+ _rangeCache = new WeakMap();
4889
5640
  _RecalcEngine_instances = new WeakSet();
5641
+ /**
5642
+ * The single write path into the model. Writing a value invalidates the
5643
+ * cached rectangles that contain the cell, so a formula evaluated later in the
5644
+ * same pass cannot read a pre-write copy of a range. Coordinates are passed in
5645
+ * where the caller already knows them, to avoid re-parsing the reference.
5646
+ */
5647
+ writeCell_fn = function(sheetName, ref, value2, row2, col) {
5648
+ __privateGet(this, _model).setCellValue(sheetName, ref, value2);
5649
+ if (row2 === void 0 || col === void 0) {
5650
+ const parsed = puredocsExcel.parseCellRef(ref);
5651
+ row2 = parsed.row;
5652
+ col = parsed.column;
5653
+ }
5654
+ __privateGet(this, _rangeCache).invalidateCell(normSheet(sheetName), row2, col);
5655
+ };
4890
5656
  /** Registers an entry in the map, the reverse index and the volatile set. */
4891
5657
  register_fn = function(entry) {
4892
5658
  __privateGet(this, _formulas).set(entry.key, entry);
@@ -4902,6 +5668,49 @@ unregister_fn = function(key) {
4902
5668
  rememberSheet_fn = function(name) {
4903
5669
  __privateGet(this, _sheetNames).set(normSheet(name), name);
4904
5670
  };
5671
+ /**
5672
+ * One pass in a caller-supplied order, watching for inversions.
5673
+ *
5674
+ * The check is the mirror image of the topological sort: instead of asking
5675
+ * "which of my precedents come first?", it asks, of each formula just
5676
+ * evaluated, "has anything that reads me already run?". The dependency index
5677
+ * answers that directly, and one positive answer is enough to know the order
5678
+ * cannot be trusted.
5679
+ */
5680
+ recalcInOrder_fn = function(order) {
5681
+ const results = [];
5682
+ const changed = [];
5683
+ const done = /* @__PURE__ */ new Set();
5684
+ let spilled = false;
5685
+ for (const { sheet, ref } of order) {
5686
+ const key = keyOf(sheet, ref);
5687
+ if (done.has(key)) continue;
5688
+ const entry = __privateGet(this, _formulas).get(key);
5689
+ if (!entry) continue;
5690
+ done.add(key);
5691
+ const written = __privateMethod(this, _RecalcEngine_instances, writeResult_fn).call(this, entry);
5692
+ results.push(written.result);
5693
+ changed.push(...written.changed);
5694
+ if (written.result.spill) spilled = true;
5695
+ let conflict;
5696
+ __privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: entry.sheet, row: entry.row, col: entry.col }, (dep) => {
5697
+ if (conflict || dep.key === entry.key) return;
5698
+ if (done.has(dep.key)) conflict = { dependent: dep.key, precedent: entry.key };
5699
+ });
5700
+ if (conflict) return { results: [], conflict };
5701
+ }
5702
+ const rest = [...__privateGet(this, _formulas).values()].filter((e) => !done.has(e.key));
5703
+ if (rest.length > 0) {
5704
+ const pass = __privateMethod(this, _RecalcEngine_instances, evaluatePass_fn).call(this, rest);
5705
+ results.push(...pass.results);
5706
+ changed.push(...pass.changed);
5707
+ }
5708
+ if (spilled) {
5709
+ const evaluated = new Set(done);
5710
+ results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, changed, evaluated), evaluated));
5711
+ }
5712
+ return { results };
5713
+ };
4905
5714
  applyShift_fn = function(shift) {
4906
5715
  const editedSheet = normSheet(shift.sheetName);
4907
5716
  const isRowShift = shift.dim === "row";
@@ -4943,6 +5752,7 @@ applyShift_fn = function(shift) {
4943
5752
  __privateGet(this, _volatiles).clear();
4944
5753
  for (const e of rekeyed) __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, e);
4945
5754
  __privateMethod(this, _RecalcEngine_instances, shiftSpillBookkeeping_fn).call(this, shift, editedSheet);
5755
+ __privateGet(this, _rangeCache).clear();
4946
5756
  const pending = /* @__PURE__ */ new Map();
4947
5757
  for (const k of needsRecalc) {
4948
5758
  const e = __privateGet(this, _formulas).get(k);
@@ -5013,7 +5823,7 @@ shiftSpillBookkeeping_fn = function(shift, editedSheet) {
5013
5823
  const shifted = shiftCellKey(m);
5014
5824
  if (!shifted) continue;
5015
5825
  const cell = parseKey(shifted);
5016
- __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
5826
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
5017
5827
  }
5018
5828
  continue;
5019
5829
  }
@@ -5154,7 +5964,7 @@ evaluatePass_fn = function(entries) {
5154
5964
  if (emitted.has(k)) continue;
5155
5965
  __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, e.key, changed);
5156
5966
  const value2 = FormulaValue.errorRef.toObject();
5157
- __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
5967
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
5158
5968
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
5159
5969
  results.push({ sheet: e.sheetName, ref: e.ref, value: value2, circular: true });
5160
5970
  }
@@ -5175,7 +5985,7 @@ writeResult_fn = function(e) {
5175
5985
  __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
5176
5986
  __privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
5177
5987
  const value2 = fv2.toObject();
5178
- __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
5988
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
5179
5989
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
5180
5990
  return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
5181
5991
  }
@@ -5196,7 +6006,7 @@ writeResult_fn = function(e) {
5196
6006
  __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, anchorKey, changed);
5197
6007
  __privateGet(this, _model).setCellArrayRef?.(e.sheetName, e.ref, null);
5198
6008
  const value2 = FormulaValue.error(9 /* Spill */).toObject();
5199
- __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
6009
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, e.ref, value2, e.row, e.col);
5200
6010
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
5201
6011
  return { result: { sheet: e.sheetName, ref: e.ref, value: value2 }, changed };
5202
6012
  }
@@ -5205,7 +6015,7 @@ writeResult_fn = function(e) {
5205
6015
  for (let dc = 0; dc < cols; dc++) {
5206
6016
  const r = e.row + dr, c = e.col + dc;
5207
6017
  const ref = puredocsExcel.cellRefFromRowCol(r, c);
5208
- __privateGet(this, _model).setCellValue(e.sheetName, ref, arr.get(dr, dc).toObject());
6018
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, e.sheetName, ref, arr.get(dr, dc).toObject(), r, c);
5209
6019
  changed.push({ sheet: e.sheet, row: r, col: c });
5210
6020
  if (dr !== 0 || dc !== 0) {
5211
6021
  const cellKey2 = keyOf(e.sheet, ref);
@@ -5217,7 +6027,7 @@ writeResult_fn = function(e) {
5217
6027
  for (const oldKey of prevSpill) {
5218
6028
  if (newSpill.has(oldKey)) continue;
5219
6029
  const cell = parseKey(oldKey);
5220
- __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
6030
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
5221
6031
  __privateGet(this, _spillOwner).delete(oldKey);
5222
6032
  changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
5223
6033
  }
@@ -5244,7 +6054,7 @@ clearSpill_fn = function(anchorKey, changed) {
5244
6054
  if (!spill) return;
5245
6055
  for (const cellKey2 of spill) {
5246
6056
  const cell = parseKey(cellKey2);
5247
- __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
6057
+ __privateMethod(this, _RecalcEngine_instances, writeCell_fn).call(this, __privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null, cell.row, cell.col);
5248
6058
  __privateGet(this, _spillOwner).delete(cellKey2);
5249
6059
  changed.push({ sheet: cell.sheet, row: cell.row, col: cell.col });
5250
6060
  }
@@ -5261,7 +6071,8 @@ evaluateFormulaValue_fn = function(e) {
5261
6071
  return evaluateAst(e.ast, __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName), {
5262
6072
  workbook: __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this),
5263
6073
  formulaRow: e.row,
5264
- formulaCol: e.col
6074
+ formulaCol: e.col,
6075
+ rangeCache: __privateGet(this, _rangeCache)
5265
6076
  });
5266
6077
  };
5267
6078
  /**
@@ -5278,9 +6089,16 @@ sheetLike_fn = function(sheetName) {
5278
6089
  return cached;
5279
6090
  };
5280
6091
  buildSheetLike_fn = function(sheetName) {
6092
+ const model = __privateGet(this, _model);
5281
6093
  return {
5282
6094
  name: sheetName,
5283
6095
  getCellValue: (ref) => __privateGet(this, _model).getCellValue(sheetName, ref),
6096
+ getCellValueAt: model.getCellValueAt ? (row2, column2) => model.getCellValueAt(sheetName, row2, column2) : void 0,
6097
+ // A getter, not a snapshot: the extent grows as formulas write results,
6098
+ // and a whole-column reference read later in the same pass must see it.
6099
+ get usedBounds() {
6100
+ return model.getUsedBounds?.(sheetName);
6101
+ },
5284
6102
  isRowHidden: __privateGet(this, _model).isRowHidden ? (row2) => __privateGet(this, _model).isRowHidden(sheetName, row2) : void 0,
5285
6103
  getCellFormula: __privateGet(this, _model).getCellFormula ? (ref) => __privateGet(this, _model).getCellFormula(sheetName, ref) : void 0,
5286
6104
  getSpillRange: __privateGet(this, _model).getSpillRange ? (ref) => __privateGet(this, _model).getSpillRange(sheetName, ref) : void 0
@@ -5319,12 +6137,14 @@ exports.FormulaLexer = FormulaLexer;
5319
6137
  exports.FormulaNode = FormulaNode;
5320
6138
  exports.FormulaParser = FormulaParser;
5321
6139
  exports.FormulaValue = FormulaValue;
6140
+ exports.FullRangeReferenceNode = FullRangeReferenceNode;
5322
6141
  exports.FunctionCallNode = FunctionCallNode;
5323
6142
  exports.FunctionRegistry = FunctionRegistry;
5324
6143
  exports.ImplicitIntersectionNode = ImplicitIntersectionNode;
5325
6144
  exports.LruCache = LruCache;
5326
6145
  exports.NamedRangeNode = NamedRangeNode;
5327
6146
  exports.NumberNode = NumberNode;
6147
+ exports.RangeCache = RangeCache;
5328
6148
  exports.RangeReferenceNode = RangeReferenceNode;
5329
6149
  exports.RecalcEngine = RecalcEngine;
5330
6150
  exports.SheetCellReferenceNode = SheetCellReferenceNode;