@pixerate/editor 0.21.0 → 0.23.0

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.
Files changed (114) hide show
  1. package/README.md +100 -13
  2. package/THIRD_PARTY_NOTICES.md +88 -0
  3. package/dist/agent/index.cjs +86 -111
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +69 -211
  6. package/dist/agent/index.d.ts +69 -211
  7. package/dist/agent/index.js +77 -102
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
  10. package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
  11. package/dist/canvas/index.cjs +2112 -60
  12. package/dist/canvas/index.cjs.map +1 -1
  13. package/dist/canvas/index.d.cts +712 -181
  14. package/dist/canvas/index.d.ts +712 -181
  15. package/dist/canvas/index.js +2105 -53
  16. package/dist/canvas/index.js.map +1 -1
  17. package/dist/chunk-26BVFKHU.cjs +453 -0
  18. package/dist/chunk-26BVFKHU.cjs.map +1 -0
  19. package/dist/chunk-2H7ZHPEJ.js +762 -0
  20. package/dist/chunk-2H7ZHPEJ.js.map +1 -0
  21. package/dist/chunk-7DWMJCAT.js +453 -0
  22. package/dist/chunk-7DWMJCAT.js.map +1 -0
  23. package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
  24. package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
  25. package/dist/chunk-ARRTP33I.js +101 -0
  26. package/dist/chunk-ARRTP33I.js.map +1 -0
  27. package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
  28. package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
  29. package/dist/chunk-CPOKFNBG.cjs +101 -0
  30. package/dist/chunk-CPOKFNBG.cjs.map +1 -0
  31. package/dist/chunk-G2I4QJOT.cjs +762 -0
  32. package/dist/chunk-G2I4QJOT.cjs.map +1 -0
  33. package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
  34. package/dist/chunk-GVSMRPHB.cjs.map +1 -0
  35. package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
  36. package/dist/{chunk-GVW4P7BG.js → chunk-K3R5NG2D.js} +448 -54
  37. package/dist/chunk-K3R5NG2D.js.map +1 -0
  38. package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
  39. package/dist/chunk-MJZBNWG7.js.map +1 -0
  40. package/dist/{chunk-A7DKIA72.js → chunk-QBJ22YZR.js} +1059 -93
  41. package/dist/chunk-QBJ22YZR.js.map +1 -0
  42. package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
  43. package/dist/chunk-QGHGJ56O.js.map +1 -0
  44. package/dist/{chunk-JYRIOFEN.cjs → chunk-RIKBJLS2.cjs} +1064 -98
  45. package/dist/chunk-RIKBJLS2.cjs.map +1 -0
  46. package/dist/{chunk-U6NDQFOK.cjs → chunk-XDMXXC4Z.cjs} +569 -175
  47. package/dist/chunk-XDMXXC4Z.cjs.map +1 -0
  48. package/dist/commands/index.cjs +860 -0
  49. package/dist/commands/index.cjs.map +1 -0
  50. package/dist/commands/index.d.cts +592 -0
  51. package/dist/commands/index.d.ts +592 -0
  52. package/dist/commands/index.js +860 -0
  53. package/dist/commands/index.js.map +1 -0
  54. package/dist/{controller-OUdxPY-X.d.ts → controller-pePCJLT1.d.cts} +216 -11
  55. package/dist/{controller-OUdxPY-X.d.cts → controller-pePCJLT1.d.ts} +216 -11
  56. package/dist/extensions/index.cjs +14 -4
  57. package/dist/extensions/index.cjs.map +1 -1
  58. package/dist/extensions/index.d.cts +3 -3
  59. package/dist/extensions/index.d.ts +3 -3
  60. package/dist/extensions/index.js +13 -3
  61. package/dist/history/index.cjs +12 -2
  62. package/dist/history/index.cjs.map +1 -1
  63. package/dist/history/index.d.cts +211 -7
  64. package/dist/history/index.d.ts +211 -7
  65. package/dist/history/index.js +13 -3
  66. package/dist/image-editor/index.cjs +418 -13
  67. package/dist/image-editor/index.cjs.map +1 -1
  68. package/dist/image-editor/index.d.cts +154 -3
  69. package/dist/image-editor/index.d.ts +154 -3
  70. package/dist/image-editor/index.js +417 -12
  71. package/dist/image-editor/index.js.map +1 -1
  72. package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
  73. package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
  74. package/dist/index.cjs +275 -83
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.cts +296 -47
  77. package/dist/index.d.ts +296 -47
  78. package/dist/index.js +266 -74
  79. package/dist/index.js.map +1 -1
  80. package/dist/registry-D5-R90p0.d.cts +209 -0
  81. package/dist/registry-D5-R90p0.d.ts +209 -0
  82. package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
  83. package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
  84. package/dist/serializers/index.cjs +3 -3
  85. package/dist/serializers/index.d.cts +1 -1
  86. package/dist/serializers/index.d.ts +1 -1
  87. package/dist/serializers/index.js +2 -2
  88. package/dist/spreadsheet/index.cjs +23 -3
  89. package/dist/spreadsheet/index.cjs.map +1 -1
  90. package/dist/spreadsheet/index.d.cts +97 -10
  91. package/dist/spreadsheet/index.d.ts +97 -10
  92. package/dist/spreadsheet/index.js +22 -2
  93. package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
  94. package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
  95. package/dist/{types-DMe0vFB1.d.cts → types-DX4Mh4SL.d.cts} +1 -1
  96. package/dist/{types-DMe0vFB1.d.ts → types-DX4Mh4SL.d.ts} +1 -1
  97. package/dist/types-GT5a5YAX.d.cts +86 -0
  98. package/dist/types-GT5a5YAX.d.ts +86 -0
  99. package/package.json +8 -2
  100. package/dist/chunk-2TWGLC5K.cjs +0 -262
  101. package/dist/chunk-2TWGLC5K.cjs.map +0 -1
  102. package/dist/chunk-3TYMNULT.js.map +0 -1
  103. package/dist/chunk-4QPLEUL2.cjs.map +0 -1
  104. package/dist/chunk-A7DKIA72.js.map +0 -1
  105. package/dist/chunk-CYFJCKC4.js.map +0 -1
  106. package/dist/chunk-GVW4P7BG.js.map +0 -1
  107. package/dist/chunk-JYRIOFEN.cjs.map +0 -1
  108. package/dist/chunk-KAADNAPF.js +0 -262
  109. package/dist/chunk-KAADNAPF.js.map +0 -1
  110. package/dist/chunk-TVRC4KVT.cjs.map +0 -1
  111. package/dist/chunk-U6NDQFOK.cjs.map +0 -1
  112. package/dist/types-BZgcDsC5.d.cts +0 -42
  113. package/dist/types-BZgcDsC5.d.ts +0 -42
  114. /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
@@ -53,7 +53,7 @@ function tokenizeFormula(formula) {
53
53
  continue;
54
54
  }
