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