55
55
  if (char === "#") {
56
- const match = /^#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|CYCLE!|ERROR!)/.exec(input.slice(i));
56
+ const match = /^#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|CYCLE!|ERROR!|SPILL!|CALC!)/.exec(input.slice(i));
57
57
  if (!match) {
58
58
  throw new Error(`Unexpected character '#' at position ${i + offset}`);
59
59
  }
@@ -104,7 +104,7 @@ function tokenizeFormula(formula) {
104
104
  i += 2;
105
105
  continue;
106
106
  }
107
- if (["+", "-", "*", "/", "^", "&", "=", "<", ">"].includes(char)) {
107
+ if (["+", "-", "*", "/", "^", "&", "=", "<", ">", "@"].includes(char)) {
108
108
  tokens.push({ type: "OPERATOR", value: char, position: i + offset });
109
109
  i++;
110
110
  continue;
@@ -144,6 +144,11 @@ function tokenizeFormula(formula) {
144
144
  continue;
145
145
  }
146
146
  if (/^\$?[A-Za-z]+\$?[0-9]+$/.test(rawWord)) {
147
+ if (input[i] === "#") {
148
+ i++;
149
+ tokens.push({ type: "SPILL_REF", value: upper.replace(/\$/g, ""), position: start + offset });
150
+ continue;
151
+ }
147
152
  tokens.push({
148
153
  type: "CELL_REF",
149
154
  value: upper.replace(/\$/g, ""),
@@ -265,7 +270,7 @@ var FormulaParser = (_class = class {
265
270
  return expr;
266
271
  }
267
272
  unary() {
268
- if (this.matchOperator("-", "+")) {
273
+ if (this.matchOperator("-", "+", "@")) {
269
274
  const op = this.previous().value;
270
275
  const argument = this.unary();
271
276
  return { type: "UnaryOp", op, argument };
@@ -304,6 +309,11 @@ var FormulaParser = (_class = class {
304
309
  this.advance();
305
310
  return parseRange(token.value);
306
311
  }
312
+ if (token.type === "SPILL_REF") {
313
+ this.advance();
314
+ const parsed = parseCellReference(token.value);
315
+ return { type: "SpillRef", ref: token.value, col: parsed.col, row: parsed.row };
316
+ }
307
317
  if (token.type === "PROPERTY_REF") {
308
318
  this.advance();
309
319
  return {
@@ -329,7 +339,7 @@ var FormulaParser = (_class = class {
329
339
  args
330
340
  };
331
341
  }
332
- return { type: "Literal", value: "#NAME?" };
342
+ return { type: "Name", name };
333
343
  }
334
344
  if (this.match("LPAREN")) {
335
345
  const expr = this.expression();
@@ -391,7 +401,9 @@ var FORMULA_ERRORS = /* @__PURE__ */ new Set([
391
401
  "#N/A",
392
402
  "#NUM!",
393
403
  "#CYCLE!",
394
- "#ERROR!"
404
+ "#ERROR!",
405
+ "#SPILL!",
406
+ "#CALC!"
395
407
  ]);
396
408
  function isFormulaError(value) {
397
409
  return typeof value === "string" && FORMULA_ERRORS.has(value);
@@ -402,9 +414,7 @@ function toNumber(value) {
402
414
  if (isBlank(value)) return 0;
403
415
  if (typeof value === "boolean") return value ? 1 : 0;
404
416
  if (typeof value === "number") return value;
405
- const trimmed = String(value).trim();
406
- const num = trimmed === "" ? NaN : Number(trimmed);
407
- return isNaN(num) ? "#VALUE!" : num;
417
+ return _nullishCoalesce(parseNumberText(String(value)), () => ( "#VALUE!"));
408
418
  }
409
419
  function toBoolean(value) {
410
420
  if (isFormulaError(value)) return value;
@@ -416,7 +426,26 @@ function toBoolean(value) {
416
426
  if (upper === "FALSE") return false;
417
427
  return "#VALUE!";
418
428
  }
419
- var toText = (value) => isBlank(value) ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
429
+ var toText = (value) => isBlank(value) ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : typeof value === "number" ? numberToText(value) : String(value);
430
+ function numberToText(n) {
431
+ if (n === 0 || !Number.isFinite(n)) return "0";
432
+ const a = Math.abs(n);
433
+ if ((a < 1e-9 || a >= 1e15) && Math.round(a) !== a || a >= 1e15) return sciText(n, 15);
434
+ let mag = Math.floor(Math.log10(a));
435
+ if (a >= Math.pow(10, mag + 1)) mag++;
436
+ const decimals = Math.min(Math.max(14 - mag, 0), 30);
437
+ return trimDecimal(n.toFixed(decimals));
438
+ }
439
+ function trimDecimal(s) {
440
+ if (!s.includes(".")) return s;
441
+ const t = s.replace(/0+$/, "").replace(/\.$/, "");
442
+ return t === "-0" ? "0" : t;
443
+ }
444
+ function sciText(n, sig) {
445
+ const [mantissa, exp] = n.toExponential(Math.max(sig - 1, 0)).split("e");
446
+ const e = Number(exp);
447
+ return `${trimDecimal(mantissa)}E${e < 0 ? "-" : "+"}${String(Math.abs(e)).padStart(2, "0")}`;
448
+ }
420
449
  function numericValue(value) {
421
450
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
422
451
  if (typeof value !== "string" || FORMULA_ERRORS.has(value)) return null;
@@ -429,17 +458,68 @@ function normalizeValue(value) {
429
458
  const num = typeof value === "string" ? numericValue(value) : null;
430
459
  return num === null ? value : num;
431
460
  }
461
+ var ERROR_ORDER = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A", "#SPILL!", "#CALC!", "#CYCLE!", "#ERROR!"];
462
+ function valueRank(value) {
463
+ if (isBlank(value)) return 4;
464
+ if (typeof value === "number") return 0;
465
+ if (typeof value === "boolean") return 2;
466
+ if (typeof value === "string" && FORMULA_ERRORS.has(value)) return 3;
467
+ return 1;
468
+ }
469
+ function compareNumbers(a, b) {
470
+ if (a === b) return 0;
471
+ if (Math.abs(a - b) > Math.max(Math.abs(a), Math.abs(b)) * 1e-13) return a < b ? -1 : 1;
472
+ const round15 = (x2) => !Number.isFinite(x2) || x2 === 0 ? x2 : Number(x2.toExponential(14));
473
+ const x = round15(a);
474
+ const y = round15(b);
475
+ return x < y ? -1 : x > y ? 1 : 0;
476
+ }
477
+ function compareText(a, b) {
478
+ const x = a.toLowerCase();
479
+ const y = b.toLowerCase();
480
+ return x < y ? -1 : x > y ? 1 : 0;
481
+ }
432
482
  function compareValues(left, right) {
433
- const rank = (v) => typeof v === "boolean" ? 2 : typeof v === "string" ? 1 : 0;
434
- const l = isBlank(left) ? typeof right === "string" ? "" : 0 : left;
435
- const r = isBlank(right) ? typeof l === "string" ? "" : 0 : right;
436
- if (rank(l) !== rank(r)) return rank(l) - rank(r);
437
- if (typeof l === "string") {
438
- const a = l.toLowerCase();
439
- const b = r.toLowerCase();
440
- return a < b ? -1 : a > b ? 1 : 0;
441
- }
442
- return Number(l) - Number(r);
483
+ const lRank = valueRank(left);
484
+ const rRank = valueRank(right);
485
+ if (lRank === 4 || rRank === 4) {
486
+ if (lRank === rRank) return 0;
487
+ const blankFirst = lRank === 4;
488
+ const other = blankFirst ? right : left;
489
+ const otherRank = blankFirst ? rRank : lRank;
490
+ let cmp;
491
+ if (otherRank === 0) cmp = compareNumbers(0, other);
492
+ else if (otherRank === 1) cmp = String(other) === "" ? 0 : -1;
493
+ else if (otherRank === 2) cmp = other ? -1 : 0;
494
+ else return lRank - rRank;
495
+ return blankFirst ? cmp : -cmp;
496
+ }
497
+ if (lRank === 1 && rRank === 1) {
498
+ const a = String(left);
499
+ const b = String(right);
500
+ const ta = isoDateTime(a);
501
+ const tb = ta === null ? null : isoDateTime(b);
502
+ if (ta !== null && tb !== null) return Math.sign(ta - tb);
503
+ return compareText(a, b);
504
+ }
505
+ if (lRank === 0 && rRank === 1 || lRank === 1 && rRank === 0) {
506
+ const date = parseIsoDate(lRank === 1 ? left : right);
507
+ if (date) {
508
+ const serial = dateToSerial(date);
509
+ return lRank === 0 ? compareNumbers(left, serial) : compareNumbers(serial, right);
510
+ }
511
+ }
512
+ if (lRank !== rRank) return lRank - rRank;
513
+ if (lRank === 0) return compareNumbers(left, right);
514
+ if (lRank === 2) return Number(left) - Number(right);
515
+ return Math.sign(ERROR_ORDER.indexOf(left) - ERROR_ORDER.indexOf(right));
516
+ }
517
+ function sortCompareValues(left, right) {
518
+ const lBlank = isBlank(left);
519
+ const rBlank = isBlank(right);
520
+ if (lBlank || rBlank) return lBlank === rBlank ? 0 : lBlank ? 1 : -1;
521
+ if (isFormulaError(left) && isFormulaError(right)) return 0;
522
+ return compareValues(left, right);
443
523
  }
444
524
  function roundHalfAwayFromZero(value, decimals) {
445
525
  const factor = Math.pow(10, decimals);
@@ -505,6 +585,20 @@ function toDate(value) {
505
585
  }
506
586
  return null;
507
587
  }
588
+ var ISO_DATE = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/;
589
+ function parseIsoDate(value) {
590
+ if (typeof value !== "string" || value.length < 8 || value.length > 19) return null;
591
+ const iso = ISO_DATE.exec(value.trim());
592
+ if (!iso) return null;
593
+ const [, y, m, d, hh = "0", mm = "0", ss = "0"] = iso;
594
+ const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));
595
+ return date.getMonth() === Number(m) - 1 && date.getDate() === Number(d) ? date : null;
596
+ }
597
+ var isoDateHasTime = (value) => /[ T]\d/.test(value.trim());
598
+ function isoDateTime(value) {
599
+ if (value.length < 8 || value.charCodeAt(4) !== 45) return null;
600
+ return _nullishCoalesce(_optionalChain([parseIsoDate, 'call', _2 => _2(value), 'optionalAccess', _3 => _3.getTime, 'call', _4 => _4()]), () => ( null));
601
+ }
508
602
  function dateToSerial(date) {
509
603
  const utc = Date.UTC(
510
604
  date.getFullYear(),
@@ -534,6 +628,282 @@ var daysBetween = (start, end) => Math.round(
534
628
  (Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()) - Date.UTC(start.getFullYear(), start.getMonth(), start.getDate())) / MS_PER_DAY
535
629
  );
536
630
 
631
+ // src/spreadsheet/formula/parse-input.ts
632
+ var MONTH_NAMES = [
633
+ "January",
634
+ "February",
635
+ "March",
636
+ "April",
637
+ "May",
638
+ "June",
639
+ "July",
640
+ "August",
641
+ "September",
642
+ "October",
643
+ "November",
644
+ "December"
645
+ ];
646
+ function daysFromCivil(year, month, day) {
647
+ const y = month <= 2 ? year - 1 : year;
648
+ const era = Math.floor((y >= 0 ? y : y - 399) / 400);
649
+ const yoe = y - era * 400;
650
+ const doy = Math.floor((153 * (month > 2 ? month - 3 : month + 9) + 2) / 5) + day - 1;
651
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
652
+ return era * 146097 + doe - 719468;
653
+ }
654
+ var isLeapYear = (y) => y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
655
+ function daysInMonth(year, month) {
656
+ if (month === 2) return isLeapYear(year) ? 29 : 28;
657
+ if ([4, 6, 9, 11].includes(month)) return 30;
658
+ return month >= 1 && month <= 12 ? 31 : 0;
659
+ }
660
+ var daysInMonth1900 = (year, month) => year === 1900 && month === 2 ? 29 : daysInMonth(year, month);
661
+ var UNIX_1899_12_30 = -25569;
662
+ var MAX_SERIAL_1900 = 2958465;
663
+ function serialFromYmd(year, month, day) {
664
+ if (![year, month, day].every(Number.isSafeInteger)) return null;
665
+ let y = year >= 0 && year < 1900 ? year + 1900 : year;
666
+ const m0 = month - 1;
667
+ y += Math.floor(m0 / 12);
668
+ const m = (m0 % 12 + 12) % 12 + 1;
669
+ if (y < 1900 || y > 9999) return null;
670
+ const first = daysFromCivil(y, m, 1) - UNIX_1899_12_30 - (y === 1900 && m <= 2 ? 1 : 0);
671
+ const serial = first + day - 1;
672
+ return serial >= 0 && serial <= MAX_SERIAL_1900 ? serial : null;
673
+ }
674
+ var timeFraction = (h, m, s) => (h * 3600 + m * 60 + s) / 86400;
675
+ var currentYear = () => (/* @__PURE__ */ new Date()).getFullYear();
676
+ function parseCellInput(input) {
677
+ if (input === "") return { value: "", type: "empty" };
678
+ if (input.startsWith("'")) return { value: input.slice(1), type: "text" };
679
+ const t = input.trim();
680
+ const upper = t.toUpperCase();
681
+ if (upper === "TRUE") return { value: true, type: "boolean" };
682
+ if (upper === "FALSE") return { value: false, type: "boolean" };
683
+ if (FORMULA_ERRORS.has(upper)) return { value: upper, type: "error" };
684
+ const numeric = parseNumeric(t);
685
+ if (numeric) {
686
+ if (numeric.kind === "date") {
687
+ const iso = isoText(numeric.date);
688
+ if (iso !== null) {
689
+ return { value: iso, type: "date", numberFormat: numeric.format, formatKind: "date", cellFormat: "date" };
690
+ }
691
+ } else {
692
+ const formatKind = numeric.kind === "time" ? "time" : formatKindOf(numeric.format);
693
+ return {
694
+ value: normalizeZero(numeric.value),
695
+ type: "number",
696
+ ...numeric.format ? { numberFormat: numeric.format } : {},
697
+ ...formatKind ? { formatKind } : {},
698
+ ...formatKind && formatKind !== "time" ? { cellFormat: formatKind } : {}
699
+ };
700
+ }
701
+ }
702
+ return { value: input, type: "text" };
703
+ }
704
+ function parseNumberText(text) {
705
+ const numeric = parseNumeric(text.trim());
706
+ if (!numeric) return null;
707
+ if (numeric.kind !== "date") return normalizeZero(numeric.value);
708
+ const { year, month, day, time = 0 } = numeric.date;
709
+ const serial = serialFromYmd(year, month, day);
710
+ return serial === null ? null : serial + time;
711
+ }
712
+ var normalizeZero = (n) => n === 0 ? 0 : n;
713
+ function formatKindOf(format) {
714
+ if (!format) return void 0;
715
+ if (format.includes("$")) return "currency";
716
+ if (format.includes("%")) return "percentage";
717
+ return "number";
718
+ }
719
+ var pad2 = (n, width = 2) => String(n).padStart(width, "0");
720
+ function isoText({ year, month, day, time }) {
721
+ if (day > daysInMonth(year, month)) return null;
722
+ const date = `${pad2(year, 4)}-${pad2(month)}-${pad2(day)}`;
723
+ if (time === void 0) return date;
724
+ let seconds = Math.round(time * 86400);
725
+ if (seconds >= 86400) seconds = 86399;
726
+ return `${date} ${pad2(Math.floor(seconds / 3600))}:${pad2(Math.floor(seconds / 60) % 60)}:${pad2(seconds % 60)}`;
727
+ }
728
+ function parseNumeric(t) {
729
+ if (t === "") return null;
730
+ if (t.startsWith("(") && t.endsWith(")") && t.length >= 2) {
731
+ const inner = parseNumeric(t.slice(1, -1).trim());
732
+ if (!inner || inner.kind === "date") return null;
733
+ return { ...inner, value: -inner.value };
734
+ }
735
+ const plain = parsePlain(t);
736
+ if (plain) return plain;
737
+ const fraction = parseFraction(t);
738
+ if (fraction !== null) return { kind: "number", value: fraction, format: "# ?/?" };
739
+ return parseDateTime(t);
740
+ }
741
+ var startsWithDigitOrDot = (s) => /^[0-9.]/.test(s);
742
+ function parsePlain(t) {
743
+ let s = t;
744
+ let neg = false;
745
+ let currency = false;
746
+ let percent = false;
747
+ if (s.startsWith("-")) {
748
+ neg = true;
749
+ s = s.slice(1).trimStart();
750
+ } else if (s.startsWith("+")) {
751
+ s = s.slice(1).trimStart();
752
+ }
753
+ if (s.startsWith("$")) {
754
+ currency = true;
755
+ s = s.slice(1).trimStart();
756
+ if (!neg && s.startsWith("-")) {
757
+ neg = true;
758
+ s = s.slice(1);
759
+ }
760
+ }
761
+ if (s.endsWith("%")) {
762
+ percent = true;
763
+ s = s.slice(0, -1).trimEnd();
764
+ }
765
+ if (s === "") return null;
766
+ const hasSep = s.includes(",");
767
+ let digits;
768
+ if (hasSep) {
769
+ const dot = s.indexOf(".");
770
+ const int = dot === -1 ? s : s.slice(0, dot);
771
+ const frac = dot === -1 ? null : s.slice(dot + 1);
772
+ const groups = int.split(",");
773
+ const first = groups[0];
774
+ if (first === "" || first.length > 3 || groups.slice(1).some((g) => g.length !== 3)) return null;
775
+ digits = groups.join("") + (frac !== null ? `.${frac}` : "");
776
+ } else {
777
+ digits = s;
778
+ }
779
+ if (!/^[0-9.eE+-]+$/.test(digits) || !startsWithDigitOrDot(digits)) return null;
780
+ if (!/^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(digits)) return null;
781
+ let n = Number(digits);
782
+ if (!Number.isFinite(n)) return null;
783
+ const sci = /[eE]/.test(digits);
784
+ if (percent) n /= 100;
785
+ if (neg) n = -n;
786
+ const hasDot = digits.includes(".");
787
+ const format = currency ? hasDot ? '"$"#,##0.00' : '"$"#,##0' : percent ? hasDot ? "0.00%" : "0%" : sci ? "0.00E+00" : hasSep ? hasDot ? "#,##0.00" : "#,##0" : void 0;
788
+ return { kind: "number", value: n, ...format ? { format } : {} };
789
+ }
790
+ var parseDecimal = (s) => /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s) ? Number(s) : null;
791
+ function parseFraction(t) {
792
+ const space = t.indexOf(" ");
793
+ if (space === -1) return null;
794
+ const whole = t.slice(0, space);
795
+ const frac = t.slice(space + 1).trim();
796
+ const neg = whole.startsWith("-");
797
+ const w = parseDecimal(whole.replace(/^-+/, ""));
798
+ const slash = frac.indexOf("/");
799
+ if (w === null || slash === -1) return null;
800
+ const a = parseDecimal(frac.slice(0, slash));
801
+ const b = parseDecimal(frac.slice(slash + 1));
802
+ if (a === null || b === null || b === 0 || !Number.isFinite(w)) return null;
803
+ const v = w + a / b;
804
+ return neg ? -v : v;
805
+ }
806
+ function monthFromName(s) {
807
+ const l = s.toLowerCase();
808
+ if (l.length < 3) return null;
809
+ const index = MONTH_NAMES.findIndex((m) => m.toLowerCase().startsWith(l));
810
+ return index === -1 ? null : index + 1;
811
+ }
812
+ function parseTime(input) {
813
+ const lower = input.trim().toLowerCase();
814
+ let body = lower;
815
+ let pm = null;
816
+ for (const [suffix, isPm] of [["am", false], ["a", false], ["pm", true], ["p", true]]) {
817
+ if (lower.endsWith(suffix)) {
818
+ body = lower.slice(0, -suffix.length).trim();
819
+ pm = isPm;
820
+ break;
821
+ }
822
+ }
823
+ const parts = body.split(":");
824
+ if (parts.length === 1 && pm === null) return null;
825
+ if (parts.length > 3 || parts.some((p) => p === "")) return null;
826
+ const num = (p) => p === void 0 ? 0 : parseDecimal(p);
827
+ let h = num(parts[0]);
828
+ const m = num(parts[1]);
829
+ const s = num(parts[2]);
830
+ if (h === null || m === null || s === null) return null;
831
+ if (m >= 60 || s >= 60 || h < 0 || m < 0 || s < 0) return null;
832
+ if (pm !== null) {
833
+ if (h > 12) return null;
834
+ if (h === 12) h = 0;
835
+ if (pm) h += 12;
836
+ } else if (h >= 1e4) {
837
+ return null;
838
+ }
839
+ return { fraction: timeFraction(h, m, s), seconds: parts.length === 3, ampm: pm !== null };
840
+ }
841
+ var expandYear = (text, y) => text.length <= 2 ? y < 30 ? 2e3 + y : 1900 + y : y;
842
+ function validYmd(year, month, day) {
843
+ if (month < 1 || month > 12 || day < 1 || year < 1900 || year > 9999) return null;
844
+ if (day > daysInMonth1900(year, month)) return null;
845
+ return { year, month, day };
846
+ }
847
+ function parseDate(input) {
848
+ const t = input.trim().replace(/,+$/, "");
849
+ const num = (s) => /^[+-]?\d+$/.test(s.trim()) ? Number(s.trim()) : null;
850
+ const withFormat = (date, format) => date ? { date, format } : null;
851
+ const parts = t.split(/[/.-]/);
852
+ if (parts.length === 3) {
853
+ const [a, b, c] = parts.map((p) => p.trim());
854
+ if (a.length === 4) {
855
+ const [y2, m2, d2] = [num(a), num(b), num(c)];
856
+ if (y2 !== null && m2 !== null && d2 !== null) return withFormat(validYmd(y2, m2, d2), "yyyy-mm-dd");
857
+ }
858
+ {
859
+ const [m2, d2, y2] = [num(a), num(b), num(c)];
860
+ if (m2 !== null && d2 !== null && y2 !== null) return withFormat(validYmd(expandYear(c, y2), m2, d2), "m/d/yyyy");
861
+ }
862
+ const [d, m, y] = [num(a), monthFromName(b), num(c)];
863
+ if (d !== null && m !== null && y !== null) return withFormat(validYmd(expandYear(c, y), m, d), "d-mmm-yy");
864
+ }
865
+ if (parts.length === 2) {
866
+ const [a, b] = parts.map((p) => p.trim());
867
+ const [x, y] = [num(a), num(b)];
868
+ if (x !== null && y !== null) {
869
+ if (x >= 1 && x <= 12 && y >= 1 && y <= 31) return withFormat(validYmd(currentYear(), x, y), "d-mmm");
870
+ if (x >= 1 && x <= 12 && b.length === 4) return withFormat(validYmd(y, x, 1), "mmm-yy");
871
+ }
872
+ const dayA = num(a);
873
+ const monthB = monthFromName(b);
874
+ if (dayA !== null && monthB !== null) return withFormat(validYmd(currentYear(), monthB, dayA), "d-mmm");
875
+ const monthA = monthFromName(a);
876
+ const yearB = num(b);
877
+ if (monthA !== null && yearB !== null) return withFormat(validYmd(expandYear(b, yearB), monthA, 1), "mmm-yy");
878
+ }
879
+ const words = t.split(/[ ,]/).filter((w) => w !== "");
880
+ if (words.length === 3) {
881
+ const [w0, w1, w2] = words;
882
+ let [m, d, y] = [monthFromName(w0), num(w1), num(w2)];
883
+ if (m !== null && d !== null && y !== null) return withFormat(validYmd(expandYear(w2, y), m, d), "d-mmm-yy");
884
+ [d, m, y] = [num(w0), monthFromName(w1), num(w2)];
885
+ if (d !== null && m !== null && y !== null) return withFormat(validYmd(expandYear(w2, y), m, d), "d-mmm-yy");
886
+ }
887
+ return null;
888
+ }
889
+ function parseDateTime(t) {
890
+ const time = parseTime(t);
891
+ if (time) {
892
+ const format = time.fraction >= 1 ? "[h]:mm:ss" : time.seconds ? time.ampm ? "h:mm:ss AM/PM" : "h:mm:ss" : time.ampm ? "h:mm AM/PM" : "h:mm";
893
+ return { kind: "time", value: time.fraction, format };
894
+ }
895
+ const date = parseDate(t);
896
+ if (date) return { kind: "date", date: date.date, format: date.format };
897
+ for (let i = t.indexOf(" "); i !== -1; i = t.indexOf(" ", i + 1)) {
898
+ const d = parseDate(t.slice(0, i));
899
+ const tm = d ? parseTime(t.slice(i + 1)) : null;
900
+ if (d && tm && tm.fraction < 1) {
901
+ return { kind: "date", date: { ...d.date, time: tm.fraction }, format: "m/d/yyyy h:mm" };
902
+ }
903
+ }
904
+ return null;
905
+ }
906
+
537
907
  // src/spreadsheet/formula/criteria.ts
538
908
  function wildcardToRegExp(pattern) {
539
909
  let source = "";
@@ -570,12 +940,17 @@ function parseCriteria(criterion) {
570
940
  if (op === "<>") return (value) => !isBlank(value);
571
941
  return () => false;
572
942
  }
573
- const operandNumber = numericValue(operand);
943
+ const parsed = parseCellInput(operand.trim());
944
+ const operandNumber = _nullishCoalesce(numericValue(operand), () => ( (parsed.type === "number" ? parsed.value : null)));
945
+ const operandDate = parsed.type === "date" ? parsed.value : null;
946
+ const isDateValue = (value) => typeof value === "number" || parseIsoDate(value) !== null;
574
947
  const operandUpper = operand.trim().toUpperCase();
575
948
  const operandBoolean = operandUpper === "TRUE" ? true : operandUpper === "FALSE" ? false : null;
576
949
  if (op === "=" || op === "<>") {
577
950
  let equals;
578
- if (operandNumber !== null) {
951
+ if (operandDate !== null) {
952
+ equals = (value) => isDateValue(value) && compareValues(value, operandDate) === 0;
953
+ } else if (operandNumber !== null) {
579
954
  equals = (value) => numericValue(value) === operandNumber;
580
955
  } else if (operandBoolean !== null) {
581
956
  equals = (value) => value === operandBoolean || typeof value === "string" && value.trim().toUpperCase() === operandUpper;
@@ -588,6 +963,9 @@ function parseCriteria(criterion) {
588
963
  return op === "=" ? (value) => !isFormulaError(value) && equals(value) : (value) => !isFormulaError(value) && !equals(value);
589
964
  }
590
965
  const test = (cmp) => op === "<" ? cmp < 0 : op === "<=" ? cmp <= 0 : op === ">" ? cmp > 0 : cmp >= 0;
966
+ if (operandDate !== null) {
967
+ return (value) => isDateValue(value) && test(compareValues(value, operandDate));
968
+ }
591
969
  if (operandNumber !== null) {
592
970
  return (value) => {
593
971
  const num = numericValue(value);
@@ -605,6 +983,42 @@ function parseCriteria(criterion) {
605
983
  }
606
984
 
607
985
  // src/spreadsheet/formula/format.ts
986
+ function formatCellDisplay(cell) {
987
+ if (!cell) return "";
988
+ if (cell.error) return cell.error;
989
+ const { value, numberFormat } = cell;
990
+ if (numberFormat && value !== "" && value !== null && value !== void 0 && typeof value !== "boolean") {
991
+ if (/^\[h\]/i.test(numberFormat) && typeof value === "number") return formatElapsedHours(value);
992
+ if (numberFormat === "# ?/?" && typeof value === "number") return formatFraction(value);
993
+ const formatted = formatValueWithCode(value, numberFormat);
994
+ if (formatted !== null) return formatted;
995
+ }
996
+ return toText(value);
997
+ }
998
+ function formatFraction(value) {
999
+ const sign = value < 0 ? "-" : "";
1000
+ const abs = Math.abs(value);
1001
+ let whole = Math.floor(abs);
1002
+ const frac = abs - whole;
1003
+ let best = { num: 0, den: 1, err: frac };
1004
+ for (let den = 2; den <= 9; den++) {
1005
+ const num = Math.round(frac * den);
1006
+ const err = Math.abs(frac - num / den);
1007
+ if (err < best.err - 1e-12) best = { num, den, err };
1008
+ }
1009
+ if (best.num === best.den) {
1010
+ whole += 1;
1011
+ best = { num: 0, den: 1, err: 0 };
1012
+ }
1013
+ if (best.num === 0) return `${sign}${whole}`;
1014
+ return `${sign}${whole === 0 ? "" : `${whole} `}${best.num}/${best.den}`;
1015
+ }
1016
+ function formatElapsedHours(days) {
1017
+ const total = Math.round(Math.abs(days) * 86400);
1018
+ const p = (n) => String(n).padStart(2, "0");
1019
+ const text = `${Math.floor(total / 3600)}:${p(Math.floor(total / 60) % 60)}:${p(total % 60)}`;
1020
+ return days < 0 ? `-${text}` : text;
1021
+ }
608
1022
  function formatValueWithCode(value, format) {
609
1023
  if (isDateFormat(format)) {
610
1024
  const date = toDate(value);
@@ -719,7 +1133,7 @@ function splitSections(format) {
719
1133
  }
720
1134
  var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
721
1135
  var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
722
- var pad2 = (n) => String(n).padStart(2, "0");
1136
+ var pad3 = (n) => String(n).padStart(2, "0");
723
1137
  function formatDate(date, format) {
724
1138
  const parts = [];
725
1139
  for (const token of tokenize(format)) {
@@ -746,30 +1160,30 @@ function formatDate(date, format) {
746
1160
  return code[0] === code[0].toLowerCase() ? text.toLowerCase() : text;
747
1161
  }
748
1162
  if (lower.startsWith("y")) {
749
- return lower.length <= 2 ? pad2(date.getFullYear() % 100) : String(date.getFullYear());
1163
+ return lower.length <= 2 ? pad3(date.getFullYear() % 100) : String(date.getFullYear());
750
1164
  }
751
1165
  if (lower.startsWith("d")) {
752
1166
  if (lower.length >= 4) return DAYS[date.getDay()];
753
1167
  if (lower.length === 3) return DAYS[date.getDay()].slice(0, 3);
754
- return lower.length === 2 ? pad2(date.getDate()) : String(date.getDate());
1168
+ return lower.length === 2 ? pad3(date.getDate()) : String(date.getDate());
755
1169
  }
756
1170
  if (lower.startsWith("h")) {
757
1171
  let hours = date.getHours();
758
1172
  if (twelveHour) hours = hours % 12 || 12;
759
- return lower.length >= 2 ? pad2(hours) : String(hours);
1173
+ return lower.length >= 2 ? pad3(hours) : String(hours);
760
1174
  }
761
1175
  if (lower.startsWith("s")) {
762
- return lower.length >= 2 ? pad2(date.getSeconds()) : String(date.getSeconds());
1176
+ return lower.length >= 2 ? pad3(date.getSeconds()) : String(date.getSeconds());
763
1177
  }
764
1178
  const position = codes.findIndex((c) => c.i === index);
765
- const prev = _nullishCoalesce(_optionalChain([codes, 'access', _2 => _2[position - 1], 'optionalAccess', _3 => _3.text, 'access', _4 => _4.toLowerCase, 'call', _5 => _5()]), () => ( ""));
766
- const next = _nullishCoalesce(_optionalChain([codes, 'access', _6 => _6[position + 1], 'optionalAccess', _7 => _7.text, 'access', _8 => _8.toLowerCase, 'call', _9 => _9()]), () => ( ""));
1179
+ const prev = _nullishCoalesce(_optionalChain([codes, 'access', _5 => _5[position - 1], 'optionalAccess', _6 => _6.text, 'access', _7 => _7.toLowerCase, 'call', _8 => _8()]), () => ( ""));
1180
+ const next = _nullishCoalesce(_optionalChain([codes, 'access', _9 => _9[position + 1], 'optionalAccess', _10 => _10.text, 'access', _11 => _11.toLowerCase, 'call', _12 => _12()]), () => ( ""));
767
1181
  if (lower.length <= 2 && (prev.startsWith("h") || next.startsWith("s"))) {
768
- return lower.length === 2 ? pad2(date.getMinutes()) : String(date.getMinutes());
1182
+ return lower.length === 2 ? pad3(date.getMinutes()) : String(date.getMinutes());
769
1183
  }
770
1184
  if (lower.length >= 4) return MONTHS[date.getMonth()];
771
1185
  if (lower.length === 3) return MONTHS[date.getMonth()].slice(0, 3);
772
- return lower.length === 2 ? pad2(date.getMonth() + 1) : String(date.getMonth() + 1);
1186
+ return lower.length === 2 ? pad3(date.getMonth() + 1) : String(date.getMonth() + 1);
773
1187
  }).join("");
774
1188
  }
775
1189
 
@@ -1606,11 +2020,11 @@ var TEXT_FUNCTIONS = [
1606
2020
  {
1607
2021
  name: "TRIM",
1608
2022
  category: "text",
1609
- description: "Removes leading and trailing spaces.",
2023
+ description: "Removes leading and trailing spaces and collapses runs of inner spaces to one, like Excel.",
1610
2024
  args: [TEXT],
1611
2025
  evaluate: (ctx) => {
1612
2026
  const text = ctx.text(0);
1613
- return failed(text) ? text : text.trim();
2027
+ return failed(text) ? text : text.trim().replace(/ {2,}/g, " ");
1614
2028
  }
1615
2029
  },
1616
2030
  {
@@ -1775,7 +2189,7 @@ var TEXT_FUNCTIONS = [
1775
2189
  if (isFormulaError(value)) return value;
1776
2190
  if (typeof value === "number") return value;
1777
2191
  if (isBlank(value)) return 0;
1778
- const num = parseNumericText(toText(value));
2192
+ const num = _nullishCoalesce(parseNumericText(toText(value)), () => ( parseNumberText(toText(value))));
1779
2193
  if (num === null) return ctx.argError(0, "#VALUE!", `("${toText(value)}") can't be converted to a number.`);
1780
2194
  return num;
1781
2195
  }
@@ -2241,6 +2655,335 @@ var INFORMATION_FUNCTIONS = [
2241
2655
  }
2242
2656
  ];
2243
2657
 
2658
+ // src/spreadsheet/formula/functions/array.ts
2659
+ var MAX_ARRAY_CELLS = 1e6;
2660
+ function rangeArg(ctx, index) {
2661
+ const value = ctx.value(index);
2662
+ if (Array.isArray(value)) return toFormulaRange(value);
2663
+ if (isFormulaError(value)) return value;
2664
+ return { rows: 1, cols: 1, values: [value] };
2665
+ }
2666
+ function fromRows(rows, cols) {
2667
+ const out = [];
2668
+ for (let c = 0; c < cols; c++) {
2669
+ for (const row of rows) out.push(row[c]);
2670
+ }
2671
+ return withRangeShape(out, rows.length, cols);
2672
+ }
2673
+ var rowOf = (range, r) => {
2674
+ const row = [];
2675
+ for (let c = 0; c < range.cols; c++) row.push(rangeGet(range, r, c));
2676
+ return row;
2677
+ };
2678
+ var colOf = (range, c) => {
2679
+ const col = [];
2680
+ for (let r = 0; r < range.rows; r++) col.push(rangeGet(range, r, c));
2681
+ return col;
2682
+ };
2683
+ var linesOf = (range, byCol) => byCol ? Array.from({ length: range.cols }, (_, c) => colOf(range, c)) : Array.from({ length: range.rows }, (_, r) => rowOf(range, r));
2684
+ function fromLines(lines, byCol, width) {
2685
+ if (!byCol) return fromRows(lines, width);
2686
+ const out = [];
2687
+ for (const line of lines) for (const v of line) out.push(v);
2688
+ return withRangeShape(out, width, lines.length);
2689
+ }
2690
+ function compareForSort(a, b, order) {
2691
+ const blankA = isBlank(a);
2692
+ const blankB = isBlank(b);
2693
+ if (blankA || blankB) return blankA === blankB ? 0 : blankA ? 1 : -1;
2694
+ return compareValues(normalizeValue(a), normalizeValue(b)) * order;
2695
+ }
2696
+ var sortOrder = (ctx, index) => {
2697
+ const order = ctx.integer(index, 1);
2698
+ if (failed(order)) return order;
2699
+ return order === 1 || order === -1 ? order : ctx.argError(index, "#VALUE!", "must be 1 (ascending) or -1 (descending).");
2700
+ };
2701
+ function includeFlag(value) {
2702
+ if (isFormulaError(value)) return value;
2703
+ const flag = toBoolean(value);
2704
+ return isFormulaError(flag) ? "#VALUE!" : flag;
2705
+ }
2706
+ var ARRAY_FUNCTIONS = [
2707
+ {
2708
+ name: "FILTER",
2709
+ category: "lookup",
2710
+ description: "Returns the rows (or columns) of an array whose include value is TRUE. The result spills.",
2711
+ args: [
2712
+ arg("array", "The range or array to filter.", "range"),
2713
+ arg("include", "TRUE/FALSE for each row (a column as tall as array) or each column (a row as wide as array)."),
2714
+ optional("if_empty", "Returned when nothing matches (default #CALC!).")
2715
+ ],
2716
+ evaluate: (ctx) => {
2717
+ const range = rangeArg(ctx, 0);
2718
+ if (failed(range)) return range;
2719
+ const include = rangeArg(ctx, 1);
2720
+ if (failed(include)) return include;
2721
+ let byCol;
2722
+ if (include.cols === 1 && include.rows === range.rows) byCol = false;
2723
+ else if (include.rows === 1 && include.cols === range.cols) byCol = true;
2724
+ else {
2725
+ return ctx.argError(1, "#VALUE!", "must be a single column as tall as argument 1 (or a single row as wide).");
2726
+ }
2727
+ const lines = linesOf(range, byCol);
2728
+ const kept = [];
2729
+ for (let i = 0; i < lines.length; i++) {
2730
+ const flag = includeFlag(include.values[i]);
2731
+ if (typeof flag === "string") {
2732
+ return flag === "#VALUE!" ? ctx.argError(1, "#VALUE!", `has a value that isn't TRUE or FALSE (${include.values[i]}).`) : flag;
2733
+ }
2734
+ if (flag) kept.push(lines[i]);
2735
+ }
2736
+ if (kept.length === 0) {
2737
+ return ctx.has(2) ? ctx.value(2) : ctx.error("#CALC!", "FILTER found no matching values.");
2738
+ }
2739
+ return fromLines(kept, byCol, byCol ? range.rows : range.cols);
2740
+ }
2741
+ },
2742
+ {
2743
+ name: "SORT",
2744
+ category: "lookup",
2745
+ description: "Sorts the rows (or columns) of an array by one of its columns (or rows). The result spills.",
2746
+ args: [
2747
+ arg("array", "The range or array to sort.", "range"),
2748
+ optional("sort_index", "The column (or row, with by_col) to sort by; 1 = first (default).", "number"),
2749
+ optional("sort_order", "1 = ascending (default), -1 = descending.", "number"),
2750
+ optional("by_col", "TRUE sorts columns by a row; FALSE (default) sorts rows by a column.", "logical")
2751
+ ],
2752
+ evaluate: (ctx) => {
2753
+ const range = rangeArg(ctx, 0);
2754
+ if (failed(range)) return range;
2755
+ const index = ctx.integer(1, 1);
2756
+ if (failed(index)) return index;
2757
+ const order = sortOrder(ctx, 2);
2758
+ if (failed(order)) return order;
2759
+ const byCol = ctx.boolean(3, false);
2760
+ if (failed(byCol)) return byCol;
2761
+ const width = byCol ? range.rows : range.cols;
2762
+ if (index < 1 || index > width) {
2763
+ return ctx.argError(1, "#VALUE!", `must be between 1 and ${width}.`);
2764
+ }
2765
+ const lines = linesOf(range, byCol).map((line, position) => ({ line, position })).sort((a, b) => compareForSort(a.line[index - 1], b.line[index - 1], order) || a.position - b.position).map((entry) => entry.line);
2766
+ return fromLines(lines, byCol, width);
2767
+ }
2768
+ },
2769
+ {
2770
+ name: "SORTBY",
2771
+ category: "lookup",
2772
+ description: "Sorts an array by the values in other ranges (one value per row or column). The result spills.",
2773
+ args: [
2774
+ arg("array", "The range or array to sort.", "range"),
2775
+ arg("by_array", "A column as tall as array (or a row as wide) to sort by.", "range"),
2776
+ optional("sort_order", "1 = ascending (default), -1 = descending.", "number"),
2777
+ arg("by_array", "Another range to sort by, for ties.", "range", { repeating: true, optional: true }),
2778
+ arg("sort_order", "Its order: 1 = ascending (default), -1 = descending.", "number", { repeating: true, optional: true })
2779
+ ],
2780
+ allowTrailingArgument: true,
2781
+ evaluate: (ctx) => {
2782
+ const range = rangeArg(ctx, 0);
2783
+ if (failed(range)) return range;
2784
+ const keys = [];
2785
+ let byCol = null;
2786
+ for (let i = 1; i < ctx.args.length; i += 2) {
2787
+ const by = rangeArg(ctx, i);
2788
+ if (failed(by)) return by;
2789
+ let keyByCol;
2790
+ if (by.cols === 1 && by.rows === range.rows) keyByCol = false;
2791
+ else if (by.rows === 1 && by.cols === range.cols) keyByCol = true;
2792
+ else {
2793
+ return ctx.argError(i, "#VALUE!", "must be a single column as tall as argument 1 (or a single row as wide).");
2794
+ }
2795
+ if (byCol !== null && byCol !== keyByCol) {
2796
+ return ctx.argError(i, "#VALUE!", "must have the same orientation as the other sort ranges.");
2797
+ }
2798
+ byCol = keyByCol;
2799
+ const order = sortOrder(ctx, i + 1);
2800
+ if (failed(order)) return order;
2801
+ keys.push({ values: by.values, order });
2802
+ }
2803
+ const sortByCol = _nullishCoalesce(byCol, () => ( false));
2804
+ const lines = linesOf(range, sortByCol).map((line, position) => ({ line, position })).sort((a, b) => {
2805
+ for (const key of keys) {
2806
+ const result = compareForSort(key.values[a.position], key.values[b.position], key.order);
2807
+ if (result !== 0) return result;
2808
+ }
2809
+ return a.position - b.position;
2810
+ }).map((entry) => entry.line);
2811
+ return fromLines(lines, sortByCol, sortByCol ? range.rows : range.cols);
2812
+ }
2813
+ },
2814
+ {
2815
+ name: "UNIQUE",
2816
+ category: "lookup",
2817
+ description: "Returns the distinct rows (or columns) of an array, in order of first appearance. The result spills.",
2818
+ args: [
2819
+ arg("array", "The range or array.", "range"),
2820
+ optional("by_col", "TRUE compares columns; FALSE (default) compares rows.", "logical"),
2821
+ optional("exactly_once", "TRUE returns only the rows that appear exactly once (default FALSE).", "logical")
2822
+ ],
2823
+ evaluate: (ctx) => {
2824
+ const range = rangeArg(ctx, 0);
2825
+ if (failed(range)) return range;
2826
+ const byCol = ctx.boolean(1, false);
2827
+ if (failed(byCol)) return byCol;
2828
+ const exactlyOnce = ctx.boolean(2, false);
2829
+ if (failed(exactlyOnce)) return exactlyOnce;
2830
+ const keyOf = (line) => JSON.stringify(
2831
+ line.map((v) => {
2832
+ const n = normalizeValue(v);
2833
+ return typeof n === "string" ? `s:${n.toLowerCase()}` : isBlank(n) ? "blank" : `${typeof n}:${String(n)}`;
2834
+ })
2835
+ );
2836
+ const counts = /* @__PURE__ */ new Map();
2837
+ for (const line of linesOf(range, byCol)) {
2838
+ const key = keyOf(line);
2839
+ const entry = counts.get(key);
2840
+ if (entry) entry.count++;
2841
+ else counts.set(key, { line, count: 1 });
2842
+ }
2843
+ const lines = Array.from(counts.values()).filter((entry) => !exactlyOnce || entry.count === 1).map((entry) => entry.line);
2844
+ if (lines.length === 0) return ctx.error("#CALC!", "UNIQUE found no values that appear exactly once.");
2845
+ return fromLines(lines, byCol, byCol ? range.rows : range.cols);
2846
+ }
2847
+ },
2848
+ {
2849
+ name: "SEQUENCE",
2850
+ category: "math",
2851
+ description: "Returns a grid of sequential numbers, filled row by row. The result spills.",
2852
+ args: [
2853
+ arg("rows", "The number of rows.", "number"),
2854
+ optional("columns", "The number of columns (default 1).", "number"),
2855
+ optional("start", "The first number (default 1).", "number"),
2856
+ optional("step", "The increment (default 1).", "number")
2857
+ ],
2858
+ evaluate: (ctx) => {
2859
+ const rows = ctx.integer(0);
2860
+ if (failed(rows)) return rows;
2861
+ const cols = ctx.integer(1, 1);
2862
+ if (failed(cols)) return cols;
2863
+ const start = ctx.number(2, 1);
2864
+ if (failed(start)) return start;
2865
+ const step = ctx.number(3, 1);
2866
+ if (failed(step)) return step;
2867
+ if (rows < 1 || cols < 1) return ctx.error("#CALC!", "SEQUENCE needs at least one row and one column.");
2868
+ if (rows * cols > MAX_ARRAY_CELLS) return ctx.error("#NUM!", `SEQUENCE can return at most ${MAX_ARRAY_CELLS} values.`);
2869
+ const out = [];
2870
+ for (let c = 0; c < cols; c++) {
2871
+ for (let r = 0; r < rows; r++) out.push(start + step * (r * cols + c));
2872
+ }
2873
+ return rows * cols === 1 ? out[0] : withRangeShape(out, rows, cols);
2874
+ }
2875
+ },
2876
+ {
2877
+ name: "TRANSPOSE",
2878
+ category: "lookup",
2879
+ description: "Swaps the rows and columns of an array. The result spills.",
2880
+ args: [arg("array", "The range or array to transpose.", "range")],
2881
+ evaluate: (ctx) => {
2882
+ const range = rangeArg(ctx, 0);
2883
+ if (failed(range)) return range;
2884
+ if (range.rows * range.cols === 1) return range.values[0];
2885
+ const out = [];
2886
+ for (let r = 0; r < range.rows; r++) {
2887
+ for (let c = 0; c < range.cols; c++) out.push(rangeGet(range, r, c));
2888
+ }
2889
+ return withRangeShape(out, range.cols, range.rows);
2890
+ }
2891
+ },
2892
+ {
2893
+ name: "RANDARRAY",
2894
+ category: "math",
2895
+ description: "Returns a grid of random numbers. Changes on every recalculation. The result spills.",
2896
+ args: [
2897
+ optional("rows", "The number of rows (default 1).", "number"),
2898
+ optional("columns", "The number of columns (default 1).", "number"),
2899
+ optional("min", "The smallest number (default 0).", "number"),
2900
+ optional("max", "The largest number (default 1).", "number"),
2901
+ optional("integer", "TRUE returns whole numbers; FALSE (default) decimals.", "logical")
2902
+ ],
2903
+ volatile: true,
2904
+ evaluate: (ctx) => {
2905
+ const rows = ctx.integer(0, 1);
2906
+ if (failed(rows)) return rows;
2907
+ const cols = ctx.integer(1, 1);
2908
+ if (failed(cols)) return cols;
2909
+ const min2 = ctx.number(2, 0);
2910
+ if (failed(min2)) return min2;
2911
+ const max2 = ctx.number(3, 1);
2912
+ if (failed(max2)) return max2;
2913
+ const integer = ctx.boolean(4, false);
2914
+ if (failed(integer)) return integer;
2915
+ if (rows < 1 || cols < 1) return ctx.error("#CALC!", "RANDARRAY needs at least one row and one column.");
2916
+ if (rows * cols > MAX_ARRAY_CELLS) return ctx.error("#NUM!", `RANDARRAY can return at most ${MAX_ARRAY_CELLS} values.`);
2917
+ if (min2 > max2) return ctx.argError(2, "#VALUE!", "can't be larger than max.");
2918
+ if (integer && (!Number.isInteger(min2) || !Number.isInteger(max2))) {
2919
+ return ctx.error("#VALUE!", "RANDARRAY needs whole min and max values when integer is TRUE.");
2920
+ }
2921
+ const next = () => integer ? min2 + Math.floor(Math.random() * (max2 - min2 + 1)) : min2 + Math.random() * (max2 - min2);
2922
+ const out = [];
2923
+ for (let i = 0; i < rows * cols; i++) out.push(next());
2924
+ return rows * cols === 1 ? out[0] : withRangeShape(out, rows, cols);
2925
+ }
2926
+ },
2927
+ {
2928
+ // Evaluated by the evaluator itself (it defines names); listed here for
2929
+ // autocomplete, signatures and argument-count checks.
2930
+ name: "LET",
2931
+ category: "logical",
2932
+ description: "Names the results of calculations so a formula can reuse them: LET(x, A1*2, x+x). The last argument is the calculation that uses the names.",
2933
+ args: [
2934
+ repeating("name", "A name to define (letters, not a cell reference)."),
2935
+ repeating("name_value", "The value of the name.")
2936
+ ],
2937
+ allowTrailingArgument: true,
2938
+ evaluate: (ctx) => ctx.error("#ERROR!", "LET must be evaluated by the formula evaluator.")
2939
+ }
2940
+ ];
2941
+ var ELEMENTWISE_BUILTINS = {
2942
+ ABS: true,
2943
+ ROUND: true,
2944
+ ROUNDUP: true,
2945
+ ROUNDDOWN: true,
2946
+ INT: true,
2947
+ MOD: true,
2948
+ POWER: true,
2949
+ SQRT: true,
2950
+ SIGN: true,
2951
+ CEILING: true,
2952
+ FLOOR: true,
2953
+ LEN: true,
2954
+ UPPER: true,
2955
+ LOWER: true,
2956
+ TRIM: true,
2957
+ PROPER: true,
2958
+ LEFT: true,
2959
+ RIGHT: true,
2960
+ MID: true,
2961
+ FIND: true,
2962
+ SEARCH: true,
2963
+ SUBSTITUTE: true,
2964
+ REPLACE: true,
2965
+ REPT: true,
2966
+ EXACT: true,
2967
+ VALUE: true,
2968
+ TEXT: true,
2969
+ YEAR: true,
2970
+ MONTH: true,
2971
+ DAY: true,
2972
+ WEEKDAY: true,
2973
+ EDATE: true,
2974
+ EOMONTH: true,
2975
+ DATE: true,
2976
+ NOT: true,
2977
+ ISBLANK: true,
2978
+ ISNUMBER: true,
2979
+ ISTEXT: true,
2980
+ ISERROR: true,
2981
+ ISNA: true,
2982
+ IF: [0],
2983
+ IFERROR: [0],
2984
+ IFNA: [0]
2985
+ };
2986
+
2244
2987
  // src/spreadsheet/formula/functions/index.ts
2245
2988
  var BUILTIN_FORMULA_FUNCTIONS = [
2246
2989
  ...MATH_FUNCTIONS,
@@ -2250,8 +2993,12 @@ var BUILTIN_FORMULA_FUNCTIONS = [
2250
2993
  ...TEXT_FUNCTIONS,
2251
2994
  ...LOOKUP_FUNCTIONS,
2252
2995
  ...DATE_FUNCTIONS,
2253
- ...INFORMATION_FUNCTIONS
2254
- ];
2996
+ ...INFORMATION_FUNCTIONS,
2997
+ ...ARRAY_FUNCTIONS
2998
+ ].map((def) => {
2999
+ const elementwise = ELEMENTWISE_BUILTINS[def.name];
3000
+ return elementwise && def.elementwise === void 0 ? { ...def, elementwise } : def;
3001
+ });
2255
3002
 
2256
3003
  // src/spreadsheet/formula/registry.ts
2257
3004
  var registry = /* @__PURE__ */ new Map();
@@ -2373,16 +3120,43 @@ var DEFAULT_MESSAGES = {
2373
3120
  "#N/A": "A value is not available.",
2374
3121
  "#NUM!": "The formula produced an invalid number.",
2375
3122
  "#CYCLE!": "This formula is part of a circular reference.",
2376
- "#ERROR!": "The formula could not be evaluated."
3123
+ "#ERROR!": "The formula could not be evaluated.",
3124
+ "#SPILL!": "The formula's array result can't spill into the cells it needs.",
3125
+ "#CALC!": "The formula produced an empty array."
2377
3126
  };
2378
3127
  function describeFormulaError(code) {
2379
3128
  return _nullishCoalesce(DEFAULT_MESSAGES[code], () => ( "The formula could not be evaluated."));
2380
3129
  }
2381
3130
  var quote = (value) => typeof value === "string" ? `"${value}"` : String(value);
3131
+ var MAX_FORMULA_ARRAY_SIZE = 1e6;
3132
+ function shapeOf(value) {
3133
+ if (!Array.isArray(value)) return { rows: 1, cols: 1 };
3134
+ const range = toFormulaRange(value);
3135
+ return { rows: range.rows, cols: range.cols };
3136
+ }
3137
+ function broadcastGet(value, row, col) {
3138
+ if (!Array.isArray(value)) return value;
3139
+ const range = toFormulaRange(value);
3140
+ const r = range.rows === 1 ? 0 : row;
3141
+ const c = range.cols === 1 ? 0 : col;
3142
+ if (r >= range.rows || c >= range.cols) return "#N/A";
3143
+ return range.values[c * range.rows + r];
3144
+ }
3145
+ function buildArray(rows, cols, fn) {
3146
+ const out = [];
3147
+ for (let c = 0; c < cols; c++) {
3148
+ for (let r = 0; r < rows; r++) out.push(fn(r, c));
3149
+ }
3150
+ return withRangeShape(out, rows, cols);
3151
+ }
3152
+ var isMultiArray = (value) => Array.isArray(value) && value.length > 1;
3153
+ var firstValue = (value) => Array.isArray(value) ? value.length ? value[0] : "" : value;
2382
3154
  var FormulaEvaluator = (_class2 = class {
2383
3155
 
2384
3156
  __init3() {this.diagnostics = []}
2385
- constructor(context = {}) {;_class2.prototype.__init3.call(this);
3157
+ // LET variables, innermost scope last.
3158
+ __init4() {this.scopes = []}
3159
+ constructor(context = {}) {;_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);
2386
3160
  this.context = context;
2387
3161
  }
2388
3162
  /**
@@ -2395,25 +3169,39 @@ var FormulaEvaluator = (_class2 = class {
2395
3169
  }
2396
3170
  /**
2397
3171
  * Evaluates a formula and explains errors. Never throws: malformed formulas
2398
- * return `#ERROR!` with the parser's message. A result that is a whole range
2399
- * (no dynamic-array spilling) is reduced to its first value.
3172
+ * return `#ERROR!` with the parser's message. A result with several values
3173
+ * (a range or a dynamic array) is returned in `array`, with its top-left
3174
+ * value as `value`; a one-value array is just that value.
2400
3175
  */
2401
3176
  evaluateWithDiagnostics(astOrFormula) {
2402
3177
  const saved = this.diagnostics;
3178
+ const savedScopes = this.scopes;
2403
3179
  this.diagnostics = [];
3180
+ this.scopes = [];
2404
3181
  try {
2405
3182
  let value = this.evaluate(astOrFormula);
2406
- if (Array.isArray(value)) value = value.length > 0 ? value[0] : "";
3183
+ let array;
3184
+ if (Array.isArray(value)) {
3185
+ if (value.length > 1) array = toFormulaRange(value);
3186
+ value = firstValue(value);
3187
+ }
2407
3188
  if (isFormulaError(value)) {
2408
3189
  const code = value;
2409
3190
  const diagnostic = this.diagnostics.find((d) => d.code === code);
2410
- return { value, error: code, message: _nullishCoalesce(_optionalChain([diagnostic, 'optionalAccess', _10 => _10.message]), () => ( describeFormulaError(code))) };
3191
+ const result = {
3192
+ value,
3193
+ error: code,
3194
+ message: _nullishCoalesce(_optionalChain([diagnostic, 'optionalAccess', _13 => _13.message]), () => ( describeFormulaError(code)))
3195
+ };
3196
+ if (array) result.array = array;
3197
+ return result;
2411
3198
  }
2412
- return { value };
3199
+ return array ? { value, array } : { value };
2413
3200
  } catch (err) {
2414
- return { value: "#ERROR!", error: "#ERROR!", message: _optionalChain([err, 'optionalAccess', _11 => _11.message]) || "Formula error" };
3201
+ return { value: "#ERROR!", error: "#ERROR!", message: _optionalChain([err, 'optionalAccess', _14 => _14.message]) || "Formula error" };
2415
3202
  } finally {
2416
3203
  this.diagnostics = saved;
3204
+ this.scopes = savedScopes;
2417
3205
  }
2418
3206
  }
2419
3207
  /** Records why an error happened and returns the error code. */
@@ -2442,6 +3230,14 @@ var FormulaEvaluator = (_class2 = class {
2442
3230
  }
2443
3231
  case "Range":
2444
3232
  return this.evaluateRange(node);
3233
+ case "SpillRef":
3234
+ return this.evaluateSpillRef(node);
3235
+ case "Name": {
3236
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
3237
+ if (this.scopes[i].has(node.name)) return this.scopes[i].get(node.name);
3238
+ }
3239
+ return this.fail("#NAME?", "This formula contains an unknown name.");
3240
+ }
2445
3241
  case "PropertyRef": {
2446
3242
  if (!this.context.resolveProperty) {
2447
3243
  throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);
@@ -2449,65 +3245,38 @@ var FormulaEvaluator = (_class2 = class {
2449
3245
  return this.context.resolveProperty(node.property);
2450
3246
  }
2451
3247
  case "UnaryOp": {
3248
+ if (node.op === "@") return this.implicitIntersection(node.argument);
2452
3249
  const raw = this.evaluateNode(node.argument);
2453
- const val = toNumber(raw);
2454
- if (isFormulaError(val)) {
2455
- return isFormulaError(raw) ? raw : this.fail("#VALUE!", `The ${node.op} operator can't use text (${quote(raw)}).`);
3250
+ if (isMultiArray(raw)) {
3251
+ const { rows, cols } = shapeOf(raw);
3252
+ return buildArray(rows, cols, (r, c) => this.unary(node.op, broadcastGet(raw, r, c)));
2456
3253
  }
2457
- if (node.op === "-") return -val;
2458
- if (node.op === "+") return val;
2459
- throw new Error(`Unsupported unary operator: ${node.op}`);
3254
+ return this.unary(node.op, firstValue(raw));
2460
3255
  }
2461
3256
  case "BinaryOp": {
2462
3257
  const left = this.evaluateNode(node.left);
2463
3258
  if (isFormulaError(left)) return left;
2464
3259
  const right = this.evaluateNode(node.right);
2465
3260
  if (isFormulaError(right)) return right;
2466
- if (["+", "-", "*", "/", "^"].includes(node.op)) {
2467
- const a = toNumber(left);
2468
- if (isFormulaError(a)) return this.fail("#VALUE!", `The ${node.op} operator can't use text (${quote(left)}).`);
2469
- const b = toNumber(right);
2470
- if (isFormulaError(b)) return this.fail("#VALUE!", `The ${node.op} operator can't use text (${quote(right)}).`);
2471
- const x = a;
2472
- const y = b;
2473
- let result;
2474
- switch (node.op) {
2475
- case "+":
2476
- result = x + y;
2477
- break;
2478
- case "-":
2479
- result = x - y;
2480
- break;
2481
- case "*":
2482
- result = x * y;
2483
- break;
2484
- case "/":
2485
- if (y === 0) return this.fail("#DIV/0!", "The formula divides by zero.");
2486
- result = x / y;
2487
- break;
2488
- default:
2489
- result = Math.pow(x, y);
2490
- }
2491
- return Number.isFinite(result) ? result : this.fail("#NUM!", "The result is too large or not a number.");
2492
- }
2493
- switch (node.op) {
2494
- case "&":
2495
- return toText(left) + toText(right);
2496
- case "=":
2497
- return compareValues(left, right) === 0;
2498
- case "<>":
2499
- return compareValues(left, right) !== 0;
2500
- case "<":
2501
- return compareValues(left, right) < 0;
2502
- case "<=":
2503
- return compareValues(left, right) <= 0;
2504
- case ">":
2505
- return compareValues(left, right) > 0;
2506
- case ">=":
2507
- return compareValues(left, right) >= 0;
2508
- default:
2509
- throw new Error(`Unsupported binary operator: ${node.op}`);
3261
+ if (isMultiArray(left) || isMultiArray(right)) {
3262
+ const a = shapeOf(left);
3263
+ const b = shapeOf(right);
3264
+ const rows = Math.max(a.rows, b.rows);
3265
+ const cols = Math.max(a.cols, b.cols);
3266
+ if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail("#NUM!", "The array result is too large.");
3267
+ return buildArray(rows, cols, (r2, c) => {
3268
+ const l2 = broadcastGet(left, r2, c);
3269
+ if (isFormulaError(l2)) return l2;
3270
+ const rv = broadcastGet(right, r2, c);
3271
+ if (isFormulaError(rv)) return rv;
3272
+ return this.binary(node.op, l2, rv);
3273
+ });
2510
3274
  }
3275
+ const l = firstValue(left);
3276
+ if (isFormulaError(l)) return l;
3277
+ const r = firstValue(right);
3278
+ if (isFormulaError(r)) return r;
3279
+ return this.binary(node.op, l, r);
2511
3280
  }
2512
3281
  case "FunctionCall":
2513
3282
  return this.callFunction(node);
@@ -2515,6 +3284,134 @@ var FormulaEvaluator = (_class2 = class {
2515
3284
  throw new Error(`Unknown AST node: ${node.type}`);
2516
3285
  }
2517
3286
  }
3287
+ unary(op, raw) {
3288
+ const val = toNumber(raw);
3289
+ if (isFormulaError(val)) {
3290
+ return isFormulaError(raw) ? raw : this.fail("#VALUE!", `The ${op} operator can't use text (${quote(raw)}).`);
3291
+ }
3292
+ if (op === "-") return -val;
3293
+ if (op === "+") return val;
3294
+ throw new Error(`Unsupported unary operator: ${op}`);
3295
+ }
3296
+ /** A binary operator on two single values. */
3297
+ binary(op, left, right) {
3298
+ if (["+", "-", "*", "/", "^"].includes(op)) {
3299
+ if (op === "+" || op === "-") {
3300
+ const leftDate = parseIsoDate(left);
3301
+ const rightDate = parseIsoDate(right);
3302
+ if (leftDate || rightDate) return this.dateArithmetic(op, left, right, leftDate, rightDate);
3303
+ }
3304
+ const a = toNumber(left);
3305
+ if (isFormulaError(a)) return this.fail("#VALUE!", `The ${op} operator can't use text (${quote(left)}).`);
3306
+ const b = toNumber(right);
3307
+ if (isFormulaError(b)) return this.fail("#VALUE!", `The ${op} operator can't use text (${quote(right)}).`);
3308
+ const x = a;
3309
+ const y = b;
3310
+ let result;
3311
+ switch (op) {
3312
+ case "+":
3313
+ result = x + y;
3314
+ break;
3315
+ case "-":
3316
+ result = x - y;
3317
+ break;
3318
+ case "*":
3319
+ result = x * y;
3320
+ break;
3321
+ case "/":
3322
+ if (y === 0) return this.fail("#DIV/0!", "The formula divides by zero.");
3323
+ result = x / y;
3324
+ break;
3325
+ default:
3326
+ result = Math.pow(x, y);
3327
+ }
3328
+ return Number.isFinite(result) ? result : this.fail("#NUM!", "The result is too large or not a number.");
3329
+ }
3330
+ switch (op) {
3331
+ case "&":
3332
+ return toText(left) + toText(right);
3333
+ case "=":
3334
+ return compareValues(left, right) === 0;
3335
+ case "<>":
3336
+ return compareValues(left, right) !== 0;
3337
+ case "<":
3338
+ return compareValues(left, right) < 0;
3339
+ case "<=":
3340
+ return compareValues(left, right) <= 0;
3341
+ case ">":
3342
+ return compareValues(left, right) > 0;
3343
+ case ">=":
3344
+ return compareValues(left, right) >= 0;
3345
+ default:
3346
+ throw new Error(`Unsupported binary operator: ${op}`);
3347
+ }
3348
+ }
3349
+ /**
3350
+ * Dates are ISO text: date +/- days is a date, date - date is a number of
3351
+ * days (fractional when times are involved).
3352
+ */
3353
+ dateArithmetic(op, left, right, leftDate, rightDate) {
3354
+ if (leftDate && rightDate) {
3355
+ if (op === "+") return this.fail("#VALUE!", "Two dates can't be added; subtract them to count the days between.");
3356
+ return cleanFloat(dateToSerial(leftDate) - dateToSerial(rightDate));
3357
+ }
3358
+ if (!leftDate && op === "-") {
3359
+ return this.fail("#VALUE!", `A date (${quote(right)}) can't be subtracted from a number.`);
3360
+ }
3361
+ const date = _nullishCoalesce(leftDate, () => ( rightDate));
3362
+ const dateText = String(leftDate ? left : right);
3363
+ const other = leftDate ? right : left;
3364
+ const days = toNumber(other);
3365
+ if (isFormulaError(days)) return this.fail("#VALUE!", `The ${op} operator can't use text (${quote(other)}).`);
3366
+ const n = days;
3367
+ const result = serialToDate(dateToSerial(date) + (op === "+" ? n : -n));
3368
+ if (!result || result.getFullYear() < 1 || result.getFullYear() > 9999) {
3369
+ return this.fail("#NUM!", "The resulting date is out of range.");
3370
+ }
3371
+ return isoDateHasTime(dateText) || !Number.isInteger(n) ? formatIsoDateTime(result) : formatIsoDate(result);
3372
+ }
3373
+ /**
3374
+ * `@` (implicit intersection): a range reference reads the cell in the
3375
+ * formula's row (one column) or column (one row); arrays give their
3376
+ * top-left value.
3377
+ */
3378
+ implicitIntersection(argument) {
3379
+ const value = this.evaluateNode(argument);
3380
+ if (!Array.isArray(value)) return value;
3381
+ if (value.length <= 1) return firstValue(value);
3382
+ const cell = _optionalChain([this, 'access', _15 => _15.context, 'access', _16 => _16.getFormulaCell, 'optionalCall', _17 => _17()]);
3383
+ if (argument.type === "Range" && cell) {
3384
+ const range = toFormulaRange(value);
3385
+ const top = Math.min(argument.startRow, argument.endRow);
3386
+ const left = Math.min(colNameToIndex(argument.startCol), colNameToIndex(argument.endCol));
3387
+ if (range.cols === 1) {
3388
+ const r = cell.row - top;
3389
+ if (r >= 0 && r < range.rows) return range.values[r];
3390
+ } else if (range.rows === 1) {
3391
+ const c = colNameToIndex(cell.col) - left;
3392
+ if (c >= 0 && c < range.cols) return range.values[c];
3393
+ }
3394
+ return this.fail("#VALUE!", `The @ operator found no cell of ${argument.raw} in the formula's row or column.`);
3395
+ }
3396
+ return value[0];
3397
+ }
3398
+ evaluateSpillRef(node) {
3399
+ if (!this.context.resolveSpill) {
3400
+ throw new Error(`Cannot resolve spill range ${node.ref}#: no spill resolver provided`);
3401
+ }
3402
+ const spill = this.context.resolveSpill(node.col, node.row);
3403
+ if (typeof spill === "string" || !spill) {
3404
+ const code = isFormulaError(spill) ? spill : "#REF!";
3405
+ return this.fail(
3406
+ code,
3407
+ code === "#REF!" ? `${node.ref}# refers to ${node.ref}, which doesn't hold a spilled array.` : `The spill range ${node.ref}# isn't available (${code}).`
3408
+ );
3409
+ }
3410
+ const values = withRangeShape(spill.values.slice(), spill.rows, spill.cols);
3411
+ const error = values.find(isFormulaError);
3412
+ if (error) this.fail(error, `The spill range ${node.ref}# contains an error (${error}).`);
3413
+ return values;
3414
+ }
2518
3415
  evaluateRange(node) {
2519
3416
  if (!this.context.resolveRange) {
2520
3417
  throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);
@@ -2532,15 +3429,23 @@ var FormulaEvaluator = (_class2 = class {
2532
3429
  if (!def) return this.fail("#NAME?", `Unknown function "${node.name.toUpperCase()}".`);
2533
3430
  const countError = checkFormulaArgumentCount(def, node.args.length);
2534
3431
  if (countError) return this.fail("#ERROR!", countError);
2535
- const result = def.evaluate(this.createContext(def, node.args));
3432
+ if (def.name === "LET") return this.evaluateLet(node);
3433
+ const args = this.lazyArguments(node.args);
3434
+ if (def.elementwise) {
3435
+ const lifted = this.liftOverArrays(def, args);
3436
+ if (lifted !== void 0) return lifted;
3437
+ }
3438
+ return this.finishCall(def, def.evaluate(this.createContext(def, args)));
3439
+ }
3440
+ finishCall(def, result) {
2536
3441
  if (typeof result === "number" && !Number.isFinite(result)) {
2537
3442
  return this.fail("#NUM!", `${def.name} produced an invalid number.`);
2538
3443
  }
2539
3444
  return result === void 0 || result === null ? "" : result;
2540
3445
  }
2541
- createContext(def, nodes) {
3446
+ lazyArguments(nodes) {
2542
3447
  const cache = /* @__PURE__ */ new Map();
2543
- const args = nodes.map((node, index) => ({
3448
+ return nodes.map((node, index) => ({
2544
3449
  index,
2545
3450
  node,
2546
3451
  value: () => {
@@ -2548,6 +3453,56 @@ var FormulaEvaluator = (_class2 = class {
2548
3453
  return cache.get(index);
2549
3454
  }
2550
3455
  }));
3456
+ }
3457
+ /**
3458
+ * Applies a scalar function element by element when it receives arrays
3459
+ * (`=LEN(A1:A3)`, `=IF(A1:A3>1, "y", "n")`). Arguments typed `range` are
3460
+ * passed whole. Returns undefined when no trigger argument is a
3461
+ * multi-value array.
3462
+ */
3463
+ liftOverArrays(def, args) {
3464
+ const liftable = (index) => _optionalChain([def, 'access', _18 => _18.args, 'access', _19 => _19[getFormulaArgumentSpecIndex(def, index)], 'optionalAccess', _20 => _20.type]) !== "range";
3465
+ const triggers = def.elementwise === true ? args.map((a) => a.index) : def.elementwise;
3466
+ if (!triggers.some((i) => i < args.length && liftable(i) && isMultiArray(args[i].value()))) return void 0;
3467
+ let rows = 1;
3468
+ let cols = 1;
3469
+ for (const a of args) {
3470
+ if (!liftable(a.index)) continue;
3471
+ const shape = shapeOf(a.value());
3472
+ rows = Math.max(rows, shape.rows);
3473
+ cols = Math.max(cols, shape.cols);
3474
+ }
3475
+ if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail("#NUM!", "The array result is too large.");
3476
+ return buildArray(rows, cols, (r, c) => {
3477
+ const elementArgs = args.map((a) => {
3478
+ if (!liftable(a.index)) return a;
3479
+ const element = broadcastGet(a.value(), r, c);
3480
+ return { index: a.index, node: a.node, value: () => element };
3481
+ });
3482
+ return firstValue(this.finishCall(def, def.evaluate(this.createContext(def, elementArgs))));
3483
+ });
3484
+ }
3485
+ /** LET(name1, value1, [name2, value2, ...], calculation). */
3486
+ evaluateLet(node) {
3487
+ if (node.args.length % 2 === 0) {
3488
+ return this.fail("#ERROR!", "LET needs name/value pairs followed by a calculation.");
3489
+ }
3490
+ const scope = /* @__PURE__ */ new Map();
3491
+ this.scopes.push(scope);
3492
+ try {
3493
+ for (let i = 0; i < node.args.length - 1; i += 2) {
3494
+ const nameNode = node.args[i];
3495
+ if (nameNode.type !== "Name") {
3496
+ return this.fail("#ERROR!", `Argument ${i + 1} of LET must be a name (names can't look like cell references).`);
3497
+ }
3498
+ scope.set(nameNode.name, this.evaluateNode(node.args[i + 1]));
3499
+ }
3500
+ return this.evaluateNode(node.args[node.args.length - 1]);
3501
+ } finally {
3502
+ this.scopes.pop();
3503
+ }
3504
+ }
3505
+ createContext(def, args) {
2551
3506
  const has = (index) => index >= 0 && index < args.length;
2552
3507
  const value = (index) => has(index) ? args[index].value() : "";
2553
3508
  const argError = (index, code, reason) => this.fail(code, `Argument ${index + 1} of ${def.name} ${reason}`);
@@ -2653,5 +3608,16 @@ var FormulaEvaluator = (_class2 = class {
2653
3608
 
2654
3609
 
2655
3610
 
2656
- exports.tokenizeFormula = tokenizeFormula; exports.WHOLE_COLUMN_END_ROW = WHOLE_COLUMN_END_ROW; exports.parseCellReference = parseCellReference; exports.parseRange = parseRange; exports.FormulaParser = FormulaParser; exports.parseFormula = parseFormula; exports.FORMULA_ERRORS = FORMULA_ERRORS; exports.isFormulaError = isFormulaError; exports.colNameToIndex = colNameToIndex; exports.indexToColName = indexToColName; exports.wildcardToRegExp = wildcardToRegExp; exports.parseCriteria = parseCriteria; exports.formatValueWithCode = formatValueWithCode; exports.registerFormulaFunction = registerFormulaFunction; exports.unregisterFormulaFunction = unregisterFormulaFunction; exports.getFormulaFunction = getFormulaFunction; exports.listFormulaFunctions = listFormulaFunctions; exports.searchFormulaFunctions = searchFormulaFunctions; exports.getFormulaArgumentSpecIndex = getFormulaArgumentSpecIndex; exports.checkFormulaArgumentCount = checkFormulaArgumentCount; exports.getFormulaSignatureParts = getFormulaSignatureParts; exports.getFormulaSignature = getFormulaSignature; exports.describeFormulaError = describeFormulaError; exports.FormulaEvaluator = FormulaEvaluator;
2657
- //# sourceMappingURL=chunk-JYRIOFEN.cjs.map
3611
+
3612
+
3613
+
3614
+
3615
+
3616
+
3617
+
3618
+
3619
+
3620
+
3621
+
3622
+ exports.tokenizeFormula = tokenizeFormula; exports.WHOLE_COLUMN_END_ROW = WHOLE_COLUMN_END_ROW; exports.parseCellReference = parseCellReference; exports.parseRange = parseRange; exports.FormulaParser = FormulaParser; exports.parseFormula = parseFormula; exports.serialFromYmd = serialFromYmd; exports.parseCellInput = parseCellInput; exports.parseNumberText = parseNumberText; exports.FORMULA_ERRORS = FORMULA_ERRORS; exports.isFormulaError = isFormulaError; exports.numberToText = numberToText; exports.numericValue = numericValue; exports.compareNumbers = compareNumbers; exports.compareValues = compareValues; exports.sortCompareValues = sortCompareValues; exports.colNameToIndex = colNameToIndex; exports.indexToColName = indexToColName; exports.parseIsoDate = parseIsoDate; exports.wildcardToRegExp = wildcardToRegExp; exports.parseCriteria = parseCriteria; exports.formatCellDisplay = formatCellDisplay; exports.formatValueWithCode = formatValueWithCode; exports.registerFormulaFunction = registerFormulaFunction; exports.unregisterFormulaFunction = unregisterFormulaFunction; exports.getFormulaFunction = getFormulaFunction; exports.listFormulaFunctions = listFormulaFunctions; exports.searchFormulaFunctions = searchFormulaFunctions; exports.getFormulaArgumentSpecIndex = getFormulaArgumentSpecIndex; exports.checkFormulaArgumentCount = checkFormulaArgumentCount; exports.getFormulaSignatureParts = getFormulaSignatureParts; exports.getFormulaSignature = getFormulaSignature; exports.describeFormulaError = describeFormulaError; exports.MAX_FORMULA_ARRAY_SIZE = MAX_FORMULA_ARRAY_SIZE; exports.FormulaEvaluator = FormulaEvaluator;
3623
+ //# sourceMappingURL=chunk-RIKBJLS2.cjs.map