@alaarab/ogrid-react 2.2.0 → 2.3.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.
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as React4 from 'react';
1
+ import * as React5 from 'react';
2
2
  import { useRef, useState, useCallback, useEffect, useMemo, useImperativeHandle, useLayoutEffect, forwardRef } from 'react';
3
3
  import { createPortal, flushSync } from 'react-dom';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -41,14 +41,23 @@ function escapeCsvValue(value) {
41
41
  function buildCsvHeader(columns) {
42
42
  return columns.map((c) => escapeCsvValue(c.name)).join(",");
43
43
  }
44
- function buildCsvRows(items, columns, getValue) {
44
+ function buildCsvRows(items, columns, getValue, formulaOptions) {
45
45
  return items.map(
46
- (item) => columns.map((c) => escapeCsvValue(getValue(item, c.columnId))).join(",")
46
+ (item, rowIdx) => columns.map((col) => {
47
+ if (formulaOptions?.exportMode === "formulas" && formulaOptions.hasFormula && formulaOptions.getFormula && formulaOptions.columnIdToIndex) {
48
+ const colIdx = formulaOptions.columnIdToIndex.get(col.columnId);
49
+ if (colIdx !== void 0 && formulaOptions.hasFormula(colIdx, rowIdx)) {
50
+ const formula = formulaOptions.getFormula(colIdx, rowIdx);
51
+ if (formula) return escapeCsvValue(formula);
52
+ }
53
+ }
54
+ return escapeCsvValue(getValue(item, col.columnId));
55
+ }).join(",")
47
56
  );
48
57
  }
49
- function exportToCsv(items, columns, getValue, filename) {
58
+ function exportToCsv(items, columns, getValue, filename, formulaOptions) {
50
59
  const header = buildCsvHeader(columns);
51
- const rows = buildCsvRows(items, columns, getValue);
60
+ const rows = buildCsvRows(items, columns, getValue, formulaOptions);
52
61
  const csv = [header, ...rows].join("\n");
53
62
  triggerCsvDownload(csv, filename ?? `export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`);
54
63
  }
@@ -775,6 +784,17 @@ function partitionColumnsForVirtualization(visibleCols, columnRange, pinnedColum
775
784
  rightSpacerWidth: columnRange.rightOffset
776
785
  };
777
786
  }
787
+ function getScrollTopForRow(rowIndex, rowHeight, containerHeight, align = "start") {
788
+ const rowTop = rowIndex * rowHeight;
789
+ switch (align) {
790
+ case "start":
791
+ return rowTop;
792
+ case "center":
793
+ return Math.max(0, rowTop - (containerHeight - rowHeight) / 2);
794
+ case "end":
795
+ return Math.max(0, rowTop - containerHeight + rowHeight);
796
+ }
797
+ }
778
798
  function workerBody() {
779
799
  const ctx = self;
780
800
  ctx.onmessage = (e) => {
@@ -994,6 +1014,15 @@ function processClientSideDataAsync(data, columns, filters, sortBy, sortDirectio
994
1014
  worker.postMessage(request);
995
1015
  });
996
1016
  }
1017
+ var FormulaError = class {
1018
+ constructor(type, message) {
1019
+ this.type = type;
1020
+ this.message = message;
1021
+ }
1022
+ toString() {
1023
+ return this.type;
1024
+ }
1025
+ };
997
1026
  function getHeaderFilterConfig(col, input) {
998
1027
  const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
999
1028
  const filterType = filterable?.type ?? "none";
@@ -1120,7 +1149,8 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
1120
1149
  const canEditAny = canEditInline || canEditPopup;
1121
1150
  const isEditing = input.editingCell?.rowId === rowId && input.editingCell?.columnId === col.columnId;
1122
1151
  const isActive = !input.isDragging && input.activeCell?.rowIndex === rowIndex && input.activeCell?.columnIndex === globalColIndex;
1123
- const isInRange = input.selectionRange != null && isInSelectionRange(input.selectionRange, rowIndex, colIdx);
1152
+ const isSingleCellRange = input.selectionRange != null && input.selectionRange.startRow === input.selectionRange.endRow && input.selectionRange.startCol === input.selectionRange.endCol;
1153
+ const isInRange = input.selectionRange != null && !isSingleCellRange && isInSelectionRange(input.selectionRange, rowIndex, colIdx);
1124
1154
  const isInCutRange = input.cutRange != null && isInSelectionRange(input.cutRange, rowIndex, colIdx);
1125
1155
  const isInCopyRange = input.copyRange != null && isInSelectionRange(input.copyRange, rowIndex, colIdx);
1126
1156
  const isSelectionEndCell = !input.isDragging && input.copyRange == null && input.cutRange == null && input.selectionRange != null && rowIndex === input.selectionRange.endRow && colIdx === input.selectionRange.endCol;
@@ -1162,6 +1192,9 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
1162
1192
  };
1163
1193
  }
1164
1194
  function resolveCellDisplayContent(col, item, displayValue) {
1195
+ if (displayValue instanceof FormulaError) {
1196
+ return displayValue.toString();
1197
+ }
1165
1198
  const c = col;
1166
1199
  if (c.renderCell && typeof c.renderCell === "function") {
1167
1200
  return c.renderCell(item);
@@ -1177,10 +1210,14 @@ function resolveCellDisplayContent(col, item, displayValue) {
1177
1210
  }
1178
1211
  return String(displayValue);
1179
1212
  }
1180
- function resolveCellStyle(col, item) {
1213
+ function resolveCellStyle(col, item, displayValue) {
1181
1214
  const c = col;
1182
- if (!c.cellStyle) return void 0;
1183
- return typeof c.cellStyle === "function" ? c.cellStyle(item) : c.cellStyle;
1215
+ const isError = displayValue instanceof FormulaError;
1216
+ const base = c.cellStyle ? typeof c.cellStyle === "function" ? c.cellStyle(item) : c.cellStyle : void 0;
1217
+ if (isError) {
1218
+ return { ...base, color: "var(--ogrid-formula-error-color, #d32f2f)" };
1219
+ }
1220
+ return base;
1184
1221
  }
1185
1222
  function buildInlineEditorProps(item, col, descriptor, callbacks) {
1186
1223
  return {
@@ -1271,6 +1308,7 @@ var AUTOSIZE_MAX_PX = 520;
1271
1308
  function measureHeaderWidth(th) {
1272
1309
  const cs = getComputedStyle(th);
1273
1310
  const thPadding = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
1311
+ const thBorders = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
1274
1312
  let resizeHandleWidth = 0;
1275
1313
  for (let i = 0; i < th.children.length; i++) {
1276
1314
  const child = th.children[i];
@@ -1293,12 +1331,14 @@ function measureHeaderWidth(th) {
1293
1331
  overflow: child.style.overflow,
1294
1332
  flexShrink: child.style.flexShrink,
1295
1333
  width: child.style.width,
1296
- minWidth: child.style.minWidth
1334
+ minWidth: child.style.minWidth,
1335
+ maxWidth: child.style.maxWidth
1297
1336
  });
1298
1337
  child.style.overflow = "visible";
1299
1338
  child.style.flexShrink = "0";
1300
1339
  child.style.width = "max-content";
1301
1340
  child.style.minWidth = "max-content";
1341
+ child.style.maxWidth = "none";
1302
1342
  }
1303
1343
  expandDescendants(child);
1304
1344
  }
@@ -1316,8 +1356,9 @@ function measureHeaderWidth(th) {
1316
1356
  m.el.style.flexShrink = m.flexShrink;
1317
1357
  m.el.style.width = m.width;
1318
1358
  m.el.style.minWidth = m.minWidth;
1359
+ m.el.style.maxWidth = m.maxWidth;
1319
1360
  }
1320
- return expandedWidth + resizeHandleWidth + thPadding;
1361
+ return expandedWidth + resizeHandleWidth + thPadding + thBorders;
1321
1362
  }
1322
1363
  function measureColumnContentWidth(columnId, minWidth, container) {
1323
1364
  const minW = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
@@ -1528,7 +1569,7 @@ function formatCellValueForTsv(raw, formatted) {
1528
1569
  return "[Object]";
1529
1570
  }
1530
1571
  }
1531
- function formatSelectionAsTsv(items, visibleCols, range) {
1572
+ function formatSelectionAsTsv(items, visibleCols, range, formulaOptions) {
1532
1573
  const norm = normalizeSelectionRange(range);
1533
1574
  const rows = [];
1534
1575
  for (let r = norm.startRow; r <= norm.endRow; r++) {
@@ -1537,6 +1578,16 @@ function formatSelectionAsTsv(items, visibleCols, range) {
1537
1578
  if (r >= items.length || c >= visibleCols.length) break;
1538
1579
  const item = items[r];
1539
1580
  const col = visibleCols[c];
1581
+ if (formulaOptions?.hasFormula && formulaOptions?.getFormula) {
1582
+ const flatColIndex = formulaOptions.flatColumns.findIndex((fc) => fc.columnId === col.columnId);
1583
+ if (flatColIndex >= 0 && formulaOptions.hasFormula(flatColIndex, r)) {
1584
+ const formulaStr = formulaOptions.getFormula(flatColIndex, r);
1585
+ if (formulaStr) {
1586
+ cells.push(formulaStr);
1587
+ continue;
1588
+ }
1589
+ }
1590
+ }
1540
1591
  const raw = getCellValue(item, col);
1541
1592
  const clipboard = col.clipboardFormatter ? col.clipboardFormatter(raw, item) : null;
1542
1593
  const formatted = clipboard ?? (col.valueFormatter ? col.valueFormatter(raw, item) : raw);
@@ -1551,7 +1602,7 @@ function parseTsvClipboard(text) {
1551
1602
  const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
1552
1603
  return lines.map((line) => line.split(" "));
1553
1604
  }
1554
- function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols) {
1605
+ function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols, formulaOptions) {
1555
1606
  const events = [];
1556
1607
  for (let r = 0; r < parsedRows.length; r++) {
1557
1608
  const cells = parsedRows[r];
@@ -1562,9 +1613,16 @@ function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols)
1562
1613
  const item = items[targetRow];
1563
1614
  const col = visibleCols[targetCol];
1564
1615
  if (!isColumnEditable(col, item)) continue;
1565
- const rawValue = cells[c] ?? "";
1616
+ const cellText = cells[c] ?? "";
1617
+ if (cellText.startsWith("=") && formulaOptions?.setFormula) {
1618
+ const flatColIndex = formulaOptions.flatColumns.findIndex((fc) => fc.columnId === col.columnId);
1619
+ if (flatColIndex >= 0) {
1620
+ formulaOptions.setFormula(flatColIndex, targetRow, cellText);
1621
+ continue;
1622
+ }
1623
+ }
1566
1624
  const oldValue = getCellValue(item, col);
1567
- const result = parseValue(rawValue, oldValue, item, col);
1625
+ const result = parseValue(cellText, oldValue, item, col);
1568
1626
  if (!result.valid) continue;
1569
1627
  events.push({
1570
1628
  item,
@@ -1599,12 +1657,101 @@ function applyCutClear(cutRange, items, visibleCols) {
1599
1657
  }
1600
1658
  return events;
1601
1659
  }
1602
- function applyFillValues(range, sourceRow, sourceCol, items, visibleCols) {
1660
+ function indexToColumnLetter(index) {
1661
+ let result = "";
1662
+ let n = index;
1663
+ while (n >= 0) {
1664
+ result = String.fromCharCode(n % 26 + 65) + result;
1665
+ n = Math.floor(n / 26) - 1;
1666
+ }
1667
+ return result;
1668
+ }
1669
+ function formatCellReference(colIndex, rowNumber) {
1670
+ return `${indexToColumnLetter(colIndex)}${rowNumber}`;
1671
+ }
1672
+ function columnLetterToIndex(letters) {
1673
+ let result = 0;
1674
+ const upper = letters.toUpperCase();
1675
+ for (let i = 0; i < upper.length; i++) {
1676
+ result = result * 26 + (upper.charCodeAt(i) - 64);
1677
+ }
1678
+ return result - 1;
1679
+ }
1680
+ var CELL_REF_RE = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
1681
+ var ADJUST_REF_RE = /(?:'[^']*'!|[A-Za-z_]\w*!)?(\$?)([A-Z]+)(\$?)(\d+)/g;
1682
+ function parseCellRef(ref) {
1683
+ const m = ref.match(CELL_REF_RE);
1684
+ if (!m) return null;
1685
+ const absCol = m[1] === "$";
1686
+ const colLetters = m[2];
1687
+ const absRow = m[3] === "$";
1688
+ const rowNum = parseInt(m[4], 10);
1689
+ if (rowNum < 1) return null;
1690
+ return {
1691
+ col: columnLetterToIndex(colLetters),
1692
+ row: rowNum - 1,
1693
+ // 0-based internally
1694
+ absCol,
1695
+ absRow
1696
+ };
1697
+ }
1698
+ function parseRange(rangeStr) {
1699
+ const parts = rangeStr.split(":");
1700
+ if (parts.length !== 2) return null;
1701
+ const start = parseCellRef(parts[0]);
1702
+ const end = parseCellRef(parts[1]);
1703
+ if (!start || !end) return null;
1704
+ return { start, end };
1705
+ }
1706
+ function adjustFormulaReferences(formula, colDelta, rowDelta) {
1707
+ ADJUST_REF_RE.lastIndex = 0;
1708
+ return formula.replace(ADJUST_REF_RE, (match, colAbs, colLetters, rowAbs, rowDigits) => {
1709
+ const cellRefStart = match.indexOf(colAbs + colLetters);
1710
+ const sheetPrefix = cellRefStart > 0 ? match.substring(0, cellRefStart) : "";
1711
+ let newCol = colLetters;
1712
+ let newRow = rowDigits;
1713
+ if (colAbs !== "$") {
1714
+ const colIdx = columnLetterToIndex(colLetters) + colDelta;
1715
+ if (colIdx < 0) return "#REF!";
1716
+ newCol = indexToColumnLetter(colIdx);
1717
+ }
1718
+ if (rowAbs !== "$") {
1719
+ const rowNum = parseInt(rowDigits, 10) + rowDelta;
1720
+ if (rowNum < 1) return "#REF!";
1721
+ newRow = String(rowNum);
1722
+ }
1723
+ return `${sheetPrefix}${colAbs}${newCol}${rowAbs}${newRow}`;
1724
+ });
1725
+ }
1726
+ function toCellKey(col, row, sheet) {
1727
+ if (sheet) return `${sheet}:${col},${row}`;
1728
+ return `${col},${row}`;
1729
+ }
1730
+ function fromCellKey(key) {
1731
+ const colonIdx = key.indexOf(":");
1732
+ if (colonIdx >= 0 && isNaN(parseInt(key.substring(0, colonIdx), 10))) {
1733
+ const sheet = key.substring(0, colonIdx);
1734
+ const rest = key.substring(colonIdx + 1);
1735
+ const commaIdx = rest.indexOf(",");
1736
+ return {
1737
+ col: parseInt(rest.substring(0, commaIdx), 10),
1738
+ row: parseInt(rest.substring(commaIdx + 1), 10),
1739
+ sheet
1740
+ };
1741
+ }
1742
+ const i = key.indexOf(",");
1743
+ return {
1744
+ col: parseInt(key.substring(0, i), 10),
1745
+ row: parseInt(key.substring(i + 1), 10)
1746
+ };
1747
+ }
1748
+ function applyFillValues(range, sourceRow, sourceCol, items, visibleCols, formulaOptions) {
1603
1749
  const events = [];
1604
1750
  const startItem = items[range.startRow];
1605
1751
  const startColDef = visibleCols[range.startCol];
1606
1752
  if (!startItem || !startColDef) return events;
1607
1753
  const startValue = getCellValue(startItem, startColDef);
1754
+ const srcFlatColIndex = formulaOptions ? formulaOptions.flatColumns.findIndex((c) => c.columnId === startColDef.columnId) : -1;
1608
1755
  for (let row = range.startRow; row <= range.endRow; row++) {
1609
1756
  for (let col = range.startCol; col <= range.endCol; col++) {
1610
1757
  if (row === sourceRow && col === sourceCol) continue;
@@ -1612,6 +1759,19 @@ function applyFillValues(range, sourceRow, sourceCol, items, visibleCols) {
1612
1759
  const item = items[row];
1613
1760
  const colDef = visibleCols[col];
1614
1761
  if (!isColumnEditable(colDef, item)) continue;
1762
+ if (formulaOptions && formulaOptions.hasFormula && formulaOptions.getFormula && formulaOptions.setFormula && srcFlatColIndex >= 0 && formulaOptions.hasFormula(srcFlatColIndex, sourceRow)) {
1763
+ const srcFormula = formulaOptions.getFormula(srcFlatColIndex, sourceRow);
1764
+ if (srcFormula) {
1765
+ const rowDelta = row - sourceRow;
1766
+ const colDelta = col - sourceCol;
1767
+ const adjusted = adjustFormulaReferences(srcFormula, colDelta, rowDelta);
1768
+ const targetFlatColIdx = formulaOptions.flatColumns.findIndex((c) => c.columnId === colDef.columnId);
1769
+ if (targetFlatColIdx >= 0) {
1770
+ formulaOptions.setFormula(targetFlatColIdx, row, adjusted);
1771
+ continue;
1772
+ }
1773
+ }
1774
+ }
1615
1775
  const oldValue = getCellValue(item, colDef);
1616
1776
  const result = parseValue(startValue, oldValue, item, colDef);
1617
1777
  if (!result.valid) continue;
@@ -1633,159 +1793,3332 @@ var UndoRedoStack = class {
1633
1793
  this.batch = null;
1634
1794
  this.maxDepth = maxDepth;
1635
1795
  }
1636
- /** Whether there are undo steps available. */
1637
- get canUndo() {
1638
- return this.history.length > 0;
1796
+ /** Whether there are undo steps available. */
1797
+ get canUndo() {
1798
+ return this.history.length > 0;
1799
+ }
1800
+ /** Whether there are redo steps available. */
1801
+ get canRedo() {
1802
+ return this.redoStack.length > 0;
1803
+ }
1804
+ /** Number of history entries. */
1805
+ get historyLength() {
1806
+ return this.history.length;
1807
+ }
1808
+ /** Number of redo entries. */
1809
+ get redoLength() {
1810
+ return this.redoStack.length;
1811
+ }
1812
+ /** Whether a batch is currently open. */
1813
+ get isBatching() {
1814
+ return this.batch !== null;
1815
+ }
1816
+ /**
1817
+ * Record a group of events as a single undoable step.
1818
+ * If a batch is open, accumulates into the batch instead.
1819
+ * Clears the redo stack on any new entry.
1820
+ */
1821
+ push(events) {
1822
+ if (events.length === 0) return;
1823
+ if (this.batch !== null) {
1824
+ this.batch.push(...events);
1825
+ } else {
1826
+ this.history.push(events);
1827
+ if (this.history.length > this.maxDepth) {
1828
+ this.history.splice(0, this.history.length - this.maxDepth);
1829
+ }
1830
+ this.redoStack.length = 0;
1831
+ }
1832
+ }
1833
+ /**
1834
+ * Record a single event as a step (shorthand for push([event])).
1835
+ * If a batch is open, accumulates into the batch instead.
1836
+ */
1837
+ record(event) {
1838
+ this.push([event]);
1839
+ }
1840
+ /**
1841
+ * Start a batch — subsequent record/push calls accumulate into one undo step.
1842
+ * Has no effect if a batch is already open.
1843
+ */
1844
+ beginBatch() {
1845
+ if (this.batch === null) {
1846
+ this.batch = [];
1847
+ }
1848
+ }
1849
+ /**
1850
+ * End a batch — commits all accumulated events as one undo step.
1851
+ * Has no effect if no batch is open or if the batch is empty.
1852
+ */
1853
+ endBatch() {
1854
+ const b = this.batch;
1855
+ this.batch = null;
1856
+ if (!b || b.length === 0) return;
1857
+ this.history.push(b);
1858
+ if (this.history.length > this.maxDepth) {
1859
+ this.history.splice(0, this.history.length - this.maxDepth);
1860
+ }
1861
+ this.redoStack.length = 0;
1862
+ }
1863
+ /**
1864
+ * Pop the most recent history entry for undo.
1865
+ * Returns the batch of events (in original order) to be reversed by the caller,
1866
+ * or null if there is nothing to undo.
1867
+ *
1868
+ * The caller is responsible for applying the events in reverse order.
1869
+ */
1870
+ undo() {
1871
+ const lastBatch = this.history.pop();
1872
+ if (!lastBatch) return null;
1873
+ this.redoStack.push(lastBatch);
1874
+ return lastBatch;
1875
+ }
1876
+ /**
1877
+ * Pop the most recent redo entry.
1878
+ * Returns the batch of events (in original order) to be re-applied by the caller,
1879
+ * or null if there is nothing to redo.
1880
+ */
1881
+ redo() {
1882
+ const nextBatch = this.redoStack.pop();
1883
+ if (!nextBatch) return null;
1884
+ this.history.push(nextBatch);
1885
+ return nextBatch;
1886
+ }
1887
+ /**
1888
+ * Clear all history and redo state.
1889
+ * Does not affect any open batch — call endBatch() first if needed.
1890
+ */
1891
+ clear() {
1892
+ this.history = [];
1893
+ this.redoStack = [];
1894
+ }
1895
+ };
1896
+ function validateColumns(columns) {
1897
+ if (!Array.isArray(columns) || columns.length === 0) {
1898
+ console.warn("[OGrid] columns prop is empty or not an array");
1899
+ return;
1900
+ }
1901
+ const isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1902
+ const ids = /* @__PURE__ */ new Set();
1903
+ for (const col of columns) {
1904
+ if (!col.columnId) {
1905
+ console.warn("[OGrid] Column missing columnId:", col);
1906
+ }
1907
+ if (ids.has(col.columnId)) {
1908
+ console.warn(`[OGrid] Duplicate columnId: "${col.columnId}"`);
1909
+ }
1910
+ ids.add(col.columnId);
1911
+ if (isDev && col.editable === true && col.cellEditor == null) {
1912
+ console.warn(
1913
+ `[OGrid] Column "${col.columnId}" has editable=true but no cellEditor defined. Cells will not open an editor on double-click. Set cellEditor to 'text', 'select', 'checkbox', 'date', or a custom component.`
1914
+ );
1915
+ }
1916
+ }
1917
+ }
1918
+ function validateVirtualScrollConfig(config) {
1919
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
1920
+ if (config.enabled !== true) return;
1921
+ if (!config.rowHeight || config.rowHeight <= 0) {
1922
+ console.warn(
1923
+ "[OGrid] virtualScroll.enabled is true but rowHeight is missing or <= 0. Set a positive rowHeight (e.g. virtualScroll: { enabled: true, rowHeight: 36 }) for correct virtual scrolling behavior."
1924
+ );
1925
+ }
1926
+ }
1927
+ function validateRowIds(items, getRowId) {
1928
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
1929
+ if (!getRowId) return;
1930
+ const ids = /* @__PURE__ */ new Set();
1931
+ const limit = Math.min(items.length, 100);
1932
+ for (let i = 0; i < limit; i++) {
1933
+ const id = getRowId(items[i]);
1934
+ if (id == null) {
1935
+ console.warn(`[OGrid] getRowId returned null/undefined for row ${i}`);
1936
+ return;
1937
+ }
1938
+ if (ids.has(id)) {
1939
+ console.warn(
1940
+ `[OGrid] Duplicate row ID "${id}" at index ${i}. getRowId must return unique values.`
1941
+ );
1942
+ return;
1943
+ }
1944
+ ids.add(id);
1945
+ }
1946
+ }
1947
+ var DEFAULT_DEBOUNCE_MS = 300;
1948
+ var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
1949
+ var CELL_REF_PATTERN = /^\$?[A-Za-z]+\$?\d+$/;
1950
+ var SINGLE_CHAR_OPERATORS = {
1951
+ "+": "PLUS",
1952
+ "-": "MINUS",
1953
+ "*": "MULTIPLY",
1954
+ "/": "DIVIDE",
1955
+ "^": "POWER",
1956
+ "%": "PERCENT",
1957
+ "&": "AMPERSAND",
1958
+ "=": "EQ"
1959
+ };
1960
+ var DELIMITERS = {
1961
+ "(": "LPAREN",
1962
+ ")": "RPAREN",
1963
+ ",": "COMMA",
1964
+ ":": "COLON"
1965
+ };
1966
+ function tokenize(input) {
1967
+ const tokens = [];
1968
+ let pos = 0;
1969
+ while (pos < input.length) {
1970
+ const ch = input[pos];
1971
+ if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
1972
+ pos++;
1973
+ continue;
1974
+ }
1975
+ if (ch >= "0" && ch <= "9" || ch === "." && pos + 1 < input.length && input[pos + 1] >= "0" && input[pos + 1] <= "9") {
1976
+ const start = pos;
1977
+ while (pos < input.length && input[pos] >= "0" && input[pos] <= "9") {
1978
+ pos++;
1979
+ }
1980
+ if (pos < input.length && input[pos] === ".") {
1981
+ pos++;
1982
+ while (pos < input.length && input[pos] >= "0" && input[pos] <= "9") {
1983
+ pos++;
1984
+ }
1985
+ }
1986
+ tokens.push({ type: "NUMBER", value: input.slice(start, pos), position: start });
1987
+ continue;
1988
+ }
1989
+ if (ch === "'") {
1990
+ const start = pos;
1991
+ pos++;
1992
+ const nameStart = pos;
1993
+ while (pos < input.length && input[pos] !== "'") {
1994
+ pos++;
1995
+ }
1996
+ const sheetName = input.slice(nameStart, pos);
1997
+ if (pos < input.length && input[pos] === "'") {
1998
+ pos++;
1999
+ if (pos < input.length && input[pos] === "!") {
2000
+ pos++;
2001
+ tokens.push({ type: "SHEET_REF", value: sheetName, position: start });
2002
+ continue;
2003
+ }
2004
+ }
2005
+ throw new FormulaError("#ERROR!", `Invalid sheet reference at position ${start}`);
2006
+ }
2007
+ if (ch === '"') {
2008
+ const start = pos;
2009
+ pos++;
2010
+ const scanStart = pos;
2011
+ let hasEscapes = false;
2012
+ while (pos < input.length) {
2013
+ if (input[pos] === '"') {
2014
+ if (pos + 1 < input.length && input[pos + 1] === '"') {
2015
+ hasEscapes = true;
2016
+ pos += 2;
2017
+ } else {
2018
+ break;
2019
+ }
2020
+ } else {
2021
+ pos++;
2022
+ }
2023
+ }
2024
+ let value;
2025
+ if (!hasEscapes) {
2026
+ value = input.slice(scanStart, pos);
2027
+ } else {
2028
+ value = input.slice(scanStart, pos).replace(/""/g, '"');
2029
+ }
2030
+ if (pos < input.length) pos++;
2031
+ tokens.push({ type: "STRING", value, position: start });
2032
+ continue;
2033
+ }
2034
+ if (ch === ">" && pos + 1 < input.length && input[pos + 1] === "=") {
2035
+ tokens.push({ type: "GTE", value: ">=", position: pos });
2036
+ pos += 2;
2037
+ continue;
2038
+ }
2039
+ if (ch === "<" && pos + 1 < input.length && input[pos + 1] === "=") {
2040
+ tokens.push({ type: "LTE", value: "<=", position: pos });
2041
+ pos += 2;
2042
+ continue;
2043
+ }
2044
+ if (ch === "<" && pos + 1 < input.length && input[pos + 1] === ">") {
2045
+ tokens.push({ type: "NEQ", value: "<>", position: pos });
2046
+ pos += 2;
2047
+ continue;
2048
+ }
2049
+ if (ch === ">" || ch === "<") {
2050
+ const type = ch === ">" ? "GT" : "LT";
2051
+ tokens.push({ type, value: ch, position: pos });
2052
+ pos++;
2053
+ continue;
2054
+ }
2055
+ if (SINGLE_CHAR_OPERATORS[ch]) {
2056
+ tokens.push({ type: SINGLE_CHAR_OPERATORS[ch], value: ch, position: pos });
2057
+ pos++;
2058
+ continue;
2059
+ }
2060
+ if (DELIMITERS[ch]) {
2061
+ tokens.push({ type: DELIMITERS[ch], value: ch, position: pos });
2062
+ pos++;
2063
+ continue;
2064
+ }
2065
+ if (ch === "$" || ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z") {
2066
+ const start = pos;
2067
+ while (pos < input.length && (input[pos] >= "A" && input[pos] <= "Z" || input[pos] >= "a" && input[pos] <= "z" || input[pos] >= "0" && input[pos] <= "9" || input[pos] === "$" || input[pos] === "_")) {
2068
+ pos++;
2069
+ }
2070
+ const word = input.slice(start, pos);
2071
+ if (pos < input.length && input[pos] === "!") {
2072
+ pos++;
2073
+ tokens.push({ type: "SHEET_REF", value: word, position: start });
2074
+ continue;
2075
+ }
2076
+ if (pos < input.length && input[pos] === "(") {
2077
+ tokens.push({ type: "FUNCTION", value: word, position: start });
2078
+ continue;
2079
+ }
2080
+ const upper = word.toUpperCase();
2081
+ if (upper === "TRUE" || upper === "FALSE") {
2082
+ tokens.push({ type: "BOOLEAN", value: upper, position: start });
2083
+ continue;
2084
+ }
2085
+ if (CELL_REF_PATTERN.test(word)) {
2086
+ tokens.push({ type: "CELL_REF", value: word, position: start });
2087
+ continue;
2088
+ }
2089
+ tokens.push({ type: "IDENTIFIER", value: word, position: start });
2090
+ continue;
2091
+ }
2092
+ throw new FormulaError("#ERROR!", `Unexpected character: ${ch}`);
2093
+ }
2094
+ tokens.push({ type: "EOF", value: "", position: pos });
2095
+ return tokens;
2096
+ }
2097
+ function parse(tokens, namedRanges) {
2098
+ let pos = 0;
2099
+ function peek() {
2100
+ return tokens[pos];
2101
+ }
2102
+ function advance() {
2103
+ const token = tokens[pos];
2104
+ pos++;
2105
+ return token;
2106
+ }
2107
+ function expect(type) {
2108
+ const token = peek();
2109
+ if (token && token.type === type) {
2110
+ return advance();
2111
+ }
2112
+ return null;
2113
+ }
2114
+ function errorNode(message) {
2115
+ return { kind: "error", error: new FormulaError("#ERROR!", message) };
2116
+ }
2117
+ function expression() {
2118
+ return comparison();
2119
+ }
2120
+ function comparison() {
2121
+ let left = concat();
2122
+ while (peek()) {
2123
+ const t = peek();
2124
+ let op = null;
2125
+ if (t.type === "GT") op = ">";
2126
+ else if (t.type === "LT") op = "<";
2127
+ else if (t.type === "GTE") op = ">=";
2128
+ else if (t.type === "LTE") op = "<=";
2129
+ else if (t.type === "EQ") op = "=";
2130
+ else if (t.type === "NEQ") op = "<>";
2131
+ else break;
2132
+ advance();
2133
+ const right = concat();
2134
+ left = { kind: "binaryOp", op, left, right };
2135
+ }
2136
+ return left;
2137
+ }
2138
+ function concat() {
2139
+ let left = addition();
2140
+ while (peek() && peek().type === "AMPERSAND") {
2141
+ advance();
2142
+ const right = addition();
2143
+ left = { kind: "binaryOp", op: "&", left, right };
2144
+ }
2145
+ return left;
2146
+ }
2147
+ function addition() {
2148
+ let left = multiplication();
2149
+ while (peek()) {
2150
+ const t = peek();
2151
+ let op = null;
2152
+ if (t.type === "PLUS") op = "+";
2153
+ else if (t.type === "MINUS") op = "-";
2154
+ else break;
2155
+ advance();
2156
+ const right = multiplication();
2157
+ left = { kind: "binaryOp", op, left, right };
2158
+ }
2159
+ return left;
2160
+ }
2161
+ function multiplication() {
2162
+ let left = power();
2163
+ while (peek()) {
2164
+ const t = peek();
2165
+ let op = null;
2166
+ if (t.type === "MULTIPLY") op = "*";
2167
+ else if (t.type === "DIVIDE") op = "/";
2168
+ else break;
2169
+ advance();
2170
+ const right = power();
2171
+ left = { kind: "binaryOp", op, left, right };
2172
+ }
2173
+ return left;
2174
+ }
2175
+ function power() {
2176
+ let left = unary();
2177
+ while (peek() && peek().type === "POWER") {
2178
+ advance();
2179
+ const right = unary();
2180
+ left = { kind: "binaryOp", op: "^", left, right };
2181
+ }
2182
+ return left;
2183
+ }
2184
+ function unary() {
2185
+ const t = peek();
2186
+ if (t && (t.type === "MINUS" || t.type === "PLUS")) {
2187
+ const op = t.type === "MINUS" ? "-" : "+";
2188
+ advance();
2189
+ const operand = unary();
2190
+ return { kind: "unaryOp", op, operand };
2191
+ }
2192
+ return postfix();
2193
+ }
2194
+ function postfix() {
2195
+ let node = primary();
2196
+ if (peek() && peek().type === "PERCENT") {
2197
+ advance();
2198
+ node = { kind: "binaryOp", op: "%", left: node, right: { kind: "number", value: 100 } };
2199
+ }
2200
+ return node;
2201
+ }
2202
+ function primary() {
2203
+ const t = peek();
2204
+ if (!t || t.type === "EOF") {
2205
+ return errorNode("Unexpected end of expression");
2206
+ }
2207
+ if (t.type === "NUMBER") {
2208
+ advance();
2209
+ return { kind: "number", value: parseFloat(t.value) };
2210
+ }
2211
+ if (t.type === "STRING") {
2212
+ advance();
2213
+ return { kind: "string", value: t.value };
2214
+ }
2215
+ if (t.type === "BOOLEAN") {
2216
+ advance();
2217
+ return { kind: "boolean", value: t.value.toUpperCase() === "TRUE" };
2218
+ }
2219
+ if (t.type === "CELL_REF") {
2220
+ return cellRefOrRange();
2221
+ }
2222
+ if (t.type === "FUNCTION") {
2223
+ return functionCall();
2224
+ }
2225
+ if (t.type === "IDENTIFIER") {
2226
+ return namedRangeRef();
2227
+ }
2228
+ if (t.type === "SHEET_REF") {
2229
+ return sheetRef();
2230
+ }
2231
+ if (t.type === "LPAREN") {
2232
+ advance();
2233
+ const node = expression();
2234
+ if (!expect("RPAREN")) {
2235
+ return errorNode("Expected closing parenthesis");
2236
+ }
2237
+ return node;
2238
+ }
2239
+ advance();
2240
+ return errorNode(`Unexpected token: ${t.value}`);
2241
+ }
2242
+ function cellRefOrRange() {
2243
+ const refToken = advance();
2244
+ const address = parseCellRef(refToken.value);
2245
+ if (!address) {
2246
+ return errorNode(`Invalid cell reference: ${refToken.value}`);
2247
+ }
2248
+ if (peek() && peek().type === "COLON") {
2249
+ advance();
2250
+ const endToken = expect("CELL_REF");
2251
+ if (!endToken) {
2252
+ return errorNode('Expected cell reference after ":"');
2253
+ }
2254
+ const endAddress = parseCellRef(endToken.value);
2255
+ if (!endAddress) {
2256
+ return errorNode(`Invalid cell reference: ${endToken.value}`);
2257
+ }
2258
+ return {
2259
+ kind: "range",
2260
+ start: address,
2261
+ end: endAddress,
2262
+ raw: `${refToken.value}:${endToken.value}`
2263
+ };
2264
+ }
2265
+ return {
2266
+ kind: "cellRef",
2267
+ address,
2268
+ raw: refToken.value
2269
+ };
2270
+ }
2271
+ function functionCall() {
2272
+ const nameToken = advance();
2273
+ const name = nameToken.value.toUpperCase();
2274
+ if (!expect("LPAREN")) {
2275
+ return errorNode(`Expected "(" after function name "${name}"`);
2276
+ }
2277
+ const args = [];
2278
+ if (peek() && peek().type !== "RPAREN" && peek().type !== "EOF") {
2279
+ args.push(expression());
2280
+ while (peek() && peek().type === "COMMA") {
2281
+ advance();
2282
+ args.push(expression());
2283
+ }
2284
+ }
2285
+ if (!expect("RPAREN")) {
2286
+ return errorNode(`Expected ")" after function arguments for "${name}"`);
2287
+ }
2288
+ return { kind: "functionCall", name, args };
2289
+ }
2290
+ function namedRangeRef() {
2291
+ const nameToken = advance();
2292
+ const name = nameToken.value.toUpperCase();
2293
+ const ref = namedRanges?.get(name);
2294
+ if (!ref) {
2295
+ return { kind: "error", error: new FormulaError("#NAME?", `Unknown name: ${nameToken.value}`) };
2296
+ }
2297
+ if (ref.includes(":")) {
2298
+ const rangeRef = parseRange(ref);
2299
+ if (rangeRef) {
2300
+ return { kind: "range", start: rangeRef.start, end: rangeRef.end, raw: ref };
2301
+ }
2302
+ }
2303
+ const cellRef = parseCellRef(ref);
2304
+ if (cellRef) {
2305
+ return { kind: "cellRef", address: cellRef, raw: ref };
2306
+ }
2307
+ return { kind: "error", error: new FormulaError("#REF!", `Invalid named range reference: ${ref}`) };
2308
+ }
2309
+ function sheetRef() {
2310
+ const sheetToken = advance();
2311
+ const sheetName = sheetToken.value;
2312
+ const cellToken = expect("CELL_REF");
2313
+ if (!cellToken) {
2314
+ return errorNode(`Expected cell reference after sheet "${sheetName}!"`);
2315
+ }
2316
+ const address = parseCellRef(cellToken.value);
2317
+ if (!address) {
2318
+ return errorNode(`Invalid cell reference: ${cellToken.value}`);
2319
+ }
2320
+ address.sheet = sheetName;
2321
+ if (peek() && peek().type === "COLON") {
2322
+ advance();
2323
+ const endToken = expect("CELL_REF");
2324
+ if (!endToken) {
2325
+ return errorNode('Expected cell reference after ":"');
2326
+ }
2327
+ const endAddress = parseCellRef(endToken.value);
2328
+ if (!endAddress) {
2329
+ return errorNode(`Invalid cell reference: ${endToken.value}`);
2330
+ }
2331
+ endAddress.sheet = sheetName;
2332
+ return {
2333
+ kind: "range",
2334
+ start: address,
2335
+ end: endAddress,
2336
+ raw: `${sheetName}!${cellToken.value}:${endToken.value}`
2337
+ };
2338
+ }
2339
+ return {
2340
+ kind: "cellRef",
2341
+ address,
2342
+ raw: `${sheetName}!${cellToken.value}`
2343
+ };
2344
+ }
2345
+ const result = expression();
2346
+ if (peek() && peek().type !== "EOF") {
2347
+ return errorNode(`Unexpected token after expression: ${peek().value}`);
2348
+ }
2349
+ return result;
2350
+ }
2351
+ function toNumber(val) {
2352
+ if (val instanceof FormulaError) return val;
2353
+ if (val === null || val === void 0 || val === "") return 0;
2354
+ if (typeof val === "boolean") return val ? 1 : 0;
2355
+ if (typeof val === "number") return val;
2356
+ if (val instanceof Date) return val.getTime();
2357
+ const n = Number(val);
2358
+ if (isNaN(n)) return new FormulaError("#VALUE!", `Cannot convert "${val}" to number`);
2359
+ return n;
2360
+ }
2361
+ function toString(val) {
2362
+ if (val === null || val === void 0) return "";
2363
+ if (val instanceof FormulaError) return val.toString();
2364
+ if (val instanceof Date) return val.toLocaleDateString();
2365
+ return String(val);
2366
+ }
2367
+ function flattenArgs(args, context, evaluator) {
2368
+ const result = [];
2369
+ for (const arg of args) {
2370
+ if (arg.kind === "range") {
2371
+ const values = context.getRangeValues({ start: arg.start, end: arg.end });
2372
+ for (const row of values) {
2373
+ for (const val of row) {
2374
+ result.push(val);
2375
+ }
2376
+ }
2377
+ } else {
2378
+ result.push(evaluator.evaluate(arg, context));
2379
+ }
2380
+ }
2381
+ return result;
2382
+ }
2383
+ var FormulaEvaluator = class {
2384
+ constructor(builtInFunctions) {
2385
+ this.functions = new Map(builtInFunctions);
2386
+ }
2387
+ registerFunction(name, fn) {
2388
+ this.functions.set(name.toUpperCase(), fn);
2389
+ }
2390
+ evaluate(node, context) {
2391
+ switch (node.kind) {
2392
+ case "number":
2393
+ return node.value;
2394
+ case "string":
2395
+ return node.value;
2396
+ case "boolean":
2397
+ return node.value;
2398
+ case "error":
2399
+ return node.error;
2400
+ case "cellRef": {
2401
+ const val = context.getCellValue(node.address);
2402
+ return val;
2403
+ }
2404
+ case "range":
2405
+ return context.getCellValue(node.start);
2406
+ case "functionCall":
2407
+ return this.evaluateFunction(node.name, node.args, context);
2408
+ case "binaryOp":
2409
+ return this.evaluateBinaryOp(node.op, node.left, node.right, context);
2410
+ case "unaryOp":
2411
+ return this.evaluateUnaryOp(node.op, node.operand, context);
2412
+ }
2413
+ }
2414
+ evaluateFunction(name, args, context) {
2415
+ const fn = this.functions.get(name);
2416
+ if (!fn) {
2417
+ return new FormulaError("#NAME?", `Unknown function: ${name}`);
2418
+ }
2419
+ if (args.length < fn.minArgs) {
2420
+ return new FormulaError("#ERROR!", `${name} requires at least ${fn.minArgs} argument(s)`);
2421
+ }
2422
+ if (fn.maxArgs >= 0 && args.length > fn.maxArgs) {
2423
+ return new FormulaError("#ERROR!", `${name} accepts at most ${fn.maxArgs} argument(s)`);
2424
+ }
2425
+ return fn.evaluate(args, context, this);
2426
+ }
2427
+ evaluateBinaryOp(op, left, right, context) {
2428
+ if (op === "&") {
2429
+ const l = this.evaluate(left, context);
2430
+ if (l instanceof FormulaError) return l;
2431
+ const r = this.evaluate(right, context);
2432
+ if (r instanceof FormulaError) return r;
2433
+ return toString(l) + toString(r);
2434
+ }
2435
+ if (op === ">" || op === "<" || op === ">=" || op === "<=" || op === "=" || op === "<>") {
2436
+ const l = this.evaluate(left, context);
2437
+ if (l instanceof FormulaError) return l;
2438
+ const r = this.evaluate(right, context);
2439
+ if (r instanceof FormulaError) return r;
2440
+ return this.compare(op, l, r);
2441
+ }
2442
+ const lVal = this.evaluate(left, context);
2443
+ if (lVal instanceof FormulaError) return lVal;
2444
+ const rVal = this.evaluate(right, context);
2445
+ if (rVal instanceof FormulaError) return rVal;
2446
+ const lNum = toNumber(lVal);
2447
+ if (lNum instanceof FormulaError) return lNum;
2448
+ const rNum = toNumber(rVal);
2449
+ if (rNum instanceof FormulaError) return rNum;
2450
+ switch (op) {
2451
+ case "+":
2452
+ return lNum + rNum;
2453
+ case "-":
2454
+ return lNum - rNum;
2455
+ case "*":
2456
+ return lNum * rNum;
2457
+ case "/":
2458
+ if (rNum === 0) return new FormulaError("#DIV/0!");
2459
+ return lNum / rNum;
2460
+ case "^":
2461
+ return Math.pow(lNum, rNum);
2462
+ case "%":
2463
+ return lNum * rNum / 100;
2464
+ default:
2465
+ return new FormulaError("#ERROR!", `Unknown operator: ${op}`);
2466
+ }
2467
+ }
2468
+ evaluateUnaryOp(op, operand, context) {
2469
+ const val = this.evaluate(operand, context);
2470
+ if (val instanceof FormulaError) return val;
2471
+ const num = toNumber(val);
2472
+ if (num instanceof FormulaError) return num;
2473
+ return op === "-" ? -num : num;
2474
+ }
2475
+ compare(op, left, right) {
2476
+ if (typeof left === "number" && typeof right === "number") {
2477
+ return this.numCompare(op, left, right);
2478
+ }
2479
+ if (typeof left === "string" && typeof right === "string") {
2480
+ return this.strCompare(op, left, right);
2481
+ }
2482
+ if (typeof left === "boolean" && typeof right === "boolean") {
2483
+ return this.numCompare(op, left ? 1 : 0, right ? 1 : 0);
2484
+ }
2485
+ const lNum = toNumber(left);
2486
+ const rNum = toNumber(right);
2487
+ if (typeof lNum === "number" && typeof rNum === "number") {
2488
+ return this.numCompare(op, lNum, rNum);
2489
+ }
2490
+ return this.strCompare(op, toString(left), toString(right));
2491
+ }
2492
+ numCompare(op, a, b) {
2493
+ switch (op) {
2494
+ case ">":
2495
+ return a > b;
2496
+ case "<":
2497
+ return a < b;
2498
+ case ">=":
2499
+ return a >= b;
2500
+ case "<=":
2501
+ return a <= b;
2502
+ case "=":
2503
+ return a === b;
2504
+ case "<>":
2505
+ return a !== b;
2506
+ default:
2507
+ return false;
2508
+ }
2509
+ }
2510
+ strCompare(op, a, b) {
2511
+ const al = a.toLowerCase();
2512
+ const bl = b.toLowerCase();
2513
+ switch (op) {
2514
+ case ">":
2515
+ return al > bl;
2516
+ case "<":
2517
+ return al < bl;
2518
+ case ">=":
2519
+ return al >= bl;
2520
+ case "<=":
2521
+ return al <= bl;
2522
+ case "=":
2523
+ return al === bl;
2524
+ case "<>":
2525
+ return al !== bl;
2526
+ default:
2527
+ return false;
2528
+ }
2529
+ }
2530
+ };
2531
+ var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2532
+ var DependencyGraph = class {
2533
+ constructor() {
2534
+ this.dependencies = /* @__PURE__ */ new Map();
2535
+ this.dependents = /* @__PURE__ */ new Map();
2536
+ }
2537
+ /**
2538
+ * Set the dependencies for a cell, replacing any previous ones.
2539
+ * Updates both the forward (dependencies) and reverse (dependents) maps.
2540
+ */
2541
+ setDependencies(cell, deps) {
2542
+ this.removeDependenciesInternal(cell);
2543
+ this.dependencies.set(cell, deps);
2544
+ for (const dep of deps) {
2545
+ let depSet = this.dependents.get(dep);
2546
+ if (!depSet) {
2547
+ depSet = /* @__PURE__ */ new Set();
2548
+ this.dependents.set(dep, depSet);
2549
+ }
2550
+ depSet.add(cell);
2551
+ }
2552
+ }
2553
+ /**
2554
+ * Remove all dependency information for a cell from both maps.
2555
+ */
2556
+ removeDependencies(cell) {
2557
+ this.removeDependenciesInternal(cell);
2558
+ this.dependencies.delete(cell);
2559
+ }
2560
+ /**
2561
+ * Get all cells that directly or transitively depend on `changedCell`,
2562
+ * returned in topological order (a cell appears AFTER all cells it depends on).
2563
+ *
2564
+ * Uses Kahn's algorithm. If a cycle is detected, cycle participants are
2565
+ * appended at the end (the engine will assign #CIRC! to them).
2566
+ */
2567
+ getRecalcOrder(changedCell) {
2568
+ return this.topologicalSort(/* @__PURE__ */ new Set([changedCell]));
2569
+ }
2570
+ /**
2571
+ * Same as getRecalcOrder but for multiple changed cells.
2572
+ * Union of all transitive dependents, topologically sorted.
2573
+ */
2574
+ getRecalcOrderBatch(changedCells) {
2575
+ return this.topologicalSort(new Set(changedCells));
2576
+ }
2577
+ /**
2578
+ * Check if adding dependencies from `cell` to `deps` would create a cycle.
2579
+ * DFS from each dep: if we can reach `cell`, it would create a cycle.
2580
+ */
2581
+ wouldCreateCycle(cell, deps) {
2582
+ if (deps.has(cell)) {
2583
+ return true;
2584
+ }
2585
+ const visited = /* @__PURE__ */ new Set();
2586
+ for (const dep of deps) {
2587
+ if (this.canReach(dep, cell, visited)) {
2588
+ return true;
2589
+ }
2590
+ }
2591
+ return false;
2592
+ }
2593
+ /**
2594
+ * Return direct dependents of a cell (cells whose formulas reference this cell).
2595
+ * Returns an empty set if none.
2596
+ */
2597
+ getDependents(cell) {
2598
+ return this.dependents.get(cell) ?? EMPTY_SET;
2599
+ }
2600
+ /**
2601
+ * Return direct dependencies of a cell (cells referenced in this cell's formula).
2602
+ * Returns an empty set if none.
2603
+ */
2604
+ getDependencies(cell) {
2605
+ return this.dependencies.get(cell) ?? EMPTY_SET;
2606
+ }
2607
+ /**
2608
+ * Clear both maps entirely.
2609
+ */
2610
+ clear() {
2611
+ this.dependencies.clear();
2612
+ this.dependents.clear();
2613
+ }
2614
+ // ---------------------------------------------------------------------------
2615
+ // Private helpers
2616
+ // ---------------------------------------------------------------------------
2617
+ /**
2618
+ * Remove `cell` from the forward dependencies map and clean up reverse
2619
+ * references in the dependents map.
2620
+ */
2621
+ removeDependenciesInternal(cell) {
2622
+ const oldDeps = this.dependencies.get(cell);
2623
+ if (!oldDeps) {
2624
+ return;
2625
+ }
2626
+ for (const oldDep of oldDeps) {
2627
+ const depSet = this.dependents.get(oldDep);
2628
+ if (depSet) {
2629
+ depSet.delete(cell);
2630
+ if (depSet.size === 0) {
2631
+ this.dependents.delete(oldDep);
2632
+ }
2633
+ }
2634
+ }
2635
+ this.dependencies.delete(cell);
2636
+ }
2637
+ /**
2638
+ * Iterative DFS: check if `target` is reachable from `start` by following
2639
+ * the dependency chain. Iterative to avoid stack overflow for deep chains.
2640
+ */
2641
+ canReach(start, target, visited) {
2642
+ if (start === target) return true;
2643
+ if (visited.has(start)) return false;
2644
+ const stack = [start];
2645
+ visited.add(start);
2646
+ while (stack.length > 0) {
2647
+ const current = stack.pop();
2648
+ const deps = this.dependencies.get(current);
2649
+ if (!deps) continue;
2650
+ for (const dep of deps) {
2651
+ if (dep === target) return true;
2652
+ if (!visited.has(dep)) {
2653
+ visited.add(dep);
2654
+ stack.push(dep);
2655
+ }
2656
+ }
2657
+ }
2658
+ return false;
2659
+ }
2660
+ /**
2661
+ * Topological sort using Kahn's algorithm.
2662
+ *
2663
+ * 1. Collect all cells transitively dependent on the changed cell(s).
2664
+ * 2. Build in-degree map for these cells (count how many of their
2665
+ * dependencies are in the affected set).
2666
+ * 3. Start with cells whose in-degree is 0 (only depend on unaffected
2667
+ * cells or the changed cells themselves).
2668
+ * 4. Process queue: for each cell, reduce in-degree of its dependents,
2669
+ * add to queue when in-degree reaches 0.
2670
+ * 5. If any cells remain unprocessed, they're in a cycle — append them
2671
+ * at the end (engine marks as #CIRC!).
2672
+ */
2673
+ topologicalSort(changedCells) {
2674
+ const affected = /* @__PURE__ */ new Set();
2675
+ const bfsQueue = [];
2676
+ for (const changed of changedCells) {
2677
+ const directDependents = this.dependents.get(changed);
2678
+ if (directDependents) {
2679
+ for (const dep of directDependents) {
2680
+ if (!affected.has(dep)) {
2681
+ affected.add(dep);
2682
+ bfsQueue.push(dep);
2683
+ }
2684
+ }
2685
+ }
2686
+ }
2687
+ let head = 0;
2688
+ while (head < bfsQueue.length) {
2689
+ const current = bfsQueue[head++];
2690
+ const currentDependents = this.dependents.get(current);
2691
+ if (currentDependents) {
2692
+ for (const dep of currentDependents) {
2693
+ if (!affected.has(dep)) {
2694
+ affected.add(dep);
2695
+ bfsQueue.push(dep);
2696
+ }
2697
+ }
2698
+ }
2699
+ }
2700
+ if (affected.size === 0) {
2701
+ return [];
2702
+ }
2703
+ const inDegree = /* @__PURE__ */ new Map();
2704
+ for (const cell of affected) {
2705
+ let degree = 0;
2706
+ const deps = this.dependencies.get(cell);
2707
+ if (deps) {
2708
+ for (const dep of deps) {
2709
+ if (affected.has(dep)) {
2710
+ degree++;
2711
+ }
2712
+ }
2713
+ }
2714
+ inDegree.set(cell, degree);
2715
+ }
2716
+ const queue = [];
2717
+ for (const [cell, degree] of inDegree) {
2718
+ if (degree === 0) {
2719
+ queue.push(cell);
2720
+ }
2721
+ }
2722
+ const result = [];
2723
+ let queueHead = 0;
2724
+ while (queueHead < queue.length) {
2725
+ const cell = queue[queueHead++];
2726
+ result.push(cell);
2727
+ const cellDependents = this.dependents.get(cell);
2728
+ if (cellDependents) {
2729
+ for (const dependent of cellDependents) {
2730
+ if (affected.has(dependent)) {
2731
+ const newDegree = inDegree.get(dependent) - 1;
2732
+ inDegree.set(dependent, newDegree);
2733
+ if (newDegree === 0) {
2734
+ queue.push(dependent);
2735
+ }
2736
+ }
2737
+ }
2738
+ }
2739
+ }
2740
+ if (result.length < affected.size) {
2741
+ const resultSet = new Set(result);
2742
+ for (const cell of affected) {
2743
+ if (!resultSet.has(cell)) {
2744
+ result.push(cell);
2745
+ }
2746
+ }
2747
+ }
2748
+ return result;
2749
+ }
2750
+ };
2751
+ function registerMathFunctions(registry) {
2752
+ registry.set("SUM", {
2753
+ minArgs: 1,
2754
+ maxArgs: -1,
2755
+ evaluate(args, context, evaluator) {
2756
+ const values = flattenArgs(args, context, evaluator);
2757
+ let sum = 0;
2758
+ for (const val of values) {
2759
+ if (val instanceof FormulaError) return val;
2760
+ if (typeof val === "number") {
2761
+ sum += val;
2762
+ } else if (typeof val === "boolean") {
2763
+ sum += val ? 1 : 0;
2764
+ }
2765
+ }
2766
+ return sum;
2767
+ }
2768
+ });
2769
+ registry.set("AVERAGE", {
2770
+ minArgs: 1,
2771
+ maxArgs: -1,
2772
+ evaluate(args, context, evaluator) {
2773
+ const values = flattenArgs(args, context, evaluator);
2774
+ let sum = 0;
2775
+ let count = 0;
2776
+ for (const val of values) {
2777
+ if (val instanceof FormulaError) return val;
2778
+ if (typeof val === "number") {
2779
+ sum += val;
2780
+ count++;
2781
+ } else if (typeof val === "boolean") {
2782
+ sum += val ? 1 : 0;
2783
+ count++;
2784
+ }
2785
+ }
2786
+ if (count === 0) return new FormulaError("#DIV/0!", "No numeric values for AVERAGE");
2787
+ return sum / count;
2788
+ }
2789
+ });
2790
+ registry.set("MIN", {
2791
+ minArgs: 1,
2792
+ maxArgs: -1,
2793
+ evaluate(args, context, evaluator) {
2794
+ const values = flattenArgs(args, context, evaluator);
2795
+ let min = Infinity;
2796
+ for (const val of values) {
2797
+ if (val instanceof FormulaError) return val;
2798
+ if (typeof val === "number") {
2799
+ if (val < min) min = val;
2800
+ } else if (typeof val === "boolean") {
2801
+ const n = val ? 1 : 0;
2802
+ if (n < min) min = n;
2803
+ }
2804
+ }
2805
+ return min === Infinity ? 0 : min;
2806
+ }
2807
+ });
2808
+ registry.set("MAX", {
2809
+ minArgs: 1,
2810
+ maxArgs: -1,
2811
+ evaluate(args, context, evaluator) {
2812
+ const values = flattenArgs(args, context, evaluator);
2813
+ let max = -Infinity;
2814
+ for (const val of values) {
2815
+ if (val instanceof FormulaError) return val;
2816
+ if (typeof val === "number") {
2817
+ if (val > max) max = val;
2818
+ } else if (typeof val === "boolean") {
2819
+ const n = val ? 1 : 0;
2820
+ if (n > max) max = n;
2821
+ }
2822
+ }
2823
+ return max === -Infinity ? 0 : max;
2824
+ }
2825
+ });
2826
+ registry.set("COUNT", {
2827
+ minArgs: 1,
2828
+ maxArgs: -1,
2829
+ evaluate(args, context, evaluator) {
2830
+ const values = flattenArgs(args, context, evaluator);
2831
+ let count = 0;
2832
+ for (const val of values) {
2833
+ if (val instanceof FormulaError) return val;
2834
+ if (typeof val === "number") {
2835
+ count++;
2836
+ }
2837
+ }
2838
+ return count;
2839
+ }
2840
+ });
2841
+ registry.set("COUNTA", {
2842
+ minArgs: 1,
2843
+ maxArgs: -1,
2844
+ evaluate(args, context, evaluator) {
2845
+ const values = flattenArgs(args, context, evaluator);
2846
+ let count = 0;
2847
+ for (const val of values) {
2848
+ if (val instanceof FormulaError) return val;
2849
+ if (val !== null && val !== void 0 && val !== "") {
2850
+ count++;
2851
+ }
2852
+ }
2853
+ return count;
2854
+ }
2855
+ });
2856
+ registry.set("ROUND", {
2857
+ minArgs: 2,
2858
+ maxArgs: 2,
2859
+ evaluate(args, context, evaluator) {
2860
+ const rawNum = evaluator.evaluate(args[0], context);
2861
+ if (rawNum instanceof FormulaError) return rawNum;
2862
+ const num = toNumber(rawNum);
2863
+ if (num instanceof FormulaError) return num;
2864
+ const rawDigits = evaluator.evaluate(args[1], context);
2865
+ if (rawDigits instanceof FormulaError) return rawDigits;
2866
+ const digits = toNumber(rawDigits);
2867
+ if (digits instanceof FormulaError) return digits;
2868
+ const factor = Math.pow(10, Math.trunc(digits));
2869
+ return Math.round(num * factor) / factor;
2870
+ }
2871
+ });
2872
+ registry.set("ABS", {
2873
+ minArgs: 1,
2874
+ maxArgs: 1,
2875
+ evaluate(args, context, evaluator) {
2876
+ const rawVal = evaluator.evaluate(args[0], context);
2877
+ if (rawVal instanceof FormulaError) return rawVal;
2878
+ const num = toNumber(rawVal);
2879
+ if (num instanceof FormulaError) return num;
2880
+ return Math.abs(num);
2881
+ }
2882
+ });
2883
+ registry.set("CEILING", {
2884
+ minArgs: 2,
2885
+ maxArgs: 2,
2886
+ evaluate(args, context, evaluator) {
2887
+ const rawNum = evaluator.evaluate(args[0], context);
2888
+ if (rawNum instanceof FormulaError) return rawNum;
2889
+ const num = toNumber(rawNum);
2890
+ if (num instanceof FormulaError) return num;
2891
+ const rawSig = evaluator.evaluate(args[1], context);
2892
+ if (rawSig instanceof FormulaError) return rawSig;
2893
+ const significance = toNumber(rawSig);
2894
+ if (significance instanceof FormulaError) return significance;
2895
+ if (significance === 0) return 0;
2896
+ return Math.ceil(num / significance) * significance;
2897
+ }
2898
+ });
2899
+ registry.set("FLOOR", {
2900
+ minArgs: 2,
2901
+ maxArgs: 2,
2902
+ evaluate(args, context, evaluator) {
2903
+ const rawNum = evaluator.evaluate(args[0], context);
2904
+ if (rawNum instanceof FormulaError) return rawNum;
2905
+ const num = toNumber(rawNum);
2906
+ if (num instanceof FormulaError) return num;
2907
+ const rawSig = evaluator.evaluate(args[1], context);
2908
+ if (rawSig instanceof FormulaError) return rawSig;
2909
+ const significance = toNumber(rawSig);
2910
+ if (significance instanceof FormulaError) return significance;
2911
+ if (significance === 0) return 0;
2912
+ return Math.floor(num / significance) * significance;
2913
+ }
2914
+ });
2915
+ registry.set("MOD", {
2916
+ minArgs: 2,
2917
+ maxArgs: 2,
2918
+ evaluate(args, context, evaluator) {
2919
+ const rawNum = evaluator.evaluate(args[0], context);
2920
+ if (rawNum instanceof FormulaError) return rawNum;
2921
+ const num = toNumber(rawNum);
2922
+ if (num instanceof FormulaError) return num;
2923
+ const rawDiv = evaluator.evaluate(args[1], context);
2924
+ if (rawDiv instanceof FormulaError) return rawDiv;
2925
+ const divisor = toNumber(rawDiv);
2926
+ if (divisor instanceof FormulaError) return divisor;
2927
+ if (divisor === 0) return new FormulaError("#DIV/0!", "Division by zero in MOD");
2928
+ return num % divisor;
2929
+ }
2930
+ });
2931
+ registry.set("POWER", {
2932
+ minArgs: 2,
2933
+ maxArgs: 2,
2934
+ evaluate(args, context, evaluator) {
2935
+ const rawBase = evaluator.evaluate(args[0], context);
2936
+ if (rawBase instanceof FormulaError) return rawBase;
2937
+ const base = toNumber(rawBase);
2938
+ if (base instanceof FormulaError) return base;
2939
+ const rawExp = evaluator.evaluate(args[1], context);
2940
+ if (rawExp instanceof FormulaError) return rawExp;
2941
+ const exponent = toNumber(rawExp);
2942
+ if (exponent instanceof FormulaError) return exponent;
2943
+ return Math.pow(base, exponent);
2944
+ }
2945
+ });
2946
+ registry.set("SQRT", {
2947
+ minArgs: 1,
2948
+ maxArgs: 1,
2949
+ evaluate(args, context, evaluator) {
2950
+ const rawVal = evaluator.evaluate(args[0], context);
2951
+ if (rawVal instanceof FormulaError) return rawVal;
2952
+ const num = toNumber(rawVal);
2953
+ if (num instanceof FormulaError) return num;
2954
+ if (num < 0) return new FormulaError("#VALUE!", "Cannot take square root of negative number");
2955
+ return Math.sqrt(num);
2956
+ }
2957
+ });
2958
+ registry.set("ROUNDUP", {
2959
+ minArgs: 2,
2960
+ maxArgs: 2,
2961
+ evaluate(args, context, evaluator) {
2962
+ const rawNum = evaluator.evaluate(args[0], context);
2963
+ if (rawNum instanceof FormulaError) return rawNum;
2964
+ const num = toNumber(rawNum);
2965
+ if (num instanceof FormulaError) return num;
2966
+ const rawDigits = evaluator.evaluate(args[1], context);
2967
+ if (rawDigits instanceof FormulaError) return rawDigits;
2968
+ const digits = toNumber(rawDigits);
2969
+ if (digits instanceof FormulaError) return digits;
2970
+ const factor = Math.pow(10, Math.trunc(digits));
2971
+ return num >= 0 ? Math.ceil(num * factor) / factor : Math.floor(num * factor) / factor;
2972
+ }
2973
+ });
2974
+ registry.set("ROUNDDOWN", {
2975
+ minArgs: 2,
2976
+ maxArgs: 2,
2977
+ evaluate(args, context, evaluator) {
2978
+ const rawNum = evaluator.evaluate(args[0], context);
2979
+ if (rawNum instanceof FormulaError) return rawNum;
2980
+ const num = toNumber(rawNum);
2981
+ if (num instanceof FormulaError) return num;
2982
+ const rawDigits = evaluator.evaluate(args[1], context);
2983
+ if (rawDigits instanceof FormulaError) return rawDigits;
2984
+ const digits = toNumber(rawDigits);
2985
+ if (digits instanceof FormulaError) return digits;
2986
+ const factor = Math.pow(10, Math.trunc(digits));
2987
+ return Math.trunc(num * factor) / factor;
2988
+ }
2989
+ });
2990
+ registry.set("INT", {
2991
+ minArgs: 1,
2992
+ maxArgs: 1,
2993
+ evaluate(args, context, evaluator) {
2994
+ const rawVal = evaluator.evaluate(args[0], context);
2995
+ if (rawVal instanceof FormulaError) return rawVal;
2996
+ const num = toNumber(rawVal);
2997
+ if (num instanceof FormulaError) return num;
2998
+ return Math.floor(num);
2999
+ }
3000
+ });
3001
+ registry.set("TRUNC", {
3002
+ minArgs: 1,
3003
+ maxArgs: 2,
3004
+ evaluate(args, context, evaluator) {
3005
+ const rawVal = evaluator.evaluate(args[0], context);
3006
+ if (rawVal instanceof FormulaError) return rawVal;
3007
+ const num = toNumber(rawVal);
3008
+ if (num instanceof FormulaError) return num;
3009
+ let digits = 0;
3010
+ if (args.length >= 2) {
3011
+ const rawD = evaluator.evaluate(args[1], context);
3012
+ if (rawD instanceof FormulaError) return rawD;
3013
+ const d = toNumber(rawD);
3014
+ if (d instanceof FormulaError) return d;
3015
+ digits = Math.trunc(d);
3016
+ }
3017
+ const factor = Math.pow(10, digits);
3018
+ return Math.trunc(num * factor) / factor;
3019
+ }
3020
+ });
3021
+ registry.set("PRODUCT", {
3022
+ minArgs: 1,
3023
+ maxArgs: -1,
3024
+ evaluate(args, context, evaluator) {
3025
+ const values = flattenArgs(args, context, evaluator);
3026
+ let product = 1;
3027
+ let hasNumber = false;
3028
+ for (const val of values) {
3029
+ if (val instanceof FormulaError) return val;
3030
+ if (typeof val === "number") {
3031
+ product *= val;
3032
+ hasNumber = true;
3033
+ }
3034
+ }
3035
+ return hasNumber ? product : 0;
3036
+ }
3037
+ });
3038
+ registry.set("SUMPRODUCT", {
3039
+ minArgs: 1,
3040
+ maxArgs: -1,
3041
+ evaluate(args, context, evaluator) {
3042
+ const arrays = [];
3043
+ for (const arg of args) {
3044
+ if (arg.kind !== "range") {
3045
+ return new FormulaError("#VALUE!", "SUMPRODUCT arguments must be ranges");
3046
+ }
3047
+ arrays.push(context.getRangeValues({ start: arg.start, end: arg.end }));
3048
+ }
3049
+ if (arrays.length === 0) return 0;
3050
+ const rows = arrays[0].length;
3051
+ const cols = rows > 0 ? arrays[0][0].length : 0;
3052
+ for (let a = 1; a < arrays.length; a++) {
3053
+ if (arrays[a].length !== rows || rows > 0 && arrays[a][0].length !== cols) {
3054
+ return new FormulaError("#VALUE!", "SUMPRODUCT arrays must have same dimensions");
3055
+ }
3056
+ }
3057
+ let sum = 0;
3058
+ for (let r = 0; r < rows; r++) {
3059
+ for (let c = 0; c < cols; c++) {
3060
+ let product = 1;
3061
+ for (let a = 0; a < arrays.length; a++) {
3062
+ const v = toNumber(arrays[a][r][c]);
3063
+ if (v instanceof FormulaError) {
3064
+ product = 0;
3065
+ break;
3066
+ }
3067
+ product *= v;
3068
+ }
3069
+ sum += product;
3070
+ }
3071
+ }
3072
+ return sum;
3073
+ }
3074
+ });
3075
+ registry.set("MEDIAN", {
3076
+ minArgs: 1,
3077
+ maxArgs: -1,
3078
+ evaluate(args, context, evaluator) {
3079
+ const values = flattenArgs(args, context, evaluator);
3080
+ const nums = [];
3081
+ for (const val of values) {
3082
+ if (val instanceof FormulaError) return val;
3083
+ if (typeof val === "number") nums.push(val);
3084
+ }
3085
+ if (nums.length === 0) return new FormulaError("#NUM!", "No numeric values for MEDIAN");
3086
+ nums.sort((a, b) => a - b);
3087
+ const mid = Math.floor(nums.length / 2);
3088
+ return nums.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
3089
+ }
3090
+ });
3091
+ registry.set("LARGE", {
3092
+ minArgs: 2,
3093
+ maxArgs: 2,
3094
+ evaluate(args, context, evaluator) {
3095
+ if (args[0].kind !== "range") {
3096
+ return new FormulaError("#VALUE!", "LARGE first argument must be a range");
3097
+ }
3098
+ const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
3099
+ const rawK = evaluator.evaluate(args[1], context);
3100
+ if (rawK instanceof FormulaError) return rawK;
3101
+ const k = toNumber(rawK);
3102
+ if (k instanceof FormulaError) return k;
3103
+ const nums = [];
3104
+ for (const row of rangeData) {
3105
+ for (const cell of row) {
3106
+ if (typeof cell === "number") nums.push(cell);
3107
+ }
3108
+ }
3109
+ const ki = Math.trunc(k);
3110
+ if (ki < 1 || ki > nums.length) return new FormulaError("#NUM!", "LARGE k out of range");
3111
+ nums.sort((a, b) => b - a);
3112
+ return nums[ki - 1];
3113
+ }
3114
+ });
3115
+ registry.set("SMALL", {
3116
+ minArgs: 2,
3117
+ maxArgs: 2,
3118
+ evaluate(args, context, evaluator) {
3119
+ if (args[0].kind !== "range") {
3120
+ return new FormulaError("#VALUE!", "SMALL first argument must be a range");
3121
+ }
3122
+ const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
3123
+ const rawK = evaluator.evaluate(args[1], context);
3124
+ if (rawK instanceof FormulaError) return rawK;
3125
+ const k = toNumber(rawK);
3126
+ if (k instanceof FormulaError) return k;
3127
+ const nums = [];
3128
+ for (const row of rangeData) {
3129
+ for (const cell of row) {
3130
+ if (typeof cell === "number") nums.push(cell);
3131
+ }
3132
+ }
3133
+ const ki = Math.trunc(k);
3134
+ if (ki < 1 || ki > nums.length) return new FormulaError("#NUM!", "SMALL k out of range");
3135
+ nums.sort((a, b) => a - b);
3136
+ return nums[ki - 1];
3137
+ }
3138
+ });
3139
+ registry.set("RANK", {
3140
+ minArgs: 2,
3141
+ maxArgs: 3,
3142
+ evaluate(args, context, evaluator) {
3143
+ const rawNum = evaluator.evaluate(args[0], context);
3144
+ if (rawNum instanceof FormulaError) return rawNum;
3145
+ const num = toNumber(rawNum);
3146
+ if (num instanceof FormulaError) return num;
3147
+ if (args[1].kind !== "range") {
3148
+ return new FormulaError("#VALUE!", "RANK second argument must be a range");
3149
+ }
3150
+ const rangeData = context.getRangeValues({ start: args[1].start, end: args[1].end });
3151
+ let order = 0;
3152
+ if (args.length >= 3) {
3153
+ const rawO = evaluator.evaluate(args[2], context);
3154
+ if (rawO instanceof FormulaError) return rawO;
3155
+ const o = toNumber(rawO);
3156
+ if (o instanceof FormulaError) return o;
3157
+ order = o;
3158
+ }
3159
+ const nums = [];
3160
+ for (const row of rangeData) {
3161
+ for (const cell of row) {
3162
+ if (typeof cell === "number") nums.push(cell);
3163
+ }
3164
+ }
3165
+ if (!nums.includes(num)) return new FormulaError("#N/A", "RANK value not found in range");
3166
+ let rank = 1;
3167
+ for (const n of nums) {
3168
+ if (order === 0 ? n > num : n < num) rank++;
3169
+ }
3170
+ return rank;
3171
+ }
3172
+ });
3173
+ registry.set("SIGN", {
3174
+ minArgs: 1,
3175
+ maxArgs: 1,
3176
+ evaluate(args, context, evaluator) {
3177
+ const rawVal = evaluator.evaluate(args[0], context);
3178
+ if (rawVal instanceof FormulaError) return rawVal;
3179
+ const num = toNumber(rawVal);
3180
+ if (num instanceof FormulaError) return num;
3181
+ return num > 0 ? 1 : num < 0 ? -1 : 0;
3182
+ }
3183
+ });
3184
+ registry.set("LOG", {
3185
+ minArgs: 1,
3186
+ maxArgs: 2,
3187
+ evaluate(args, context, evaluator) {
3188
+ const rawVal = evaluator.evaluate(args[0], context);
3189
+ if (rawVal instanceof FormulaError) return rawVal;
3190
+ const num = toNumber(rawVal);
3191
+ if (num instanceof FormulaError) return num;
3192
+ if (num <= 0) return new FormulaError("#NUM!", "LOG requires a positive number");
3193
+ let base = 10;
3194
+ if (args.length >= 2) {
3195
+ const rawB = evaluator.evaluate(args[1], context);
3196
+ if (rawB instanceof FormulaError) return rawB;
3197
+ const b = toNumber(rawB);
3198
+ if (b instanceof FormulaError) return b;
3199
+ if (b <= 0 || b === 1) return new FormulaError("#NUM!", "LOG base must be positive and not 1");
3200
+ base = b;
3201
+ }
3202
+ return Math.log(num) / Math.log(base);
3203
+ }
3204
+ });
3205
+ registry.set("LN", {
3206
+ minArgs: 1,
3207
+ maxArgs: 1,
3208
+ evaluate(args, context, evaluator) {
3209
+ const rawVal = evaluator.evaluate(args[0], context);
3210
+ if (rawVal instanceof FormulaError) return rawVal;
3211
+ const num = toNumber(rawVal);
3212
+ if (num instanceof FormulaError) return num;
3213
+ if (num <= 0) return new FormulaError("#NUM!", "LN requires a positive number");
3214
+ return Math.log(num);
3215
+ }
3216
+ });
3217
+ registry.set("EXP", {
3218
+ minArgs: 1,
3219
+ maxArgs: 1,
3220
+ evaluate(args, context, evaluator) {
3221
+ const rawVal = evaluator.evaluate(args[0], context);
3222
+ if (rawVal instanceof FormulaError) return rawVal;
3223
+ const num = toNumber(rawVal);
3224
+ if (num instanceof FormulaError) return num;
3225
+ return Math.exp(num);
3226
+ }
3227
+ });
3228
+ registry.set("PI", {
3229
+ minArgs: 0,
3230
+ maxArgs: 0,
3231
+ evaluate() {
3232
+ return Math.PI;
3233
+ }
3234
+ });
3235
+ registry.set("RAND", {
3236
+ minArgs: 0,
3237
+ maxArgs: 0,
3238
+ evaluate() {
3239
+ return Math.random();
3240
+ }
3241
+ });
3242
+ registry.set("RANDBETWEEN", {
3243
+ minArgs: 2,
3244
+ maxArgs: 2,
3245
+ evaluate(args, context, evaluator) {
3246
+ const rawLow = evaluator.evaluate(args[0], context);
3247
+ if (rawLow instanceof FormulaError) return rawLow;
3248
+ const low = toNumber(rawLow);
3249
+ if (low instanceof FormulaError) return low;
3250
+ const rawHigh = evaluator.evaluate(args[1], context);
3251
+ if (rawHigh instanceof FormulaError) return rawHigh;
3252
+ const high = toNumber(rawHigh);
3253
+ if (high instanceof FormulaError) return high;
3254
+ const lo = Math.ceil(low);
3255
+ const hi = Math.floor(high);
3256
+ if (lo > hi) return new FormulaError("#NUM!", "RANDBETWEEN bottom must be <= top");
3257
+ return Math.floor(Math.random() * (hi - lo + 1)) + lo;
3258
+ }
3259
+ });
3260
+ }
3261
+ function flattenArgs2(args, context, evaluator) {
3262
+ const result = [];
3263
+ for (const arg of args) {
3264
+ if (arg.kind === "range") {
3265
+ const values = context.getRangeValues({ start: arg.start, end: arg.end });
3266
+ for (const row of values) {
3267
+ for (const cell of row) {
3268
+ result.push(cell);
3269
+ }
3270
+ }
3271
+ } else {
3272
+ result.push(evaluator.evaluate(arg, context));
3273
+ }
3274
+ }
3275
+ return result;
3276
+ }
3277
+ function registerLogicalFunctions(registry) {
3278
+ registry.set("IF", {
3279
+ minArgs: 2,
3280
+ maxArgs: 3,
3281
+ evaluate(args, context, evaluator) {
3282
+ const condition = evaluator.evaluate(args[0], context);
3283
+ if (condition instanceof FormulaError) return condition;
3284
+ if (condition) {
3285
+ return evaluator.evaluate(args[1], context);
3286
+ } else {
3287
+ if (args.length >= 3) {
3288
+ return evaluator.evaluate(args[2], context);
3289
+ }
3290
+ return false;
3291
+ }
3292
+ }
3293
+ });
3294
+ registry.set("AND", {
3295
+ minArgs: 1,
3296
+ maxArgs: -1,
3297
+ evaluate(args, context, evaluator) {
3298
+ const values = flattenArgs2(args, context, evaluator);
3299
+ for (const val of values) {
3300
+ if (val instanceof FormulaError) return val;
3301
+ if (!val) return false;
3302
+ }
3303
+ return true;
3304
+ }
3305
+ });
3306
+ registry.set("OR", {
3307
+ minArgs: 1,
3308
+ maxArgs: -1,
3309
+ evaluate(args, context, evaluator) {
3310
+ const values = flattenArgs2(args, context, evaluator);
3311
+ for (const val of values) {
3312
+ if (val instanceof FormulaError) return val;
3313
+ if (val) return true;
3314
+ }
3315
+ return false;
3316
+ }
3317
+ });
3318
+ registry.set("NOT", {
3319
+ minArgs: 1,
3320
+ maxArgs: 1,
3321
+ evaluate(args, context, evaluator) {
3322
+ const val = evaluator.evaluate(args[0], context);
3323
+ if (val instanceof FormulaError) return val;
3324
+ return !val;
3325
+ }
3326
+ });
3327
+ registry.set("IFERROR", {
3328
+ minArgs: 2,
3329
+ maxArgs: 2,
3330
+ evaluate(args, context, evaluator) {
3331
+ const val = evaluator.evaluate(args[0], context);
3332
+ if (val instanceof FormulaError) {
3333
+ return evaluator.evaluate(args[1], context);
3334
+ }
3335
+ return val;
3336
+ }
3337
+ });
3338
+ registry.set("IFNA", {
3339
+ minArgs: 2,
3340
+ maxArgs: 2,
3341
+ evaluate(args, context, evaluator) {
3342
+ const val = evaluator.evaluate(args[0], context);
3343
+ if (val instanceof FormulaError && val.type === "#N/A") {
3344
+ return evaluator.evaluate(args[1], context);
3345
+ }
3346
+ return val;
3347
+ }
3348
+ });
3349
+ registry.set("IFS", {
3350
+ minArgs: 2,
3351
+ maxArgs: -1,
3352
+ evaluate(args, context, evaluator) {
3353
+ if (args.length % 2 !== 0) {
3354
+ return new FormulaError("#VALUE!", "IFS requires pairs of condition, value");
3355
+ }
3356
+ for (let i = 0; i < args.length; i += 2) {
3357
+ const condition = evaluator.evaluate(args[i], context);
3358
+ if (condition instanceof FormulaError) return condition;
3359
+ if (condition) {
3360
+ return evaluator.evaluate(args[i + 1], context);
3361
+ }
3362
+ }
3363
+ return new FormulaError("#N/A", "IFS no condition was TRUE");
3364
+ }
3365
+ });
3366
+ registry.set("SWITCH", {
3367
+ minArgs: 3,
3368
+ maxArgs: -1,
3369
+ evaluate(args, context, evaluator) {
3370
+ const expr = evaluator.evaluate(args[0], context);
3371
+ if (expr instanceof FormulaError) return expr;
3372
+ const hasDefault = (args.length - 1) % 2 !== 0;
3373
+ const pairCount = hasDefault ? (args.length - 2) / 2 : (args.length - 1) / 2;
3374
+ for (let i = 0; i < pairCount; i++) {
3375
+ const caseVal = evaluator.evaluate(args[1 + i * 2], context);
3376
+ if (caseVal instanceof FormulaError) return caseVal;
3377
+ if (expr === caseVal) {
3378
+ return evaluator.evaluate(args[2 + i * 2], context);
3379
+ }
3380
+ }
3381
+ if (hasDefault) {
3382
+ return evaluator.evaluate(args[args.length - 1], context);
3383
+ }
3384
+ return new FormulaError("#N/A", "SWITCH no match found");
3385
+ }
3386
+ });
3387
+ registry.set("CHOOSE", {
3388
+ minArgs: 2,
3389
+ maxArgs: -1,
3390
+ evaluate(args, context, evaluator) {
3391
+ const rawIdx = evaluator.evaluate(args[0], context);
3392
+ if (rawIdx instanceof FormulaError) return rawIdx;
3393
+ if (typeof rawIdx !== "number") return new FormulaError("#VALUE!", "CHOOSE index must be a number");
3394
+ const idx = Math.trunc(rawIdx);
3395
+ if (idx < 1 || idx >= args.length) {
3396
+ return new FormulaError("#VALUE!", "CHOOSE index out of range");
3397
+ }
3398
+ return evaluator.evaluate(args[idx], context);
3399
+ }
3400
+ });
3401
+ registry.set("XOR", {
3402
+ minArgs: 1,
3403
+ maxArgs: -1,
3404
+ evaluate(args, context, evaluator) {
3405
+ const values = flattenArgs2(args, context, evaluator);
3406
+ let trueCount = 0;
3407
+ for (const val of values) {
3408
+ if (val instanceof FormulaError) return val;
3409
+ if (val) trueCount++;
3410
+ }
3411
+ return trueCount % 2 === 1;
3412
+ }
3413
+ });
3414
+ }
3415
+ function registerLookupFunctions(registry) {
3416
+ registry.set("VLOOKUP", {
3417
+ minArgs: 3,
3418
+ maxArgs: 4,
3419
+ evaluate(args, context, evaluator) {
3420
+ const lookupValue = evaluator.evaluate(args[0], context);
3421
+ if (lookupValue instanceof FormulaError) return lookupValue;
3422
+ if (args[1].kind !== "range") {
3423
+ return new FormulaError("#VALUE!", "VLOOKUP table_array must be a range");
3424
+ }
3425
+ const tableData = context.getRangeValues({ start: args[1].start, end: args[1].end });
3426
+ const rawColIndex = evaluator.evaluate(args[2], context);
3427
+ if (rawColIndex instanceof FormulaError) return rawColIndex;
3428
+ const colIndex = toNumber(rawColIndex);
3429
+ if (colIndex instanceof FormulaError) return colIndex;
3430
+ if (colIndex < 1) return new FormulaError("#VALUE!", "VLOOKUP col_index must be >= 1");
3431
+ if (tableData.length > 0 && colIndex > tableData[0].length) {
3432
+ return new FormulaError("#REF!", "VLOOKUP col_index exceeds table columns");
3433
+ }
3434
+ let rangeLookup = true;
3435
+ if (args.length >= 4) {
3436
+ const rawRL = evaluator.evaluate(args[3], context);
3437
+ if (rawRL instanceof FormulaError) return rawRL;
3438
+ rangeLookup = !!rawRL;
3439
+ }
3440
+ const col = Math.trunc(colIndex) - 1;
3441
+ const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
3442
+ if (rangeLookup) {
3443
+ let bestRow = -1;
3444
+ for (let r = 0; r < tableData.length; r++) {
3445
+ const cellVal = tableData[r][0];
3446
+ if (cellVal === null || cellVal === void 0) continue;
3447
+ if (typeof lookupValue === "number" && typeof cellVal === "number") {
3448
+ if (cellVal <= lookupValue) bestRow = r;
3449
+ else break;
3450
+ } else if (lookupLower !== null && typeof cellVal === "string") {
3451
+ if (cellVal.toLowerCase() <= lookupLower) bestRow = r;
3452
+ else break;
3453
+ }
3454
+ }
3455
+ if (bestRow === -1) return new FormulaError("#N/A", "VLOOKUP no match found");
3456
+ return tableData[bestRow][col] ?? null;
3457
+ } else {
3458
+ for (let r = 0; r < tableData.length; r++) {
3459
+ const cellVal = tableData[r][0];
3460
+ if (lookupLower !== null && typeof cellVal === "string") {
3461
+ if (cellVal.toLowerCase() === lookupLower) {
3462
+ return tableData[r][col] ?? null;
3463
+ }
3464
+ } else if (cellVal === lookupValue) {
3465
+ return tableData[r][col] ?? null;
3466
+ }
3467
+ }
3468
+ return new FormulaError("#N/A", "VLOOKUP no exact match found");
3469
+ }
3470
+ }
3471
+ });
3472
+ registry.set("INDEX", {
3473
+ minArgs: 2,
3474
+ maxArgs: 3,
3475
+ evaluate(args, context, evaluator) {
3476
+ if (args[0].kind !== "range") {
3477
+ return new FormulaError("#VALUE!", "INDEX first argument must be a range");
3478
+ }
3479
+ const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
3480
+ const rawRow = evaluator.evaluate(args[1], context);
3481
+ if (rawRow instanceof FormulaError) return rawRow;
3482
+ const rowNum = toNumber(rawRow);
3483
+ if (rowNum instanceof FormulaError) return rowNum;
3484
+ let colNum = 1;
3485
+ if (args.length >= 3) {
3486
+ const rawCol = evaluator.evaluate(args[2], context);
3487
+ if (rawCol instanceof FormulaError) return rawCol;
3488
+ const c2 = toNumber(rawCol);
3489
+ if (c2 instanceof FormulaError) return c2;
3490
+ colNum = c2;
3491
+ }
3492
+ const r = Math.trunc(rowNum) - 1;
3493
+ const c = Math.trunc(colNum) - 1;
3494
+ if (r < 0 || r >= rangeData.length) {
3495
+ return new FormulaError("#REF!", "INDEX row out of bounds");
3496
+ }
3497
+ if (c < 0 || rangeData.length > 0 && c >= rangeData[0].length) {
3498
+ return new FormulaError("#REF!", "INDEX column out of bounds");
3499
+ }
3500
+ return rangeData[r][c] ?? null;
3501
+ }
3502
+ });
3503
+ registry.set("HLOOKUP", {
3504
+ minArgs: 3,
3505
+ maxArgs: 4,
3506
+ evaluate(args, context, evaluator) {
3507
+ const lookupValue = evaluator.evaluate(args[0], context);
3508
+ if (lookupValue instanceof FormulaError) return lookupValue;
3509
+ if (args[1].kind !== "range") {
3510
+ return new FormulaError("#VALUE!", "HLOOKUP table_array must be a range");
3511
+ }
3512
+ const tableData = context.getRangeValues({ start: args[1].start, end: args[1].end });
3513
+ const rawRowIndex = evaluator.evaluate(args[2], context);
3514
+ if (rawRowIndex instanceof FormulaError) return rawRowIndex;
3515
+ const rowIndex = toNumber(rawRowIndex);
3516
+ if (rowIndex instanceof FormulaError) return rowIndex;
3517
+ if (rowIndex < 1) return new FormulaError("#VALUE!", "HLOOKUP row_index must be >= 1");
3518
+ if (rowIndex > tableData.length) {
3519
+ return new FormulaError("#REF!", "HLOOKUP row_index exceeds table rows");
3520
+ }
3521
+ let rangeLookup = true;
3522
+ if (args.length >= 4) {
3523
+ const rawRL = evaluator.evaluate(args[3], context);
3524
+ if (rawRL instanceof FormulaError) return rawRL;
3525
+ rangeLookup = !!rawRL;
3526
+ }
3527
+ const row = Math.trunc(rowIndex) - 1;
3528
+ const firstRow = tableData[0] || [];
3529
+ const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
3530
+ if (rangeLookup) {
3531
+ let bestCol = -1;
3532
+ for (let c = 0; c < firstRow.length; c++) {
3533
+ const cellVal = firstRow[c];
3534
+ if (cellVal === null || cellVal === void 0) continue;
3535
+ if (typeof lookupValue === "number" && typeof cellVal === "number") {
3536
+ if (cellVal <= lookupValue) bestCol = c;
3537
+ else break;
3538
+ } else if (lookupLower !== null && typeof cellVal === "string") {
3539
+ if (cellVal.toLowerCase() <= lookupLower) bestCol = c;
3540
+ else break;
3541
+ }
3542
+ }
3543
+ if (bestCol === -1) return new FormulaError("#N/A", "HLOOKUP no match found");
3544
+ return tableData[row][bestCol] ?? null;
3545
+ } else {
3546
+ for (let c = 0; c < firstRow.length; c++) {
3547
+ const cellVal = firstRow[c];
3548
+ if (lookupLower !== null && typeof cellVal === "string") {
3549
+ if (cellVal.toLowerCase() === lookupLower) return tableData[row][c] ?? null;
3550
+ } else if (cellVal === lookupValue) {
3551
+ return tableData[row][c] ?? null;
3552
+ }
3553
+ }
3554
+ return new FormulaError("#N/A", "HLOOKUP no exact match found");
3555
+ }
3556
+ }
3557
+ });
3558
+ registry.set("XLOOKUP", {
3559
+ minArgs: 3,
3560
+ maxArgs: 6,
3561
+ evaluate(args, context, evaluator) {
3562
+ const lookupValue = evaluator.evaluate(args[0], context);
3563
+ if (lookupValue instanceof FormulaError) return lookupValue;
3564
+ if (args[1].kind !== "range") {
3565
+ return new FormulaError("#VALUE!", "XLOOKUP lookup_array must be a range");
3566
+ }
3567
+ const lookupArray = context.getRangeValues({ start: args[1].start, end: args[1].end });
3568
+ if (args[2].kind !== "range") {
3569
+ return new FormulaError("#VALUE!", "XLOOKUP return_array must be a range");
3570
+ }
3571
+ const returnArray = context.getRangeValues({ start: args[2].start, end: args[2].end });
3572
+ let ifNotFound = new FormulaError("#N/A", "XLOOKUP no match found");
3573
+ if (args.length >= 4) {
3574
+ ifNotFound = evaluator.evaluate(args[3], context);
3575
+ }
3576
+ let matchMode = 0;
3577
+ if (args.length >= 5) {
3578
+ const rawMM = evaluator.evaluate(args[4], context);
3579
+ if (rawMM instanceof FormulaError) return rawMM;
3580
+ const mm = toNumber(rawMM);
3581
+ if (mm instanceof FormulaError) return mm;
3582
+ matchMode = Math.trunc(mm);
3583
+ }
3584
+ let searchMode = 1;
3585
+ if (args.length >= 6) {
3586
+ const rawSM = evaluator.evaluate(args[5], context);
3587
+ if (rawSM instanceof FormulaError) return rawSM;
3588
+ const sm = toNumber(rawSM);
3589
+ if (sm instanceof FormulaError) return sm;
3590
+ searchMode = Math.trunc(sm);
3591
+ }
3592
+ const isRow = lookupArray.length === 1;
3593
+ const len = isRow ? lookupArray[0].length : lookupArray.length;
3594
+ const getVal = isRow ? (i) => lookupArray[0][i] : (i) => lookupArray[i][0];
3595
+ const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
3596
+ const eq = (a, b) => {
3597
+ if (lookupLower !== null && typeof a === "string") return a.toLowerCase() === lookupLower;
3598
+ return a === b;
3599
+ };
3600
+ let foundIdx = -1;
3601
+ if (matchMode === 0) {
3602
+ const start = searchMode >= 0 ? 0 : len - 1;
3603
+ const end = searchMode >= 0 ? len : -1;
3604
+ const step = searchMode >= 0 ? 1 : -1;
3605
+ for (let i = start; i !== end; i += step) {
3606
+ if (eq(getVal(i), lookupValue)) {
3607
+ foundIdx = i;
3608
+ break;
3609
+ }
3610
+ }
3611
+ } else if (matchMode === -1) {
3612
+ let best = -1;
3613
+ for (let i = 0; i < len; i++) {
3614
+ const v = getVal(i);
3615
+ if (eq(v, lookupValue)) {
3616
+ foundIdx = i;
3617
+ break;
3618
+ }
3619
+ if (typeof lookupValue === "number" && typeof v === "number" && v < lookupValue) {
3620
+ if (best === -1 || v > getVal(best)) best = i;
3621
+ }
3622
+ }
3623
+ if (foundIdx === -1) foundIdx = best;
3624
+ } else if (matchMode === 1) {
3625
+ let best = -1;
3626
+ for (let i = 0; i < len; i++) {
3627
+ const v = getVal(i);
3628
+ if (eq(v, lookupValue)) {
3629
+ foundIdx = i;
3630
+ break;
3631
+ }
3632
+ if (typeof lookupValue === "number" && typeof v === "number" && v > lookupValue) {
3633
+ if (best === -1 || v < getVal(best)) best = i;
3634
+ }
3635
+ }
3636
+ if (foundIdx === -1) foundIdx = best;
3637
+ }
3638
+ if (foundIdx === -1) return ifNotFound;
3639
+ const isReturnRow = returnArray.length === 1;
3640
+ if (isReturnRow) return returnArray[0][foundIdx] ?? null;
3641
+ return returnArray[foundIdx]?.[0] ?? null;
3642
+ }
3643
+ });
3644
+ registry.set("MATCH", {
3645
+ minArgs: 2,
3646
+ maxArgs: 3,
3647
+ evaluate(args, context, evaluator) {
3648
+ const lookupValue = evaluator.evaluate(args[0], context);
3649
+ if (lookupValue instanceof FormulaError) return lookupValue;
3650
+ if (args[1].kind !== "range") {
3651
+ return new FormulaError("#VALUE!", "MATCH lookup_array must be a range");
3652
+ }
3653
+ const rangeData = context.getRangeValues({ start: args[1].start, end: args[1].end });
3654
+ const isSingleRow = rangeData.length === 1;
3655
+ const values = isSingleRow ? rangeData[0] : rangeData;
3656
+ const getValue = isSingleRow ? (i) => values[i] : (i) => values[i][0];
3657
+ const valuesLength = isSingleRow ? values.length : values.length;
3658
+ let matchType = 1;
3659
+ if (args.length >= 3) {
3660
+ const rawMT = evaluator.evaluate(args[2], context);
3661
+ if (rawMT instanceof FormulaError) return rawMT;
3662
+ const mt = toNumber(rawMT);
3663
+ if (mt instanceof FormulaError) return mt;
3664
+ matchType = mt;
3665
+ }
3666
+ const lookupLower = typeof lookupValue === "string" ? lookupValue.toLowerCase() : null;
3667
+ if (matchType === 0) {
3668
+ for (let i = 0; i < valuesLength; i++) {
3669
+ const cellVal = getValue(i);
3670
+ if (lookupLower !== null && typeof cellVal === "string") {
3671
+ if (cellVal.toLowerCase() === lookupLower) return i + 1;
3672
+ } else if (cellVal === lookupValue) {
3673
+ return i + 1;
3674
+ }
3675
+ }
3676
+ return new FormulaError("#N/A", "MATCH no exact match found");
3677
+ } else if (matchType === 1) {
3678
+ let bestIndex = -1;
3679
+ for (let i = 0; i < valuesLength; i++) {
3680
+ const cellVal = getValue(i);
3681
+ if (cellVal === null || cellVal === void 0) continue;
3682
+ if (typeof lookupValue === "number" && typeof cellVal === "number") {
3683
+ if (cellVal <= lookupValue) bestIndex = i;
3684
+ else break;
3685
+ } else if (lookupLower !== null && typeof cellVal === "string") {
3686
+ if (cellVal.toLowerCase() <= lookupLower) bestIndex = i;
3687
+ else break;
3688
+ }
3689
+ }
3690
+ if (bestIndex === -1) return new FormulaError("#N/A", "MATCH no match found");
3691
+ return bestIndex + 1;
3692
+ } else {
3693
+ let bestIndex = -1;
3694
+ for (let i = 0; i < valuesLength; i++) {
3695
+ const cellVal = getValue(i);
3696
+ if (cellVal === null || cellVal === void 0) continue;
3697
+ if (typeof lookupValue === "number" && typeof cellVal === "number") {
3698
+ if (cellVal >= lookupValue) bestIndex = i;
3699
+ else break;
3700
+ } else if (lookupLower !== null && typeof cellVal === "string") {
3701
+ if (cellVal.toLowerCase() >= lookupLower) bestIndex = i;
3702
+ else break;
3703
+ }
3704
+ }
3705
+ if (bestIndex === -1) return new FormulaError("#N/A", "MATCH no match found");
3706
+ return bestIndex + 1;
3707
+ }
3708
+ }
3709
+ });
3710
+ }
3711
+ function registerTextFunctions(registry) {
3712
+ registry.set("CONCATENATE", {
3713
+ minArgs: 1,
3714
+ maxArgs: -1,
3715
+ evaluate(args, context, evaluator) {
3716
+ const values = flattenArgs(args, context, evaluator);
3717
+ const parts = [];
3718
+ for (const val of values) {
3719
+ if (val instanceof FormulaError) return val;
3720
+ parts.push(toString(val));
3721
+ }
3722
+ return parts.join("");
3723
+ }
3724
+ });
3725
+ registry.set("CONCAT", {
3726
+ minArgs: 1,
3727
+ maxArgs: -1,
3728
+ evaluate(args, context, evaluator) {
3729
+ const values = flattenArgs(args, context, evaluator);
3730
+ const parts = [];
3731
+ for (const val of values) {
3732
+ if (val instanceof FormulaError) return val;
3733
+ parts.push(toString(val));
3734
+ }
3735
+ return parts.join("");
3736
+ }
3737
+ });
3738
+ registry.set("UPPER", {
3739
+ minArgs: 1,
3740
+ maxArgs: 1,
3741
+ evaluate(args, context, evaluator) {
3742
+ const val = evaluator.evaluate(args[0], context);
3743
+ if (val instanceof FormulaError) return val;
3744
+ return toString(val).toUpperCase();
3745
+ }
3746
+ });
3747
+ registry.set("LOWER", {
3748
+ minArgs: 1,
3749
+ maxArgs: 1,
3750
+ evaluate(args, context, evaluator) {
3751
+ const val = evaluator.evaluate(args[0], context);
3752
+ if (val instanceof FormulaError) return val;
3753
+ return toString(val).toLowerCase();
3754
+ }
3755
+ });
3756
+ registry.set("TRIM", {
3757
+ minArgs: 1,
3758
+ maxArgs: 1,
3759
+ evaluate(args, context, evaluator) {
3760
+ const val = evaluator.evaluate(args[0], context);
3761
+ if (val instanceof FormulaError) return val;
3762
+ return toString(val).trim();
3763
+ }
3764
+ });
3765
+ registry.set("LEFT", {
3766
+ minArgs: 1,
3767
+ maxArgs: 2,
3768
+ evaluate(args, context, evaluator) {
3769
+ const val = evaluator.evaluate(args[0], context);
3770
+ if (val instanceof FormulaError) return val;
3771
+ const text = toString(val);
3772
+ let numChars = 1;
3773
+ if (args.length >= 2) {
3774
+ const rawNum = evaluator.evaluate(args[1], context);
3775
+ if (rawNum instanceof FormulaError) return rawNum;
3776
+ const n = toNumber(rawNum);
3777
+ if (n instanceof FormulaError) return n;
3778
+ numChars = Math.trunc(n);
3779
+ }
3780
+ if (numChars < 0) return new FormulaError("#VALUE!", "LEFT num_chars must be >= 0");
3781
+ return text.substring(0, numChars);
3782
+ }
3783
+ });
3784
+ registry.set("RIGHT", {
3785
+ minArgs: 1,
3786
+ maxArgs: 2,
3787
+ evaluate(args, context, evaluator) {
3788
+ const val = evaluator.evaluate(args[0], context);
3789
+ if (val instanceof FormulaError) return val;
3790
+ const text = toString(val);
3791
+ let numChars = 1;
3792
+ if (args.length >= 2) {
3793
+ const rawNum = evaluator.evaluate(args[1], context);
3794
+ if (rawNum instanceof FormulaError) return rawNum;
3795
+ const n = toNumber(rawNum);
3796
+ if (n instanceof FormulaError) return n;
3797
+ numChars = Math.trunc(n);
3798
+ }
3799
+ if (numChars < 0) return new FormulaError("#VALUE!", "RIGHT num_chars must be >= 0");
3800
+ return text.substring(Math.max(0, text.length - numChars));
3801
+ }
3802
+ });
3803
+ registry.set("MID", {
3804
+ minArgs: 3,
3805
+ maxArgs: 3,
3806
+ evaluate(args, context, evaluator) {
3807
+ const val = evaluator.evaluate(args[0], context);
3808
+ if (val instanceof FormulaError) return val;
3809
+ const text = toString(val);
3810
+ const rawStart = evaluator.evaluate(args[1], context);
3811
+ if (rawStart instanceof FormulaError) return rawStart;
3812
+ const startPos = toNumber(rawStart);
3813
+ if (startPos instanceof FormulaError) return startPos;
3814
+ const rawNum = evaluator.evaluate(args[2], context);
3815
+ if (rawNum instanceof FormulaError) return rawNum;
3816
+ const numChars = toNumber(rawNum);
3817
+ if (numChars instanceof FormulaError) return numChars;
3818
+ const start = Math.trunc(startPos);
3819
+ const count = Math.trunc(numChars);
3820
+ if (start < 1) return new FormulaError("#VALUE!", "MID start_num must be >= 1");
3821
+ if (count < 0) return new FormulaError("#VALUE!", "MID num_chars must be >= 0");
3822
+ return text.substring(start - 1, start - 1 + count);
3823
+ }
3824
+ });
3825
+ registry.set("LEN", {
3826
+ minArgs: 1,
3827
+ maxArgs: 1,
3828
+ evaluate(args, context, evaluator) {
3829
+ const val = evaluator.evaluate(args[0], context);
3830
+ if (val instanceof FormulaError) return val;
3831
+ return toString(val).length;
3832
+ }
3833
+ });
3834
+ registry.set("SUBSTITUTE", {
3835
+ minArgs: 3,
3836
+ maxArgs: 4,
3837
+ evaluate(args, context, evaluator) {
3838
+ const val = evaluator.evaluate(args[0], context);
3839
+ if (val instanceof FormulaError) return val;
3840
+ const text = toString(val);
3841
+ const rawOld = evaluator.evaluate(args[1], context);
3842
+ if (rawOld instanceof FormulaError) return rawOld;
3843
+ const oldText = toString(rawOld);
3844
+ const rawNew = evaluator.evaluate(args[2], context);
3845
+ if (rawNew instanceof FormulaError) return rawNew;
3846
+ const newText = toString(rawNew);
3847
+ if (args.length >= 4) {
3848
+ const rawInstance = evaluator.evaluate(args[3], context);
3849
+ if (rawInstance instanceof FormulaError) return rawInstance;
3850
+ const instanceNum = toNumber(rawInstance);
3851
+ if (instanceNum instanceof FormulaError) return instanceNum;
3852
+ const n = Math.trunc(instanceNum);
3853
+ if (n < 1) return new FormulaError("#VALUE!", "SUBSTITUTE instance_num must be >= 1");
3854
+ let count = 0;
3855
+ let result = "";
3856
+ let searchFrom = 0;
3857
+ while (searchFrom <= text.length) {
3858
+ const idx = text.indexOf(oldText, searchFrom);
3859
+ if (idx === -1) {
3860
+ result += text.substring(searchFrom);
3861
+ break;
3862
+ }
3863
+ count++;
3864
+ if (count === n) {
3865
+ result += text.substring(searchFrom, idx) + newText;
3866
+ result += text.substring(idx + oldText.length);
3867
+ break;
3868
+ } else {
3869
+ result += text.substring(searchFrom, idx + oldText.length);
3870
+ searchFrom = idx + oldText.length;
3871
+ }
3872
+ }
3873
+ return result;
3874
+ } else {
3875
+ if (oldText === "") return text;
3876
+ return text.split(oldText).join(newText);
3877
+ }
3878
+ }
3879
+ });
3880
+ registry.set("FIND", {
3881
+ minArgs: 2,
3882
+ maxArgs: 3,
3883
+ evaluate(args, context, evaluator) {
3884
+ const rawFind = evaluator.evaluate(args[0], context);
3885
+ if (rawFind instanceof FormulaError) return rawFind;
3886
+ const findText = toString(rawFind);
3887
+ const rawWithin = evaluator.evaluate(args[1], context);
3888
+ if (rawWithin instanceof FormulaError) return rawWithin;
3889
+ const withinText = toString(rawWithin);
3890
+ let startNum = 1;
3891
+ if (args.length >= 3) {
3892
+ const rawStart = evaluator.evaluate(args[2], context);
3893
+ if (rawStart instanceof FormulaError) return rawStart;
3894
+ const s = toNumber(rawStart);
3895
+ if (s instanceof FormulaError) return s;
3896
+ startNum = Math.trunc(s);
3897
+ }
3898
+ if (startNum < 1) return new FormulaError("#VALUE!", "FIND start_num must be >= 1");
3899
+ const idx = withinText.indexOf(findText, startNum - 1);
3900
+ if (idx === -1) return new FormulaError("#VALUE!", "FIND text not found");
3901
+ return idx + 1;
3902
+ }
3903
+ });
3904
+ registry.set("SEARCH", {
3905
+ minArgs: 2,
3906
+ maxArgs: 3,
3907
+ evaluate(args, context, evaluator) {
3908
+ const rawFind = evaluator.evaluate(args[0], context);
3909
+ if (rawFind instanceof FormulaError) return rawFind;
3910
+ const findText = toString(rawFind).toLowerCase();
3911
+ const rawWithin = evaluator.evaluate(args[1], context);
3912
+ if (rawWithin instanceof FormulaError) return rawWithin;
3913
+ const withinText = toString(rawWithin).toLowerCase();
3914
+ let startNum = 1;
3915
+ if (args.length >= 3) {
3916
+ const rawStart = evaluator.evaluate(args[2], context);
3917
+ if (rawStart instanceof FormulaError) return rawStart;
3918
+ const s = toNumber(rawStart);
3919
+ if (s instanceof FormulaError) return s;
3920
+ startNum = Math.trunc(s);
3921
+ }
3922
+ if (startNum < 1) return new FormulaError("#VALUE!", "SEARCH start_num must be >= 1");
3923
+ const idx = withinText.indexOf(findText, startNum - 1);
3924
+ if (idx === -1) return new FormulaError("#VALUE!", "SEARCH text not found");
3925
+ return idx + 1;
3926
+ }
3927
+ });
3928
+ registry.set("REPLACE", {
3929
+ minArgs: 4,
3930
+ maxArgs: 4,
3931
+ evaluate(args, context, evaluator) {
3932
+ const rawText = evaluator.evaluate(args[0], context);
3933
+ if (rawText instanceof FormulaError) return rawText;
3934
+ const text = toString(rawText);
3935
+ const rawStart = evaluator.evaluate(args[1], context);
3936
+ if (rawStart instanceof FormulaError) return rawStart;
3937
+ const startPos = toNumber(rawStart);
3938
+ if (startPos instanceof FormulaError) return startPos;
3939
+ const rawNum = evaluator.evaluate(args[2], context);
3940
+ if (rawNum instanceof FormulaError) return rawNum;
3941
+ const numChars = toNumber(rawNum);
3942
+ if (numChars instanceof FormulaError) return numChars;
3943
+ const rawNew = evaluator.evaluate(args[3], context);
3944
+ if (rawNew instanceof FormulaError) return rawNew;
3945
+ const newText = toString(rawNew);
3946
+ const start = Math.trunc(startPos) - 1;
3947
+ const count = Math.trunc(numChars);
3948
+ return text.substring(0, start) + newText + text.substring(start + count);
3949
+ }
3950
+ });
3951
+ registry.set("REPT", {
3952
+ minArgs: 2,
3953
+ maxArgs: 2,
3954
+ evaluate(args, context, evaluator) {
3955
+ const rawText = evaluator.evaluate(args[0], context);
3956
+ if (rawText instanceof FormulaError) return rawText;
3957
+ const text = toString(rawText);
3958
+ const rawTimes = evaluator.evaluate(args[1], context);
3959
+ if (rawTimes instanceof FormulaError) return rawTimes;
3960
+ const times = toNumber(rawTimes);
3961
+ if (times instanceof FormulaError) return times;
3962
+ const n = Math.trunc(times);
3963
+ if (n < 0) return new FormulaError("#VALUE!", "REPT number must be >= 0");
3964
+ return text.repeat(n);
3965
+ }
3966
+ });
3967
+ registry.set("EXACT", {
3968
+ minArgs: 2,
3969
+ maxArgs: 2,
3970
+ evaluate(args, context, evaluator) {
3971
+ const rawA = evaluator.evaluate(args[0], context);
3972
+ if (rawA instanceof FormulaError) return rawA;
3973
+ const rawB = evaluator.evaluate(args[1], context);
3974
+ if (rawB instanceof FormulaError) return rawB;
3975
+ return toString(rawA) === toString(rawB);
3976
+ }
3977
+ });
3978
+ registry.set("PROPER", {
3979
+ minArgs: 1,
3980
+ maxArgs: 1,
3981
+ evaluate(args, context, evaluator) {
3982
+ const val = evaluator.evaluate(args[0], context);
3983
+ if (val instanceof FormulaError) return val;
3984
+ return toString(val).toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
3985
+ }
3986
+ });
3987
+ registry.set("CLEAN", {
3988
+ minArgs: 1,
3989
+ maxArgs: 1,
3990
+ evaluate(args, context, evaluator) {
3991
+ const val = evaluator.evaluate(args[0], context);
3992
+ if (val instanceof FormulaError) return val;
3993
+ return toString(val).replace(/[\x00-\x1F]/g, "");
3994
+ }
3995
+ });
3996
+ registry.set("CHAR", {
3997
+ minArgs: 1,
3998
+ maxArgs: 1,
3999
+ evaluate(args, context, evaluator) {
4000
+ const rawVal = evaluator.evaluate(args[0], context);
4001
+ if (rawVal instanceof FormulaError) return rawVal;
4002
+ const num = toNumber(rawVal);
4003
+ if (num instanceof FormulaError) return num;
4004
+ const n = Math.trunc(num);
4005
+ if (n < 1 || n > 65535) return new FormulaError("#VALUE!", "CHAR number must be 1-65535");
4006
+ return String.fromCharCode(n);
4007
+ }
4008
+ });
4009
+ registry.set("CODE", {
4010
+ minArgs: 1,
4011
+ maxArgs: 1,
4012
+ evaluate(args, context, evaluator) {
4013
+ const val = evaluator.evaluate(args[0], context);
4014
+ if (val instanceof FormulaError) return val;
4015
+ const text = toString(val);
4016
+ if (text.length === 0) return new FormulaError("#VALUE!", "CODE requires non-empty text");
4017
+ return text.charCodeAt(0);
4018
+ }
4019
+ });
4020
+ registry.set("TEXT", {
4021
+ minArgs: 2,
4022
+ maxArgs: 2,
4023
+ evaluate(args, context, evaluator) {
4024
+ const rawVal = evaluator.evaluate(args[0], context);
4025
+ if (rawVal instanceof FormulaError) return rawVal;
4026
+ const rawFmt = evaluator.evaluate(args[1], context);
4027
+ if (rawFmt instanceof FormulaError) return rawFmt;
4028
+ const fmt = toString(rawFmt);
4029
+ const num = toNumber(rawVal);
4030
+ if (num instanceof FormulaError) return toString(rawVal);
4031
+ if (fmt.includes("%")) {
4032
+ const decimals2 = (fmt.match(/0/g) || []).length - 1;
4033
+ return (num * 100).toFixed(Math.max(0, decimals2)) + "%";
4034
+ }
4035
+ const decimalMatch = fmt.match(/\.(0+)/);
4036
+ const decimals = decimalMatch ? decimalMatch[1].length : 0;
4037
+ const useCommas = fmt.includes(",");
4038
+ const result = num.toFixed(decimals);
4039
+ if (useCommas) {
4040
+ const [intPart, decPart] = result.split(".");
4041
+ const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
4042
+ return decPart ? withCommas + "." + decPart : withCommas;
4043
+ }
4044
+ return result;
4045
+ }
4046
+ });
4047
+ registry.set("VALUE", {
4048
+ minArgs: 1,
4049
+ maxArgs: 1,
4050
+ evaluate(args, context, evaluator) {
4051
+ const val = evaluator.evaluate(args[0], context);
4052
+ if (val instanceof FormulaError) return val;
4053
+ if (typeof val === "number") return val;
4054
+ const raw = toString(val).trim();
4055
+ const isPercent = raw.endsWith("%");
4056
+ const text = raw.replace(/[,$%\s]/g, "");
4057
+ const n = Number(text);
4058
+ if (isNaN(n)) return new FormulaError("#VALUE!", "VALUE cannot convert text to number");
4059
+ return isPercent ? n / 100 : n;
4060
+ }
4061
+ });
4062
+ registry.set("TEXTJOIN", {
4063
+ minArgs: 3,
4064
+ maxArgs: -1,
4065
+ evaluate(args, context, evaluator) {
4066
+ const rawDelim = evaluator.evaluate(args[0], context);
4067
+ if (rawDelim instanceof FormulaError) return rawDelim;
4068
+ const delimiter = toString(rawDelim);
4069
+ const rawIgnore = evaluator.evaluate(args[1], context);
4070
+ if (rawIgnore instanceof FormulaError) return rawIgnore;
4071
+ const ignoreEmpty = !!rawIgnore;
4072
+ const parts = [];
4073
+ for (let i = 2; i < args.length; i++) {
4074
+ const arg = args[i];
4075
+ if (arg.kind === "range") {
4076
+ const rangeData = context.getRangeValues({ start: arg.start, end: arg.end });
4077
+ for (const row of rangeData) {
4078
+ for (const cell of row) {
4079
+ if (cell instanceof FormulaError) return cell;
4080
+ const s = toString(cell);
4081
+ if (!ignoreEmpty || s !== "") parts.push(s);
4082
+ }
4083
+ }
4084
+ } else {
4085
+ const val = evaluator.evaluate(args[i], context);
4086
+ if (val instanceof FormulaError) return val;
4087
+ const s = toString(val);
4088
+ if (!ignoreEmpty || s !== "") parts.push(s);
4089
+ }
4090
+ }
4091
+ return parts.join(delimiter);
4092
+ }
4093
+ });
4094
+ }
4095
+ function toDate(val) {
4096
+ if (val instanceof FormulaError) return val;
4097
+ if (val instanceof Date) {
4098
+ if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
4099
+ return val;
4100
+ }
4101
+ if (typeof val === "string") {
4102
+ const d = new Date(val);
4103
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
4104
+ return d;
4105
+ }
4106
+ if (typeof val === "number") {
4107
+ const d = new Date(val);
4108
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
4109
+ return d;
4110
+ }
4111
+ return new FormulaError("#VALUE!", "Cannot convert value to date");
4112
+ }
4113
+ function registerDateFunctions(registry) {
4114
+ registry.set("TODAY", {
4115
+ minArgs: 0,
4116
+ maxArgs: 0,
4117
+ evaluate(_args, context) {
4118
+ const now = context.now();
4119
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
4120
+ }
4121
+ });
4122
+ registry.set("NOW", {
4123
+ minArgs: 0,
4124
+ maxArgs: 0,
4125
+ evaluate(_args, context) {
4126
+ return context.now();
4127
+ }
4128
+ });
4129
+ registry.set("YEAR", {
4130
+ minArgs: 1,
4131
+ maxArgs: 1,
4132
+ evaluate(args, context, evaluator) {
4133
+ const val = evaluator.evaluate(args[0], context);
4134
+ if (val instanceof FormulaError) return val;
4135
+ const date = toDate(val);
4136
+ if (date instanceof FormulaError) return date;
4137
+ return date.getFullYear();
4138
+ }
4139
+ });
4140
+ registry.set("MONTH", {
4141
+ minArgs: 1,
4142
+ maxArgs: 1,
4143
+ evaluate(args, context, evaluator) {
4144
+ const val = evaluator.evaluate(args[0], context);
4145
+ if (val instanceof FormulaError) return val;
4146
+ const date = toDate(val);
4147
+ if (date instanceof FormulaError) return date;
4148
+ return date.getMonth() + 1;
4149
+ }
4150
+ });
4151
+ registry.set("DAY", {
4152
+ minArgs: 1,
4153
+ maxArgs: 1,
4154
+ evaluate(args, context, evaluator) {
4155
+ const val = evaluator.evaluate(args[0], context);
4156
+ if (val instanceof FormulaError) return val;
4157
+ const date = toDate(val);
4158
+ if (date instanceof FormulaError) return date;
4159
+ return date.getDate();
4160
+ }
4161
+ });
4162
+ registry.set("DATE", {
4163
+ minArgs: 3,
4164
+ maxArgs: 3,
4165
+ evaluate(args, context, evaluator) {
4166
+ const rawY = evaluator.evaluate(args[0], context);
4167
+ if (rawY instanceof FormulaError) return rawY;
4168
+ const y = toNumber(rawY);
4169
+ if (y instanceof FormulaError) return y;
4170
+ const rawM = evaluator.evaluate(args[1], context);
4171
+ if (rawM instanceof FormulaError) return rawM;
4172
+ const m = toNumber(rawM);
4173
+ if (m instanceof FormulaError) return m;
4174
+ const rawD = evaluator.evaluate(args[2], context);
4175
+ if (rawD instanceof FormulaError) return rawD;
4176
+ const d = toNumber(rawD);
4177
+ if (d instanceof FormulaError) return d;
4178
+ return new Date(Math.trunc(y), Math.trunc(m) - 1, Math.trunc(d));
4179
+ }
4180
+ });
4181
+ registry.set("DATEDIF", {
4182
+ minArgs: 3,
4183
+ maxArgs: 3,
4184
+ evaluate(args, context, evaluator) {
4185
+ const rawStart = evaluator.evaluate(args[0], context);
4186
+ if (rawStart instanceof FormulaError) return rawStart;
4187
+ const startDate = toDate(rawStart);
4188
+ if (startDate instanceof FormulaError) return startDate;
4189
+ const rawEnd = evaluator.evaluate(args[1], context);
4190
+ if (rawEnd instanceof FormulaError) return rawEnd;
4191
+ const endDate = toDate(rawEnd);
4192
+ if (endDate instanceof FormulaError) return endDate;
4193
+ const rawUnit = evaluator.evaluate(args[2], context);
4194
+ if (rawUnit instanceof FormulaError) return rawUnit;
4195
+ const unit = String(rawUnit).toUpperCase();
4196
+ if (startDate > endDate) return new FormulaError("#NUM!", "DATEDIF start date must be <= end date");
4197
+ switch (unit) {
4198
+ case "Y": {
4199
+ let years = endDate.getFullYear() - startDate.getFullYear();
4200
+ if (endDate.getMonth() < startDate.getMonth() || endDate.getMonth() === startDate.getMonth() && endDate.getDate() < startDate.getDate()) {
4201
+ years--;
4202
+ }
4203
+ return years;
4204
+ }
4205
+ case "M": {
4206
+ let months = (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth();
4207
+ if (endDate.getDate() < startDate.getDate()) months--;
4208
+ return months;
4209
+ }
4210
+ case "D":
4211
+ return Math.floor((endDate.getTime() - startDate.getTime()) / 864e5);
4212
+ default:
4213
+ return new FormulaError("#VALUE!", "DATEDIF unit must be Y, M, or D");
4214
+ }
4215
+ }
4216
+ });
4217
+ registry.set("EDATE", {
4218
+ minArgs: 2,
4219
+ maxArgs: 2,
4220
+ evaluate(args, context, evaluator) {
4221
+ const rawDate = evaluator.evaluate(args[0], context);
4222
+ if (rawDate instanceof FormulaError) return rawDate;
4223
+ const date = toDate(rawDate);
4224
+ if (date instanceof FormulaError) return date;
4225
+ const rawMonths = evaluator.evaluate(args[1], context);
4226
+ if (rawMonths instanceof FormulaError) return rawMonths;
4227
+ const months = toNumber(rawMonths);
4228
+ if (months instanceof FormulaError) return months;
4229
+ const result = new Date(date);
4230
+ result.setMonth(result.getMonth() + Math.trunc(months));
4231
+ return result;
4232
+ }
4233
+ });
4234
+ registry.set("EOMONTH", {
4235
+ minArgs: 2,
4236
+ maxArgs: 2,
4237
+ evaluate(args, context, evaluator) {
4238
+ const rawDate = evaluator.evaluate(args[0], context);
4239
+ if (rawDate instanceof FormulaError) return rawDate;
4240
+ const date = toDate(rawDate);
4241
+ if (date instanceof FormulaError) return date;
4242
+ const rawMonths = evaluator.evaluate(args[1], context);
4243
+ if (rawMonths instanceof FormulaError) return rawMonths;
4244
+ const months = toNumber(rawMonths);
4245
+ if (months instanceof FormulaError) return months;
4246
+ const result = new Date(date.getFullYear(), date.getMonth() + Math.trunc(months) + 1, 0);
4247
+ return result;
4248
+ }
4249
+ });
4250
+ registry.set("WEEKDAY", {
4251
+ minArgs: 1,
4252
+ maxArgs: 2,
4253
+ evaluate(args, context, evaluator) {
4254
+ const rawDate = evaluator.evaluate(args[0], context);
4255
+ if (rawDate instanceof FormulaError) return rawDate;
4256
+ const date = toDate(rawDate);
4257
+ if (date instanceof FormulaError) return date;
4258
+ let returnType = 1;
4259
+ if (args.length >= 2) {
4260
+ const rawRT = evaluator.evaluate(args[1], context);
4261
+ if (rawRT instanceof FormulaError) return rawRT;
4262
+ const rt = toNumber(rawRT);
4263
+ if (rt instanceof FormulaError) return rt;
4264
+ returnType = Math.trunc(rt);
4265
+ }
4266
+ const day = date.getDay();
4267
+ switch (returnType) {
4268
+ case 1:
4269
+ return day + 1;
4270
+ // 1=Sun, 7=Sat
4271
+ case 2:
4272
+ return day === 0 ? 7 : day;
4273
+ // 1=Mon, 7=Sun
4274
+ case 3:
4275
+ return day === 0 ? 6 : day - 1;
4276
+ // 0=Mon, 6=Sun
4277
+ default:
4278
+ return new FormulaError("#VALUE!", "WEEKDAY return_type must be 1, 2, or 3");
4279
+ }
4280
+ }
4281
+ });
4282
+ registry.set("HOUR", {
4283
+ minArgs: 1,
4284
+ maxArgs: 1,
4285
+ evaluate(args, context, evaluator) {
4286
+ const val = evaluator.evaluate(args[0], context);
4287
+ if (val instanceof FormulaError) return val;
4288
+ const date = toDate(val);
4289
+ if (date instanceof FormulaError) return date;
4290
+ return date.getHours();
4291
+ }
4292
+ });
4293
+ registry.set("MINUTE", {
4294
+ minArgs: 1,
4295
+ maxArgs: 1,
4296
+ evaluate(args, context, evaluator) {
4297
+ const val = evaluator.evaluate(args[0], context);
4298
+ if (val instanceof FormulaError) return val;
4299
+ const date = toDate(val);
4300
+ if (date instanceof FormulaError) return date;
4301
+ return date.getMinutes();
4302
+ }
4303
+ });
4304
+ registry.set("SECOND", {
4305
+ minArgs: 1,
4306
+ maxArgs: 1,
4307
+ evaluate(args, context, evaluator) {
4308
+ const val = evaluator.evaluate(args[0], context);
4309
+ if (val instanceof FormulaError) return val;
4310
+ const date = toDate(val);
4311
+ if (date instanceof FormulaError) return date;
4312
+ return date.getSeconds();
4313
+ }
4314
+ });
4315
+ registry.set("NETWORKDAYS", {
4316
+ minArgs: 2,
4317
+ maxArgs: 2,
4318
+ evaluate(args, context, evaluator) {
4319
+ const rawStart = evaluator.evaluate(args[0], context);
4320
+ if (rawStart instanceof FormulaError) return rawStart;
4321
+ const startDate = toDate(rawStart);
4322
+ if (startDate instanceof FormulaError) return startDate;
4323
+ const rawEnd = evaluator.evaluate(args[1], context);
4324
+ if (rawEnd instanceof FormulaError) return rawEnd;
4325
+ const endDate = toDate(rawEnd);
4326
+ if (endDate instanceof FormulaError) return endDate;
4327
+ const start = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
4328
+ const end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
4329
+ const sign = end >= start ? 1 : -1;
4330
+ const [from, to] = sign === 1 ? [start, end] : [end, start];
4331
+ let count = 0;
4332
+ const current = new Date(from);
4333
+ while (current <= to) {
4334
+ const day = current.getDay();
4335
+ if (day !== 0 && day !== 6) count++;
4336
+ current.setDate(current.getDate() + 1);
4337
+ }
4338
+ return count * sign;
4339
+ }
4340
+ });
4341
+ }
4342
+ function makeCriteria(op, value) {
4343
+ return { op, value, valueLower: typeof value === "string" ? value.toLowerCase() : null };
4344
+ }
4345
+ function parseCriteria(criteria) {
4346
+ if (typeof criteria === "number") {
4347
+ return makeCriteria("=", criteria);
4348
+ }
4349
+ if (typeof criteria !== "string") {
4350
+ return makeCriteria("=", criteria);
4351
+ }
4352
+ const str = criteria.trim();
4353
+ if (str.startsWith(">=")) {
4354
+ return makeCriteria(">=", parseNumericOrString(str.substring(2).trim()));
4355
+ }
4356
+ if (str.startsWith("<=")) {
4357
+ return makeCriteria("<=", parseNumericOrString(str.substring(2).trim()));
4358
+ }
4359
+ if (str.startsWith("<>")) {
4360
+ return makeCriteria("<>", parseNumericOrString(str.substring(2).trim()));
4361
+ }
4362
+ if (str.startsWith(">")) {
4363
+ return makeCriteria(">", parseNumericOrString(str.substring(1).trim()));
4364
+ }
4365
+ if (str.startsWith("<")) {
4366
+ return makeCriteria("<", parseNumericOrString(str.substring(1).trim()));
4367
+ }
4368
+ if (str.startsWith("=")) {
4369
+ return makeCriteria("=", parseNumericOrString(str.substring(1).trim()));
4370
+ }
4371
+ return makeCriteria("=", parseNumericOrString(str));
4372
+ }
4373
+ function parseNumericOrString(s) {
4374
+ const n = Number(s);
4375
+ if (!isNaN(n) && s !== "") return n;
4376
+ return s;
4377
+ }
4378
+ function matchesCriteria(cellValue, criteria) {
4379
+ const { op, value } = criteria;
4380
+ let comparableCell = cellValue;
4381
+ if (typeof value === "number" && typeof cellValue !== "number") {
4382
+ const n = toNumber(cellValue);
4383
+ if (n instanceof FormulaError) return false;
4384
+ comparableCell = n;
4385
+ }
4386
+ if (typeof comparableCell === "string" && criteria.valueLower !== null) {
4387
+ const a = comparableCell.toLowerCase();
4388
+ const b = criteria.valueLower;
4389
+ switch (op) {
4390
+ case "=":
4391
+ return a === b;
4392
+ case "<>":
4393
+ return a !== b;
4394
+ case ">":
4395
+ return a > b;
4396
+ case "<":
4397
+ return a < b;
4398
+ case ">=":
4399
+ return a >= b;
4400
+ case "<=":
4401
+ return a <= b;
4402
+ }
4403
+ }
4404
+ if (typeof comparableCell === "number" && typeof value === "number") {
4405
+ switch (op) {
4406
+ case "=":
4407
+ return comparableCell === value;
4408
+ case "<>":
4409
+ return comparableCell !== value;
4410
+ case ">":
4411
+ return comparableCell > value;
4412
+ case "<":
4413
+ return comparableCell < value;
4414
+ case ">=":
4415
+ return comparableCell >= value;
4416
+ case "<=":
4417
+ return comparableCell <= value;
4418
+ }
4419
+ }
4420
+ if (op === "=") return comparableCell === value;
4421
+ if (op === "<>") return comparableCell !== value;
4422
+ return false;
4423
+ }
4424
+ function registerStatsFunctions(registry) {
4425
+ registry.set("SUMIF", {
4426
+ minArgs: 2,
4427
+ maxArgs: 3,
4428
+ evaluate(args, context, evaluator) {
4429
+ if (args[0].kind !== "range") {
4430
+ return new FormulaError("#VALUE!", "SUMIF range must be a cell range");
4431
+ }
4432
+ const criteriaRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
4433
+ const rawCriteria = evaluator.evaluate(args[1], context);
4434
+ if (rawCriteria instanceof FormulaError) return rawCriteria;
4435
+ const criteria = parseCriteria(rawCriteria);
4436
+ let sumRange;
4437
+ if (args.length >= 3) {
4438
+ if (args[2].kind !== "range") {
4439
+ return new FormulaError("#VALUE!", "SUMIF sum_range must be a cell range");
4440
+ }
4441
+ sumRange = context.getRangeValues({ start: args[2].start, end: args[2].end });
4442
+ } else {
4443
+ sumRange = criteriaRange;
4444
+ }
4445
+ let sum = 0;
4446
+ for (let r = 0; r < criteriaRange.length; r++) {
4447
+ for (let c = 0; c < criteriaRange[r].length; c++) {
4448
+ if (matchesCriteria(criteriaRange[r][c], criteria)) {
4449
+ const sumVal = sumRange[r] && sumRange[r][c] !== void 0 ? sumRange[r][c] : null;
4450
+ const n = toNumber(sumVal);
4451
+ if (typeof n === "number") {
4452
+ sum += n;
4453
+ }
4454
+ }
4455
+ }
4456
+ }
4457
+ return sum;
4458
+ }
4459
+ });
4460
+ registry.set("COUNTIF", {
4461
+ minArgs: 2,
4462
+ maxArgs: 2,
4463
+ evaluate(args, context, evaluator) {
4464
+ if (args[0].kind !== "range") {
4465
+ return new FormulaError("#VALUE!", "COUNTIF range must be a cell range");
4466
+ }
4467
+ const rangeData = context.getRangeValues({ start: args[0].start, end: args[0].end });
4468
+ const rawCriteria = evaluator.evaluate(args[1], context);
4469
+ if (rawCriteria instanceof FormulaError) return rawCriteria;
4470
+ const criteria = parseCriteria(rawCriteria);
4471
+ let count = 0;
4472
+ for (let r = 0; r < rangeData.length; r++) {
4473
+ for (let c = 0; c < rangeData[r].length; c++) {
4474
+ if (matchesCriteria(rangeData[r][c], criteria)) {
4475
+ count++;
4476
+ }
4477
+ }
4478
+ }
4479
+ return count;
4480
+ }
4481
+ });
4482
+ registry.set("AVERAGEIF", {
4483
+ minArgs: 2,
4484
+ maxArgs: 3,
4485
+ evaluate(args, context, evaluator) {
4486
+ if (args[0].kind !== "range") {
4487
+ return new FormulaError("#VALUE!", "AVERAGEIF range must be a cell range");
4488
+ }
4489
+ const criteriaRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
4490
+ const rawCriteria = evaluator.evaluate(args[1], context);
4491
+ if (rawCriteria instanceof FormulaError) return rawCriteria;
4492
+ const criteria = parseCriteria(rawCriteria);
4493
+ let avgRange;
4494
+ if (args.length >= 3) {
4495
+ if (args[2].kind !== "range") {
4496
+ return new FormulaError("#VALUE!", "AVERAGEIF avg_range must be a cell range");
4497
+ }
4498
+ avgRange = context.getRangeValues({ start: args[2].start, end: args[2].end });
4499
+ } else {
4500
+ avgRange = criteriaRange;
4501
+ }
4502
+ let sum = 0;
4503
+ let count = 0;
4504
+ for (let r = 0; r < criteriaRange.length; r++) {
4505
+ for (let c = 0; c < criteriaRange[r].length; c++) {
4506
+ if (matchesCriteria(criteriaRange[r][c], criteria)) {
4507
+ const avgVal = avgRange[r] && avgRange[r][c] !== void 0 ? avgRange[r][c] : null;
4508
+ const n = toNumber(avgVal);
4509
+ if (typeof n === "number") {
4510
+ sum += n;
4511
+ count++;
4512
+ }
4513
+ }
4514
+ }
4515
+ }
4516
+ if (count === 0) return new FormulaError("#DIV/0!", "No matching values for AVERAGEIF");
4517
+ return sum / count;
4518
+ }
4519
+ });
4520
+ registry.set("SUMIFS", {
4521
+ minArgs: 3,
4522
+ maxArgs: -1,
4523
+ evaluate(args, context, evaluator) {
4524
+ if ((args.length - 1) % 2 !== 0) {
4525
+ return new FormulaError("#VALUE!", "SUMIFS requires sum_range + pairs of criteria_range, criteria");
4526
+ }
4527
+ if (args[0].kind !== "range") {
4528
+ return new FormulaError("#VALUE!", "SUMIFS sum_range must be a cell range");
4529
+ }
4530
+ const sumRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
4531
+ const pairs = [];
4532
+ for (let i = 1; i < args.length; i += 2) {
4533
+ const rangeArg = args[i];
4534
+ if (rangeArg.kind !== "range") {
4535
+ return new FormulaError("#VALUE!", "SUMIFS criteria_range must be a cell range");
4536
+ }
4537
+ const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
4538
+ const rawCriteria = evaluator.evaluate(args[i + 1], context);
4539
+ if (rawCriteria instanceof FormulaError) return rawCriteria;
4540
+ pairs.push({ range, criteria: parseCriteria(rawCriteria) });
4541
+ }
4542
+ let sum = 0;
4543
+ for (let r = 0; r < sumRange.length; r++) {
4544
+ for (let c = 0; c < sumRange[r].length; c++) {
4545
+ let allMatch = true;
4546
+ for (const pair of pairs) {
4547
+ const cellVal = pair.range[r]?.[c];
4548
+ if (!matchesCriteria(cellVal, pair.criteria)) {
4549
+ allMatch = false;
4550
+ break;
4551
+ }
4552
+ }
4553
+ if (allMatch) {
4554
+ const n = toNumber(sumRange[r][c]);
4555
+ if (typeof n === "number") sum += n;
4556
+ }
4557
+ }
4558
+ }
4559
+ return sum;
4560
+ }
4561
+ });
4562
+ registry.set("COUNTIFS", {
4563
+ minArgs: 2,
4564
+ maxArgs: -1,
4565
+ evaluate(args, context, evaluator) {
4566
+ if (args.length % 2 !== 0) {
4567
+ return new FormulaError("#VALUE!", "COUNTIFS requires pairs of criteria_range, criteria");
4568
+ }
4569
+ const pairs = [];
4570
+ for (let i = 0; i < args.length; i += 2) {
4571
+ const rangeArg = args[i];
4572
+ if (rangeArg.kind !== "range") {
4573
+ return new FormulaError("#VALUE!", "COUNTIFS criteria_range must be a cell range");
4574
+ }
4575
+ const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
4576
+ const rawCriteria = evaluator.evaluate(args[i + 1], context);
4577
+ if (rawCriteria instanceof FormulaError) return rawCriteria;
4578
+ pairs.push({ range, criteria: parseCriteria(rawCriteria) });
4579
+ }
4580
+ const firstRange = pairs[0].range;
4581
+ let count = 0;
4582
+ for (let r = 0; r < firstRange.length; r++) {
4583
+ for (let c = 0; c < firstRange[r].length; c++) {
4584
+ let allMatch = true;
4585
+ for (const pair of pairs) {
4586
+ const cellVal = pair.range[r]?.[c];
4587
+ if (!matchesCriteria(cellVal, pair.criteria)) {
4588
+ allMatch = false;
4589
+ break;
4590
+ }
4591
+ }
4592
+ if (allMatch) count++;
4593
+ }
4594
+ }
4595
+ return count;
4596
+ }
4597
+ });
4598
+ registry.set("AVERAGEIFS", {
4599
+ minArgs: 3,
4600
+ maxArgs: -1,
4601
+ evaluate(args, context, evaluator) {
4602
+ if ((args.length - 1) % 2 !== 0) {
4603
+ return new FormulaError("#VALUE!", "AVERAGEIFS requires avg_range + pairs of criteria_range, criteria");
4604
+ }
4605
+ if (args[0].kind !== "range") {
4606
+ return new FormulaError("#VALUE!", "AVERAGEIFS avg_range must be a cell range");
4607
+ }
4608
+ const avgRange = context.getRangeValues({ start: args[0].start, end: args[0].end });
4609
+ const pairs = [];
4610
+ for (let i = 1; i < args.length; i += 2) {
4611
+ const rangeArg = args[i];
4612
+ if (rangeArg.kind !== "range") {
4613
+ return new FormulaError("#VALUE!", "AVERAGEIFS criteria_range must be a cell range");
4614
+ }
4615
+ const range = context.getRangeValues({ start: rangeArg.start, end: rangeArg.end });
4616
+ const rawCriteria = evaluator.evaluate(args[i + 1], context);
4617
+ if (rawCriteria instanceof FormulaError) return rawCriteria;
4618
+ pairs.push({ range, criteria: parseCriteria(rawCriteria) });
4619
+ }
4620
+ let sum = 0;
4621
+ let count = 0;
4622
+ for (let r = 0; r < avgRange.length; r++) {
4623
+ for (let c = 0; c < avgRange[r].length; c++) {
4624
+ let allMatch = true;
4625
+ for (const pair of pairs) {
4626
+ const cellVal = pair.range[r]?.[c];
4627
+ if (!matchesCriteria(cellVal, pair.criteria)) {
4628
+ allMatch = false;
4629
+ break;
4630
+ }
4631
+ }
4632
+ if (allMatch) {
4633
+ const n = toNumber(avgRange[r][c]);
4634
+ if (typeof n === "number") {
4635
+ sum += n;
4636
+ count++;
4637
+ }
4638
+ }
4639
+ }
4640
+ }
4641
+ if (count === 0) return new FormulaError("#DIV/0!", "No matching values for AVERAGEIFS");
4642
+ return sum / count;
4643
+ }
4644
+ });
4645
+ }
4646
+ function registerInfoFunctions(registry) {
4647
+ registry.set("ISBLANK", {
4648
+ minArgs: 1,
4649
+ maxArgs: 1,
4650
+ evaluate(args, context, evaluator) {
4651
+ const val = evaluator.evaluate(args[0], context);
4652
+ return val === null || val === void 0 || val === "";
4653
+ }
4654
+ });
4655
+ registry.set("ISNUMBER", {
4656
+ minArgs: 1,
4657
+ maxArgs: 1,
4658
+ evaluate(args, context, evaluator) {
4659
+ const val = evaluator.evaluate(args[0], context);
4660
+ return typeof val === "number" && !isNaN(val);
4661
+ }
4662
+ });
4663
+ registry.set("ISTEXT", {
4664
+ minArgs: 1,
4665
+ maxArgs: 1,
4666
+ evaluate(args, context, evaluator) {
4667
+ const val = evaluator.evaluate(args[0], context);
4668
+ return typeof val === "string";
4669
+ }
4670
+ });
4671
+ registry.set("ISERROR", {
4672
+ minArgs: 1,
4673
+ maxArgs: 1,
4674
+ evaluate(args, context, evaluator) {
4675
+ const val = evaluator.evaluate(args[0], context);
4676
+ return val instanceof FormulaError;
4677
+ }
4678
+ });
4679
+ registry.set("ISNA", {
4680
+ minArgs: 1,
4681
+ maxArgs: 1,
4682
+ evaluate(args, context, evaluator) {
4683
+ const val = evaluator.evaluate(args[0], context);
4684
+ return val instanceof FormulaError && val.type === "#N/A";
4685
+ }
4686
+ });
4687
+ registry.set("TYPE", {
4688
+ minArgs: 1,
4689
+ maxArgs: 1,
4690
+ evaluate(args, context, evaluator) {
4691
+ const val = evaluator.evaluate(args[0], context);
4692
+ if (val instanceof FormulaError) return 16;
4693
+ if (typeof val === "number") return 1;
4694
+ if (typeof val === "string") return 2;
4695
+ if (typeof val === "boolean") return 4;
4696
+ if (val === null || val === void 0) return 1;
4697
+ return 1;
4698
+ }
4699
+ });
4700
+ }
4701
+ function createBuiltInFunctions() {
4702
+ const registry = /* @__PURE__ */ new Map();
4703
+ registerMathFunctions(registry);
4704
+ registerLogicalFunctions(registry);
4705
+ registerLookupFunctions(registry);
4706
+ registerTextFunctions(registry);
4707
+ registerDateFunctions(registry);
4708
+ registerStatsFunctions(registry);
4709
+ registerInfoFunctions(registry);
4710
+ return registry;
4711
+ }
4712
+ function extractDependencies(node) {
4713
+ const deps = /* @__PURE__ */ new Set();
4714
+ function walk(n) {
4715
+ switch (n.kind) {
4716
+ case "cellRef":
4717
+ deps.add(toCellKey(n.address.col, n.address.row, n.address.sheet));
4718
+ break;
4719
+ case "range": {
4720
+ const sheet = n.start.sheet;
4721
+ const minRow = Math.min(n.start.row, n.end.row);
4722
+ const maxRow = Math.max(n.start.row, n.end.row);
4723
+ const minCol = Math.min(n.start.col, n.end.col);
4724
+ const maxCol = Math.max(n.start.col, n.end.col);
4725
+ for (let r = minRow; r <= maxRow; r++) {
4726
+ for (let c = minCol; c <= maxCol; c++) {
4727
+ deps.add(toCellKey(c, r, sheet));
4728
+ }
4729
+ }
4730
+ break;
4731
+ }
4732
+ case "functionCall":
4733
+ for (const arg of n.args) walk(arg);
4734
+ break;
4735
+ case "binaryOp":
4736
+ walk(n.left);
4737
+ walk(n.right);
4738
+ break;
4739
+ case "unaryOp":
4740
+ walk(n.operand);
4741
+ break;
4742
+ }
4743
+ }
4744
+ walk(node);
4745
+ return deps;
4746
+ }
4747
+ var FormulaEngine = class {
4748
+ constructor(config) {
4749
+ this.formulas = /* @__PURE__ */ new Map();
4750
+ this.parsedFormulas = /* @__PURE__ */ new Map();
4751
+ this.values = /* @__PURE__ */ new Map();
4752
+ this.depGraph = new DependencyGraph();
4753
+ this.namedRanges = /* @__PURE__ */ new Map();
4754
+ this.sheetAccessors = /* @__PURE__ */ new Map();
4755
+ const builtIns = createBuiltInFunctions();
4756
+ if (config?.customFunctions) {
4757
+ for (const [name, fn] of Object.entries(config.customFunctions)) {
4758
+ builtIns.set(name.toUpperCase(), fn);
4759
+ }
4760
+ }
4761
+ if (config?.namedRanges) {
4762
+ for (const [name, ref] of Object.entries(config.namedRanges)) {
4763
+ this.namedRanges.set(name.toUpperCase(), ref);
4764
+ }
4765
+ }
4766
+ this.evaluator = new FormulaEvaluator(builtIns);
4767
+ this.maxChainLength = config?.maxChainLength ?? 1e3;
4768
+ }
4769
+ /**
4770
+ * Set or clear a formula for a cell.
4771
+ */
4772
+ setFormula(col, row, formula, accessor) {
4773
+ const key = toCellKey(col, row);
4774
+ if (formula === null || formula === "") {
4775
+ const oldValue2 = this.values.get(key);
4776
+ this.formulas.delete(key);
4777
+ this.parsedFormulas.delete(key);
4778
+ this.values.delete(key);
4779
+ this.depGraph.removeDependencies(key);
4780
+ return {
4781
+ updatedCells: oldValue2 !== void 0 ? [{ cellKey: key, col, row, oldValue: oldValue2, newValue: void 0 }] : []
4782
+ };
4783
+ }
4784
+ const expression = formula.startsWith("=") ? formula.slice(1) : formula;
4785
+ let ast;
4786
+ try {
4787
+ const tokens = tokenize(expression);
4788
+ ast = parse(tokens, this.namedRanges);
4789
+ } catch (err) {
4790
+ const error = err instanceof FormulaError ? err : new FormulaError("#ERROR!", String(err));
4791
+ ast = { kind: "error", error };
4792
+ }
4793
+ const deps = extractDependencies(ast);
4794
+ if (deps.has(key) || this.depGraph.wouldCreateCycle(key, deps)) {
4795
+ const oldValue2 = this.values.get(key);
4796
+ const circError = new FormulaError("#CIRC!", "Circular reference detected");
4797
+ this.formulas.set(key, formula);
4798
+ this.parsedFormulas.set(key, ast);
4799
+ this.values.set(key, circError);
4800
+ this.depGraph.setDependencies(key, deps);
4801
+ return {
4802
+ updatedCells: [{ cellKey: key, col, row, oldValue: oldValue2, newValue: circError }]
4803
+ };
4804
+ }
4805
+ this.depGraph.setDependencies(key, deps);
4806
+ this.formulas.set(key, formula);
4807
+ this.parsedFormulas.set(key, ast);
4808
+ const oldValue = this.values.get(key);
4809
+ const context = this.createContext(accessor);
4810
+ const newValue = this.evaluator.evaluate(ast, context);
4811
+ this.values.set(key, newValue);
4812
+ const updatedCells = [
4813
+ { cellKey: key, col, row, oldValue, newValue }
4814
+ ];
4815
+ const recalcOrder = this.depGraph.getRecalcOrder(key);
4816
+ this.recalcCells(recalcOrder, accessor, updatedCells);
4817
+ return { updatedCells };
4818
+ }
4819
+ /**
4820
+ * Notify the engine that a non-formula cell's value changed.
4821
+ */
4822
+ onCellChanged(col, row, accessor) {
4823
+ const key = toCellKey(col, row);
4824
+ const recalcOrder = this.depGraph.getRecalcOrder(key);
4825
+ if (recalcOrder.length === 0) return { updatedCells: [] };
4826
+ const updatedCells = [];
4827
+ this.recalcCells(recalcOrder, accessor, updatedCells);
4828
+ return { updatedCells };
1639
4829
  }
1640
- /** Whether there are redo steps available. */
1641
- get canRedo() {
1642
- return this.redoStack.length > 0;
4830
+ /**
4831
+ * Batch notify: multiple cells changed.
4832
+ */
4833
+ onCellsChanged(cells, accessor) {
4834
+ const keys = cells.map((c) => toCellKey(c.col, c.row));
4835
+ const recalcOrder = this.depGraph.getRecalcOrderBatch(keys);
4836
+ if (recalcOrder.length === 0) return { updatedCells: [] };
4837
+ const updatedCells = [];
4838
+ this.recalcCells(recalcOrder, accessor, updatedCells);
4839
+ return { updatedCells };
1643
4840
  }
1644
- /** Number of history entries. */
1645
- get historyLength() {
1646
- return this.history.length;
4841
+ /**
4842
+ * Get the current computed value for a cell.
4843
+ */
4844
+ getValue(col, row) {
4845
+ return this.values.get(toCellKey(col, row));
1647
4846
  }
1648
- /** Number of redo entries. */
1649
- get redoLength() {
1650
- return this.redoStack.length;
4847
+ /**
4848
+ * Get the formula string for a cell.
4849
+ */
4850
+ getFormula(col, row) {
4851
+ return this.formulas.get(toCellKey(col, row));
1651
4852
  }
1652
- /** Whether a batch is currently open. */
1653
- get isBatching() {
1654
- return this.batch !== null;
4853
+ /**
4854
+ * Check if a cell has a formula.
4855
+ */
4856
+ hasFormula(col, row) {
4857
+ return this.formulas.has(toCellKey(col, row));
1655
4858
  }
1656
4859
  /**
1657
- * Record a group of events as a single undoable step.
1658
- * If a batch is open, accumulates into the batch instead.
1659
- * Clears the redo stack on any new entry.
4860
+ * Register a custom function at runtime.
1660
4861
  */
1661
- push(events) {
1662
- if (events.length === 0) return;
1663
- if (this.batch !== null) {
1664
- this.batch.push(...events);
1665
- } else {
1666
- this.history.push(events);
1667
- if (this.history.length > this.maxDepth) {
1668
- this.history.splice(0, this.history.length - this.maxDepth);
4862
+ registerFunction(name, fn) {
4863
+ this.evaluator.registerFunction(name, fn);
4864
+ }
4865
+ /**
4866
+ * Full recalculation of all formulas.
4867
+ */
4868
+ recalcAll(accessor) {
4869
+ const updatedCells = [];
4870
+ const context = this.createContext(accessor);
4871
+ if (this.formulas.size === 0) return { updatedCells };
4872
+ const allFormulaKeys = [];
4873
+ for (const key of this.formulas.keys()) allFormulaKeys.push(key);
4874
+ const recalcOrder = this.depGraph.getRecalcOrderBatch(allFormulaKeys);
4875
+ const ordered = new Set(recalcOrder);
4876
+ for (const key of allFormulaKeys) {
4877
+ if (!ordered.has(key)) {
4878
+ const { col, row } = fromCellKey(key);
4879
+ const ast = this.parsedFormulas.get(key);
4880
+ if (!ast) continue;
4881
+ const oldValue = this.values.get(key);
4882
+ const newValue = this.evaluator.evaluate(ast, context);
4883
+ this.values.set(key, newValue);
4884
+ updatedCells.push({ cellKey: key, col, row, oldValue, newValue });
1669
4885
  }
1670
- this.redoStack.length = 0;
1671
4886
  }
4887
+ this.recalcCells(recalcOrder, accessor, updatedCells);
4888
+ return { updatedCells };
1672
4889
  }
1673
4890
  /**
1674
- * Record a single event as a step (shorthand for push([event])).
1675
- * If a batch is open, accumulates into the batch instead.
4891
+ * Clear all formulas and cached values.
1676
4892
  */
1677
- record(event) {
1678
- this.push([event]);
4893
+ clear() {
4894
+ this.formulas.clear();
4895
+ this.parsedFormulas.clear();
4896
+ this.values.clear();
4897
+ this.depGraph.clear();
1679
4898
  }
1680
4899
  /**
1681
- * Start a batch subsequent record/push calls accumulate into one undo step.
1682
- * Has no effect if a batch is already open.
4900
+ * Get all formula entries for serialization.
1683
4901
  */
1684
- beginBatch() {
1685
- if (this.batch === null) {
1686
- this.batch = [];
4902
+ getAllFormulas() {
4903
+ const result = [];
4904
+ for (const [key, formula] of this.formulas) {
4905
+ const { col, row } = fromCellKey(key);
4906
+ result.push({ col, row, formula });
1687
4907
  }
4908
+ return result;
1688
4909
  }
1689
4910
  /**
1690
- * End a batch — commits all accumulated events as one undo step.
1691
- * Has no effect if no batch is open or if the batch is empty.
4911
+ * Bulk-load formulas. Recalculates everything.
1692
4912
  */
1693
- endBatch() {
1694
- const b = this.batch;
1695
- this.batch = null;
1696
- if (!b || b.length === 0) return;
1697
- this.history.push(b);
1698
- if (this.history.length > this.maxDepth) {
1699
- this.history.splice(0, this.history.length - this.maxDepth);
4913
+ loadFormulas(formulas, accessor) {
4914
+ this.clear();
4915
+ for (const { col, row, formula } of formulas) {
4916
+ const key = toCellKey(col, row);
4917
+ const expression = formula.startsWith("=") ? formula.slice(1) : formula;
4918
+ let ast;
4919
+ try {
4920
+ const tokens = tokenize(expression);
4921
+ ast = parse(tokens, this.namedRanges);
4922
+ } catch (err) {
4923
+ const error = err instanceof FormulaError ? err : new FormulaError("#ERROR!", String(err));
4924
+ ast = { kind: "error", error };
4925
+ }
4926
+ this.formulas.set(key, formula);
4927
+ this.parsedFormulas.set(key, ast);
4928
+ const deps = extractDependencies(ast);
4929
+ this.depGraph.setDependencies(key, deps);
1700
4930
  }
1701
- this.redoStack.length = 0;
4931
+ return this.recalcAll(accessor);
1702
4932
  }
4933
+ // --- Named Ranges ---
1703
4934
  /**
1704
- * Pop the most recent history entry for undo.
1705
- * Returns the batch of events (in original order) to be reversed by the caller,
1706
- * or null if there is nothing to undo.
1707
- *
1708
- * The caller is responsible for applying the events in reverse order.
4935
+ * Define a named range (e.g. "Revenue" "A1:A10").
1709
4936
  */
1710
- undo() {
1711
- const lastBatch = this.history.pop();
1712
- if (!lastBatch) return null;
1713
- this.redoStack.push(lastBatch);
1714
- return lastBatch;
4937
+ defineNamedRange(name, ref) {
4938
+ this.namedRanges.set(name.toUpperCase(), ref);
1715
4939
  }
1716
4940
  /**
1717
- * Pop the most recent redo entry.
1718
- * Returns the batch of events (in original order) to be re-applied by the caller,
1719
- * or null if there is nothing to redo.
4941
+ * Remove a named range by name.
1720
4942
  */
1721
- redo() {
1722
- const nextBatch = this.redoStack.pop();
1723
- if (!nextBatch) return null;
1724
- this.history.push(nextBatch);
1725
- return nextBatch;
4943
+ removeNamedRange(name) {
4944
+ this.namedRanges.delete(name.toUpperCase());
1726
4945
  }
1727
4946
  /**
1728
- * Clear all history and redo state.
1729
- * Does not affect any open batch — call endBatch() first if needed.
4947
+ * Get all named ranges as a Map (name → ref).
1730
4948
  */
1731
- clear() {
1732
- this.history = [];
1733
- this.redoStack = [];
4949
+ getNamedRanges() {
4950
+ return this.namedRanges;
1734
4951
  }
1735
- };
1736
- function validateColumns(columns) {
1737
- if (!Array.isArray(columns) || columns.length === 0) {
1738
- console.warn("[OGrid] columns prop is empty or not an array");
1739
- return;
4952
+ // --- Sheet Accessors ---
4953
+ /**
4954
+ * Register a data accessor for a named sheet (for cross-sheet references).
4955
+ */
4956
+ registerSheet(name, accessor) {
4957
+ this.sheetAccessors.set(name, accessor);
1740
4958
  }
1741
- const isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1742
- const ids = /* @__PURE__ */ new Set();
1743
- for (const col of columns) {
1744
- if (!col.columnId) {
1745
- console.warn("[OGrid] Column missing columnId:", col);
4959
+ /**
4960
+ * Unregister a sheet accessor.
4961
+ */
4962
+ unregisterSheet(name) {
4963
+ this.sheetAccessors.delete(name);
4964
+ }
4965
+ // --- Formula Auditing ---
4966
+ /**
4967
+ * Get all cells that a cell depends on (deep, transitive precedents).
4968
+ */
4969
+ getPrecedents(col, row) {
4970
+ const key = toCellKey(col, row);
4971
+ const result = [];
4972
+ const visited = /* @__PURE__ */ new Set();
4973
+ const queue = [];
4974
+ const directDeps = this.depGraph.getDependencies(key);
4975
+ for (const dep of directDeps) {
4976
+ if (!visited.has(dep)) {
4977
+ visited.add(dep);
4978
+ queue.push(dep);
4979
+ }
1746
4980
  }
1747
- if (ids.has(col.columnId)) {
1748
- console.warn(`[OGrid] Duplicate columnId: "${col.columnId}"`);
4981
+ let head = 0;
4982
+ while (head < queue.length) {
4983
+ const current = queue[head++];
4984
+ const parsed = fromCellKey(current);
4985
+ result.push({
4986
+ cellKey: current,
4987
+ col: parsed.col,
4988
+ row: parsed.row,
4989
+ formula: this.formulas.get(current),
4990
+ value: this.values.has(current) ? this.values.get(current) : void 0
4991
+ });
4992
+ const deps = this.depGraph.getDependencies(current);
4993
+ for (const dep of deps) {
4994
+ if (!visited.has(dep)) {
4995
+ visited.add(dep);
4996
+ queue.push(dep);
4997
+ }
4998
+ }
1749
4999
  }
1750
- ids.add(col.columnId);
1751
- if (isDev && col.editable === true && col.cellEditor == null) {
1752
- console.warn(
1753
- `[OGrid] Column "${col.columnId}" has editable=true but no cellEditor defined. Cells will not open an editor on double-click. Set cellEditor to 'text', 'select', 'checkbox', 'date', or a custom component.`
1754
- );
5000
+ return result;
5001
+ }
5002
+ /**
5003
+ * Get all cells that depend on this cell (deep, transitive dependents).
5004
+ */
5005
+ getDependents(col, row) {
5006
+ const key = toCellKey(col, row);
5007
+ const result = [];
5008
+ const visited = /* @__PURE__ */ new Set();
5009
+ const queue = [];
5010
+ const directDeps = this.depGraph.getDependents(key);
5011
+ for (const dep of directDeps) {
5012
+ if (!visited.has(dep)) {
5013
+ visited.add(dep);
5014
+ queue.push(dep);
5015
+ }
5016
+ }
5017
+ let head = 0;
5018
+ while (head < queue.length) {
5019
+ const current = queue[head++];
5020
+ const parsed = fromCellKey(current);
5021
+ result.push({
5022
+ cellKey: current,
5023
+ col: parsed.col,
5024
+ row: parsed.row,
5025
+ formula: this.formulas.get(current),
5026
+ value: this.values.has(current) ? this.values.get(current) : void 0
5027
+ });
5028
+ const deps = this.depGraph.getDependents(current);
5029
+ for (const dep of deps) {
5030
+ if (!visited.has(dep)) {
5031
+ visited.add(dep);
5032
+ queue.push(dep);
5033
+ }
5034
+ }
1755
5035
  }
5036
+ return result;
1756
5037
  }
1757
- }
1758
- function validateVirtualScrollConfig(config) {
1759
- if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
1760
- if (config.enabled !== true) return;
1761
- if (!config.rowHeight || config.rowHeight <= 0) {
1762
- console.warn(
1763
- "[OGrid] virtualScroll.enabled is true but rowHeight is missing or <= 0. Set a positive rowHeight (e.g. virtualScroll: { enabled: true, rowHeight: 36 }) for correct virtual scrolling behavior."
1764
- );
5038
+ /**
5039
+ * Get a full audit trail for a cell: target + precedents + dependents.
5040
+ */
5041
+ getAuditTrail(col, row) {
5042
+ const key = toCellKey(col, row);
5043
+ const target = {
5044
+ cellKey: key,
5045
+ col,
5046
+ row,
5047
+ formula: this.formulas.get(key),
5048
+ value: this.values.has(key) ? this.values.get(key) : void 0
5049
+ };
5050
+ return {
5051
+ target,
5052
+ precedents: this.getPrecedents(col, row),
5053
+ dependents: this.getDependents(col, row)
5054
+ };
1765
5055
  }
1766
- }
1767
- function validateRowIds(items, getRowId) {
1768
- if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
1769
- if (!getRowId) return;
1770
- const ids = /* @__PURE__ */ new Set();
1771
- const limit = Math.min(items.length, 100);
1772
- for (let i = 0; i < limit; i++) {
1773
- const id = getRowId(items[i]);
1774
- if (id == null) {
1775
- console.warn(`[OGrid] getRowId returned null/undefined for row ${i}`);
1776
- return;
1777
- }
1778
- if (ids.has(id)) {
1779
- console.warn(
1780
- `[OGrid] Duplicate row ID "${id}" at index ${i}. getRowId must return unique values.`
1781
- );
1782
- return;
5056
+ // --- Private methods ---
5057
+ createContext(accessor) {
5058
+ const contextNow = /* @__PURE__ */ new Date();
5059
+ return {
5060
+ getCellValue: (addr) => {
5061
+ const key = toCellKey(addr.col, addr.row, addr.sheet);
5062
+ const cached = this.values.get(key);
5063
+ if (cached !== void 0) return cached;
5064
+ if (addr.sheet) {
5065
+ const sheetAccessor = this.sheetAccessors.get(addr.sheet);
5066
+ if (!sheetAccessor) return new FormulaError("#REF!", `Unknown sheet: ${addr.sheet}`);
5067
+ return sheetAccessor.getCellValue(addr.col, addr.row);
5068
+ }
5069
+ return accessor.getCellValue(addr.col, addr.row);
5070
+ },
5071
+ getRangeValues: (range) => {
5072
+ const result = [];
5073
+ const sheet = range.start.sheet;
5074
+ const rangeAccessor = sheet ? this.sheetAccessors.get(sheet) : accessor;
5075
+ if (sheet && !rangeAccessor) {
5076
+ return [[new FormulaError("#REF!", `Unknown sheet: ${sheet}`)]];
5077
+ }
5078
+ const minRow = Math.min(range.start.row, range.end.row);
5079
+ const maxRow = Math.max(range.start.row, range.end.row);
5080
+ const minCol = Math.min(range.start.col, range.end.col);
5081
+ const maxCol = Math.max(range.start.col, range.end.col);
5082
+ for (let r = minRow; r <= maxRow; r++) {
5083
+ const row = [];
5084
+ for (let c = minCol; c <= maxCol; c++) {
5085
+ const key = toCellKey(c, r, sheet);
5086
+ const cached = this.values.get(key);
5087
+ if (cached !== void 0) {
5088
+ row.push(cached);
5089
+ } else {
5090
+ row.push(rangeAccessor.getCellValue(c, r));
5091
+ }
5092
+ }
5093
+ result.push(row);
5094
+ }
5095
+ return result;
5096
+ },
5097
+ now: () => contextNow
5098
+ };
5099
+ }
5100
+ recalcCells(order, accessor, updatedCells) {
5101
+ const context = this.createContext(accessor);
5102
+ let count = 0;
5103
+ for (const key of order) {
5104
+ if (count++ > this.maxChainLength) {
5105
+ const { col: col2, row: row2 } = fromCellKey(key);
5106
+ const oldValue2 = this.values.get(key);
5107
+ const circError = new FormulaError("#CIRC!", "Dependency chain too long");
5108
+ this.values.set(key, circError);
5109
+ updatedCells.push({ cellKey: key, col: col2, row: row2, oldValue: oldValue2, newValue: circError });
5110
+ continue;
5111
+ }
5112
+ const ast = this.parsedFormulas.get(key);
5113
+ if (!ast) continue;
5114
+ const { col, row } = fromCellKey(key);
5115
+ const oldValue = this.values.get(key);
5116
+ const newValue = this.evaluator.evaluate(ast, context);
5117
+ this.values.set(key, newValue);
5118
+ updatedCells.push({ cellKey: key, col, row, oldValue, newValue });
1783
5119
  }
1784
- ids.add(id);
1785
5120
  }
1786
- }
1787
- var DEFAULT_DEBOUNCE_MS = 300;
1788
- var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
5121
+ };
1789
5122
  function useLatestRef(value) {
1790
5123
  const ref = useRef(value);
1791
5124
  ref.current = value;
@@ -1848,8 +5181,8 @@ function useFilterOptions(dataSource, fields) {
1848
5181
  function resolveCellDisplayContent2(col, item, displayValue) {
1849
5182
  return resolveCellDisplayContent(col, item, displayValue);
1850
5183
  }
1851
- function resolveCellStyle2(col, item) {
1852
- return resolveCellStyle(col, item);
5184
+ function resolveCellStyle2(col, item, displayValue) {
5185
+ return resolveCellStyle(col, item, displayValue);
1853
5186
  }
1854
5187
  function buildInlineEditorProps2(item, col, descriptor, callbacks) {
1855
5188
  const result = buildInlineEditorProps(item, col, descriptor, callbacks);
@@ -1863,6 +5196,7 @@ function getCellInteractionProps(descriptor, columnId, handlers) {
1863
5196
  return {
1864
5197
  "data-row-index": descriptor.rowIndex,
1865
5198
  "data-col-index": descriptor.globalColIndex,
5199
+ ...descriptor.isActive ? { "data-active-cell": "true" } : {},
1866
5200
  ...descriptor.isInRange ? { "data-in-range": "true" } : {},
1867
5201
  tabIndex: descriptor.isActive ? 0 : -1,
1868
5202
  onMouseDown: (e) => handlers.handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex),
@@ -1874,6 +5208,127 @@ function getCellInteractionProps(descriptor, columnId, handlers) {
1874
5208
  } : {}
1875
5209
  };
1876
5210
  }
5211
+ var NOOP_RESULT = {
5212
+ getFormulaValue: () => void 0,
5213
+ hasFormula: () => false,
5214
+ getFormula: () => void 0,
5215
+ setFormula: () => {
5216
+ },
5217
+ onCellChanged: () => {
5218
+ },
5219
+ getPrecedents: () => [],
5220
+ getDependents: () => [],
5221
+ getAuditTrail: () => null,
5222
+ enabled: false
5223
+ };
5224
+ function useFormulaEngine(params) {
5225
+ const {
5226
+ formulas,
5227
+ items,
5228
+ flatColumns,
5229
+ initialFormulas,
5230
+ onFormulaRecalc,
5231
+ formulaFunctions,
5232
+ namedRanges,
5233
+ sheets
5234
+ } = params;
5235
+ const itemsRef = useLatestRef(items);
5236
+ const flatColumnsRef = useLatestRef(flatColumns);
5237
+ const onFormulaRecalcRef = useLatestRef(onFormulaRecalc);
5238
+ const engineRef = useRef(null);
5239
+ if (formulas && !engineRef.current) {
5240
+ engineRef.current = new FormulaEngine({
5241
+ customFunctions: formulaFunctions,
5242
+ namedRanges
5243
+ });
5244
+ } else if (!formulas && engineRef.current) {
5245
+ engineRef.current = null;
5246
+ }
5247
+ useEffect(() => {
5248
+ if (!engineRef.current || !sheets) return;
5249
+ for (const [name, accessor] of Object.entries(sheets)) {
5250
+ engineRef.current.registerSheet(name, accessor);
5251
+ }
5252
+ return () => {
5253
+ if (!engineRef.current || !sheets) return;
5254
+ for (const name of Object.keys(sheets)) {
5255
+ engineRef.current.unregisterSheet(name);
5256
+ }
5257
+ };
5258
+ }, [sheets]);
5259
+ const createAccessor = useCallback(() => {
5260
+ const currentItems = itemsRef.current;
5261
+ const currentCols = flatColumnsRef.current;
5262
+ return {
5263
+ getCellValue: (col, row) => {
5264
+ if (row < 0 || row >= currentItems.length) return null;
5265
+ if (col < 0 || col >= currentCols.length) return null;
5266
+ return getCellValue(currentItems[row], currentCols[col]);
5267
+ },
5268
+ getRowCount: () => currentItems.length,
5269
+ getColumnCount: () => currentCols.length
5270
+ };
5271
+ }, [itemsRef, flatColumnsRef]);
5272
+ const initialLoadedRef = useRef(false);
5273
+ useEffect(() => {
5274
+ if (formulas && engineRef.current && initialFormulas && !initialLoadedRef.current) {
5275
+ initialLoadedRef.current = true;
5276
+ const accessor = createAccessor();
5277
+ const result = engineRef.current.loadFormulas(initialFormulas, accessor);
5278
+ if (result.updatedCells.length > 0) {
5279
+ onFormulaRecalcRef.current?.(result);
5280
+ }
5281
+ }
5282
+ }, [formulas, initialFormulas, createAccessor, onFormulaRecalcRef]);
5283
+ const getFormulaValue = useCallback((col, row) => {
5284
+ return engineRef.current?.getValue(col, row);
5285
+ }, []);
5286
+ const hasFormula = useCallback((col, row) => {
5287
+ return engineRef.current?.hasFormula(col, row) ?? false;
5288
+ }, []);
5289
+ const getFormula = useCallback((col, row) => {
5290
+ return engineRef.current?.getFormula(col, row);
5291
+ }, []);
5292
+ const setFormula = useCallback((col, row, formula) => {
5293
+ if (!engineRef.current) return;
5294
+ const accessor = createAccessor();
5295
+ const result = engineRef.current.setFormula(col, row, formula, accessor);
5296
+ if (result.updatedCells.length > 0) {
5297
+ onFormulaRecalcRef.current?.(result);
5298
+ }
5299
+ }, [createAccessor, onFormulaRecalcRef]);
5300
+ const onCellChanged = useCallback((col, row) => {
5301
+ if (!engineRef.current) return;
5302
+ const accessor = createAccessor();
5303
+ const result = engineRef.current.onCellChanged(col, row, accessor);
5304
+ if (result.updatedCells.length > 0) {
5305
+ onFormulaRecalcRef.current?.(result);
5306
+ }
5307
+ }, [createAccessor, onFormulaRecalcRef]);
5308
+ const getPrecedents = useCallback((col, row) => {
5309
+ return engineRef.current?.getPrecedents(col, row) ?? [];
5310
+ }, []);
5311
+ const getDependents = useCallback((col, row) => {
5312
+ return engineRef.current?.getDependents(col, row) ?? [];
5313
+ }, []);
5314
+ const getAuditTrail = useCallback((col, row) => {
5315
+ return engineRef.current?.getAuditTrail(col, row) ?? null;
5316
+ }, []);
5317
+ return useMemo(() => {
5318
+ if (!formulas) return NOOP_RESULT;
5319
+ return {
5320
+ getFormulaValue,
5321
+ hasFormula,
5322
+ getFormula,
5323
+ setFormula,
5324
+ onCellChanged,
5325
+ getPrecedents,
5326
+ getDependents,
5327
+ getAuditTrail,
5328
+ enabled: true
5329
+ };
5330
+ }, [formulas, getFormulaValue, hasFormula, getFormula, setFormula, onCellChanged, getPrecedents, getDependents, getAuditTrail]);
5331
+ }
1877
5332
  function useOGridPagination(params) {
1878
5333
  const { controlledPage, controlledPageSize, defaultPageSize, onPageChange, onPageSizeChange } = params;
1879
5334
  const [internalPage, setInternalPage] = useState(1);
@@ -2127,6 +5582,20 @@ function useSideBarState(params) {
2127
5582
  // src/hooks/useOGrid.ts
2128
5583
  var DEFAULT_PAGE_SIZE = 25;
2129
5584
  var EMPTY_LOADING_OPTIONS2 = {};
5585
+ var NAME_BOX_STYLE = {
5586
+ fontFamily: "monospace",
5587
+ fontSize: "12px",
5588
+ fontWeight: 500,
5589
+ padding: "2px 8px",
5590
+ border: "1px solid var(--ogrid-border, #e0e0e0)",
5591
+ borderRadius: 3,
5592
+ background: "var(--ogrid-bg, #fff)",
5593
+ color: "var(--ogrid-fg, #242424)",
5594
+ minWidth: 48,
5595
+ textAlign: "center",
5596
+ lineHeight: "20px",
5597
+ userSelect: "none"
5598
+ };
2130
5599
  function useOGrid(props, ref) {
2131
5600
  const {
2132
5601
  columns: columnsProp,
@@ -2169,6 +5638,7 @@ function useOGrid(props, ref) {
2169
5638
  selectedRows,
2170
5639
  onSelectionChange,
2171
5640
  showRowNumbers,
5641
+ cellReferences,
2172
5642
  statusBar,
2173
5643
  pageSizeOptions,
2174
5644
  sideBar,
@@ -2182,6 +5652,12 @@ function useOGrid(props, ref) {
2182
5652
  rowHeight,
2183
5653
  density = "normal",
2184
5654
  workerSort,
5655
+ formulas,
5656
+ initialFormulas,
5657
+ onFormulaRecalc,
5658
+ formulaFunctions,
5659
+ namedRanges,
5660
+ sheets,
2185
5661
  "aria-label": ariaLabel,
2186
5662
  "aria-labelledby": ariaLabelledBy
2187
5663
  } = props;
@@ -2488,8 +5964,22 @@ function useOGrid(props, ref) {
2488
5964
  filtersState.handleFilterChange,
2489
5965
  filtersState.clientFilterOptions
2490
5966
  ]);
5967
+ const formulaEngine = useFormulaEngine({
5968
+ formulas,
5969
+ items: dataFetchingState.displayItems,
5970
+ flatColumns: columns,
5971
+ initialFormulas,
5972
+ onFormulaRecalc,
5973
+ formulaFunctions,
5974
+ namedRanges,
5975
+ sheets
5976
+ });
2491
5977
  const clearAllFilters = useCallback(() => filtersState.setFilters({}), [filtersState]);
2492
5978
  const isLoadingResolved = isServerSide && dataFetchingState.serverLoading || displayLoading;
5979
+ const [activeCellRef, setActiveCellRef] = useState(null);
5980
+ const onActiveCellChange = useCallback((ref2) => {
5981
+ setActiveCellRef(ref2);
5982
+ }, []);
2493
5983
  const dataGridProps = useMemo(() => ({
2494
5984
  items: dataFetchingState.displayItems,
2495
5985
  columns: columnsProp,
@@ -2514,7 +6004,10 @@ function useOGrid(props, ref) {
2514
6004
  rowSelection,
2515
6005
  selectedRows: effectiveSelectedRows,
2516
6006
  onSelectionChange: handleSelectionChange,
2517
- showRowNumbers,
6007
+ showRowNumbers: showRowNumbers || cellReferences,
6008
+ showColumnLetters: !!cellReferences,
6009
+ showNameBox: !!cellReferences,
6010
+ onActiveCellChange: cellReferences ? onActiveCellChange : void 0,
2518
6011
  currentPage: paginationState.page,
2519
6012
  pageSize: paginationState.pageSize,
2520
6013
  statusBar: statusBarConfig,
@@ -2539,7 +6032,16 @@ function useOGrid(props, ref) {
2539
6032
  onClearAll: clearAllFilters,
2540
6033
  message: emptyState?.message,
2541
6034
  render: emptyState?.render
2542
- }
6035
+ },
6036
+ formulas,
6037
+ getFormulaValue: formulaEngine.enabled ? formulaEngine.getFormulaValue : void 0,
6038
+ hasFormula: formulaEngine.enabled ? formulaEngine.hasFormula : void 0,
6039
+ getFormula: formulaEngine.enabled ? formulaEngine.getFormula : void 0,
6040
+ setFormula: formulaEngine.enabled ? formulaEngine.setFormula : void 0,
6041
+ onFormulaCellChanged: formulaEngine.enabled ? formulaEngine.onCellChanged : void 0,
6042
+ getPrecedents: formulaEngine.enabled ? formulaEngine.getPrecedents : void 0,
6043
+ getDependents: formulaEngine.enabled ? formulaEngine.getDependents : void 0,
6044
+ getAuditTrail: formulaEngine.enabled ? formulaEngine.getAuditTrail : void 0
2543
6045
  }), [
2544
6046
  dataFetchingState.displayItems,
2545
6047
  columnsProp,
@@ -2565,6 +6067,8 @@ function useOGrid(props, ref) {
2565
6067
  effectiveSelectedRows,
2566
6068
  handleSelectionChange,
2567
6069
  showRowNumbers,
6070
+ cellReferences,
6071
+ onActiveCellChange,
2568
6072
  paginationState.page,
2569
6073
  paginationState.pageSize,
2570
6074
  statusBarConfig,
@@ -2585,7 +6089,9 @@ function useOGrid(props, ref) {
2585
6089
  ariaLabelledBy,
2586
6090
  filtersState.hasActiveFilters,
2587
6091
  clearAllFilters,
2588
- emptyState
6092
+ emptyState,
6093
+ formulas,
6094
+ formulaEngine
2589
6095
  ]);
2590
6096
  const pagination = useMemo(() => ({
2591
6097
  page: paginationState.page,
@@ -2603,14 +6109,20 @@ function useOGrid(props, ref) {
2603
6109
  onSetVisibleColumns: setVisibleColumns,
2604
6110
  placement: columnChooserPlacement
2605
6111
  }), [columnChooserColumns, visibleColumns, handleVisibilityChange, setVisibleColumns, columnChooserPlacement]);
6112
+ const showNameBox = !!cellReferences;
6113
+ const nameBoxEl = useMemo(() => showNameBox ? React5.createElement("div", {
6114
+ style: NAME_BOX_STYLE,
6115
+ "aria-label": "Active cell reference"
6116
+ }, activeCellRef ?? "\u2014") : null, [showNameBox, activeCellRef]);
6117
+ const resolvedToolbar = useMemo(() => showNameBox ? React5.createElement(React5.Fragment, null, nameBoxEl, toolbar) : toolbar, [showNameBox, nameBoxEl, toolbar]);
2606
6118
  const layout = useMemo(() => ({
2607
- toolbar,
6119
+ toolbar: resolvedToolbar,
2608
6120
  toolbarBelow,
2609
6121
  className,
2610
6122
  emptyState,
2611
6123
  sideBarProps,
2612
6124
  fullScreen
2613
- }), [toolbar, toolbarBelow, className, emptyState, sideBarProps, fullScreen]);
6125
+ }), [resolvedToolbar, toolbarBelow, className, emptyState, sideBarProps, fullScreen]);
2614
6126
  const filtersResult = useMemo(() => ({
2615
6127
  hasActiveFilters: filtersState.hasActiveFilters,
2616
6128
  setFilters: filtersState.setFilters
@@ -3036,6 +6548,11 @@ function useClipboard(params) {
3036
6548
  const activeCellRef = useLatestRef(params.activeCell);
3037
6549
  const editableRef = useLatestRef(params.editable);
3038
6550
  const onCellValueChangedRef = useLatestRef(params.onCellValueChanged);
6551
+ const formulasRef = useLatestRef(params.formulas);
6552
+ const flatColumnsRef = useLatestRef(params.flatColumns);
6553
+ const getFormulaRef = useLatestRef(params.getFormula);
6554
+ const hasFormulaRef = useLatestRef(params.hasFormula);
6555
+ const setFormulaRef = useLatestRef(params.setFormula);
3039
6556
  const cutRangeRef = useRef(null);
3040
6557
  const [cutRange, setCutRange] = useState(null);
3041
6558
  const [copyRange, setCopyRange] = useState(null);
@@ -3053,12 +6570,18 @@ function useClipboard(params) {
3053
6570
  const range = getEffectiveRange();
3054
6571
  if (range == null) return;
3055
6572
  const norm = normalizeSelectionRange(range);
3056
- const tsv = formatSelectionAsTsv(itemsRef.current, visibleColsRef.current, norm);
6573
+ const formulaOptions = formulasRef.current && flatColumnsRef.current ? {
6574
+ colOffset,
6575
+ flatColumns: flatColumnsRef.current,
6576
+ getFormula: getFormulaRef.current,
6577
+ hasFormula: hasFormulaRef.current
6578
+ } : void 0;
6579
+ const tsv = formatSelectionAsTsv(itemsRef.current, visibleColsRef.current, norm, formulaOptions);
3057
6580
  internalClipboardRef.current = tsv;
3058
6581
  setCopyRange(norm);
3059
6582
  void navigator.clipboard.writeText(tsv).catch(() => {
3060
6583
  });
3061
- }, [getEffectiveRange, itemsRef, visibleColsRef]);
6584
+ }, [getEffectiveRange, itemsRef, visibleColsRef, formulasRef, flatColumnsRef, getFormulaRef, hasFormulaRef, colOffset]);
3062
6585
  const handleCut = useCallback(() => {
3063
6586
  if (editableRef.current === false) return;
3064
6587
  const range = getEffectiveRange();
@@ -3091,8 +6614,13 @@ function useClipboard(params) {
3091
6614
  const items = itemsRef.current;
3092
6615
  const visibleCols = visibleColsRef.current;
3093
6616
  const parsedRows = parseTsvClipboard(text);
6617
+ const formulaOptions = formulasRef.current && flatColumnsRef.current ? {
6618
+ colOffset,
6619
+ flatColumns: flatColumnsRef.current,
6620
+ setFormula: setFormulaRef.current
6621
+ } : void 0;
3094
6622
  beginBatch?.();
3095
- const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols);
6623
+ const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols, formulaOptions);
3096
6624
  for (const evt of pasteEvents) onCellValueChanged(evt);
3097
6625
  if (cutRangeRef.current) {
3098
6626
  const cutEvents = applyCutClear(cutRangeRef.current, items, visibleCols);
@@ -3102,7 +6630,7 @@ function useClipboard(params) {
3102
6630
  }
3103
6631
  endBatch?.();
3104
6632
  setCopyRange(null);
3105
- }, [getEffectiveRange, itemsRef, visibleColsRef, editableRef, onCellValueChangedRef, beginBatch, endBatch]);
6633
+ }, [getEffectiveRange, itemsRef, visibleColsRef, editableRef, onCellValueChangedRef, beginBatch, endBatch, formulasRef, flatColumnsRef, setFormulaRef, colOffset]);
3106
6634
  const clearClipboardRanges = useCallback(() => {
3107
6635
  setCopyRange(null);
3108
6636
  setCutRange(null);
@@ -3208,7 +6736,9 @@ function useKeyboardNavigation(params) {
3208
6736
  "Tab",
3209
6737
  "Enter",
3210
6738
  "Home",
3211
- "End"
6739
+ "End",
6740
+ "PageDown",
6741
+ "PageUp"
3212
6742
  ].includes(e.key)) {
3213
6743
  setActiveCell({ rowIndex: 0, columnIndex: colOffset });
3214
6744
  e.preventDefault();
@@ -3312,6 +6842,42 @@ function useKeyboardNavigation(params) {
3312
6842
  setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
3313
6843
  break;
3314
6844
  }
6845
+ case "PageDown":
6846
+ case "PageUp": {
6847
+ e.preventDefault();
6848
+ const wrapper = wrapperRef.current;
6849
+ let pageSize = 10;
6850
+ let rowHeight = 36;
6851
+ if (wrapper) {
6852
+ const firstRow = wrapper.querySelector("tbody tr");
6853
+ if (firstRow && firstRow.offsetHeight > 0) {
6854
+ rowHeight = firstRow.offsetHeight;
6855
+ pageSize = Math.max(1, Math.floor(wrapper.clientHeight / rowHeight));
6856
+ }
6857
+ }
6858
+ const pgDirection = e.key === "PageDown" ? 1 : -1;
6859
+ const newRowPage = Math.max(0, Math.min(rowIndex + pgDirection * pageSize, maxRowIndex));
6860
+ if (shift) {
6861
+ setSelectionRange({
6862
+ startRow: selectionRange?.startRow ?? rowIndex,
6863
+ startCol: selectionRange?.startCol ?? dataColIndex,
6864
+ endRow: newRowPage,
6865
+ endCol: selectionRange?.endCol ?? dataColIndex
6866
+ });
6867
+ } else {
6868
+ setSelectionRange({
6869
+ startRow: newRowPage,
6870
+ startCol: dataColIndex,
6871
+ endRow: newRowPage,
6872
+ endCol: dataColIndex
6873
+ });
6874
+ }
6875
+ setActiveCell({ rowIndex: newRowPage, columnIndex });
6876
+ if (wrapper) {
6877
+ wrapper.scrollTop = getScrollTopForRow(newRowPage, rowHeight, wrapper.clientHeight, "center");
6878
+ }
6879
+ break;
6880
+ }
3315
6881
  case "Enter":
3316
6882
  case "F2": {
3317
6883
  e.preventDefault();
@@ -3534,7 +7100,8 @@ function useFillHandle(params) {
3534
7100
  colOffset,
3535
7101
  wrapperRef,
3536
7102
  beginBatch,
3537
- endBatch
7103
+ endBatch,
7104
+ formulaOptions
3538
7105
  } = params;
3539
7106
  const onCellValueChangedRef = useLatestRef(onCellValueChangedProp);
3540
7107
  const [fillDrag, setFillDrag] = useState(null);
@@ -3640,7 +7207,7 @@ function useFillHandle(params) {
3640
7207
  });
3641
7208
  setSelectionRange(norm);
3642
7209
  setActiveCell({ rowIndex: fillDrag.startRow, columnIndex: fillDrag.startCol + colOffsetRef.current });
3643
- const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols);
7210
+ const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols, formulaOptions);
3644
7211
  if (fillEvents.length > 0) {
3645
7212
  beginBatch?.();
3646
7213
  for (const evt of fillEvents) onCellValueChangedRef.current?.(evt);
@@ -3695,7 +7262,8 @@ function useFillHandle(params) {
3695
7262
  norm.startRow,
3696
7263
  norm.startCol,
3697
7264
  itemsRef.current,
3698
- visibleColsRef.current
7265
+ visibleColsRef.current,
7266
+ formulaOptions
3699
7267
  );
3700
7268
  if (fillEvents.length > 0) {
3701
7269
  beginBatch?.();
@@ -4166,14 +7734,38 @@ function useDataGridEditing(params) {
4166
7734
  onCellValueChanged,
4167
7735
  setActiveCell,
4168
7736
  setSelectionRange,
4169
- colOffset
7737
+ colOffset,
7738
+ setFormula,
7739
+ onFormulaCellChanged,
7740
+ formulas,
7741
+ flatColumns
4170
7742
  } = params;
4171
7743
  const [popoverAnchorEl, setPopoverAnchorEl] = useState(null);
4172
7744
  const visibleColsRef = useLatestRef(params.visibleCols);
4173
7745
  const itemsLengthRef = useLatestRef(params.itemsLength);
4174
7746
  const onCellValueChangedRef = useLatestRef(onCellValueChanged);
7747
+ const setFormulaRef = useLatestRef(setFormula);
7748
+ const onFormulaCellChangedRef = useLatestRef(onFormulaCellChanged);
7749
+ const flatColumnsRef = useLatestRef(flatColumns);
4175
7750
  const commitCellEdit = useCallback(
4176
7751
  (item, columnId, oldValue, newValue, rowIndex, globalColIndex) => {
7752
+ if (formulas && typeof newValue === "string" && newValue.startsWith("=") && setFormulaRef.current) {
7753
+ const cols = flatColumnsRef.current;
7754
+ const colIndex = cols ? cols.findIndex((c) => c.columnId === columnId) : -1;
7755
+ if (colIndex >= 0) {
7756
+ setFormulaRef.current(colIndex, rowIndex, newValue);
7757
+ setEditingCell(null);
7758
+ setPopoverAnchorEl(null);
7759
+ setPendingEditorValue(void 0);
7760
+ if (rowIndex < itemsLengthRef.current - 1) {
7761
+ const newRow = rowIndex + 1;
7762
+ const localCol = globalColIndex - colOffset;
7763
+ setActiveCell({ rowIndex: newRow, columnIndex: globalColIndex });
7764
+ setSelectionRange({ startRow: newRow, startCol: localCol, endRow: newRow, endCol: localCol });
7765
+ }
7766
+ return;
7767
+ }
7768
+ }
4177
7769
  const col = visibleColsRef.current.find((c) => c.columnId === columnId);
4178
7770
  if (col) {
4179
7771
  const result = parseValue(newValue, oldValue, item, col);
@@ -4192,6 +7784,12 @@ function useDataGridEditing(params) {
4192
7784
  newValue,
4193
7785
  rowIndex
4194
7786
  });
7787
+ if (formulas && onFormulaCellChangedRef.current && flatColumnsRef.current) {
7788
+ const colIndex = flatColumnsRef.current.findIndex((c) => c.columnId === columnId);
7789
+ if (colIndex >= 0) {
7790
+ onFormulaCellChangedRef.current(colIndex, rowIndex);
7791
+ }
7792
+ }
4195
7793
  setEditingCell(null);
4196
7794
  setPopoverAnchorEl(null);
4197
7795
  setPendingEditorValue(void 0);
@@ -4202,7 +7800,7 @@ function useDataGridEditing(params) {
4202
7800
  setSelectionRange({ startRow: newRow, startCol: localCol, endRow: newRow, endCol: localCol });
4203
7801
  }
4204
7802
  },
4205
- [setEditingCell, setPendingEditorValue, setActiveCell, setSelectionRange, colOffset, visibleColsRef, itemsLengthRef, onCellValueChangedRef]
7803
+ [formulas, setEditingCell, setPendingEditorValue, setActiveCell, setSelectionRange, colOffset, visibleColsRef, itemsLengthRef, onCellValueChangedRef, setFormulaRef, onFormulaCellChangedRef, flatColumnsRef]
4206
7804
  );
4207
7805
  const cancelPopoverEdit = useCallback(() => {
4208
7806
  setEditingCell(null);
@@ -4249,7 +7847,12 @@ function useDataGridInteraction(params) {
4249
7847
  handleRowCheckboxChange,
4250
7848
  setContextMenuPosition,
4251
7849
  wrapperRef,
4252
- onKeyDown
7850
+ onKeyDown,
7851
+ formulas,
7852
+ flatColumns,
7853
+ getFormula,
7854
+ hasFormula,
7855
+ setFormula
4253
7856
  } = params;
4254
7857
  const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
4255
7858
  const onCellValueChanged = undoRedo.onCellValueChanged;
@@ -4275,7 +7878,12 @@ function useDataGridInteraction(params) {
4275
7878
  editable,
4276
7879
  onCellValueChanged,
4277
7880
  beginBatch: undoRedo.beginBatch,
4278
- endBatch: undoRedo.endBatch
7881
+ endBatch: undoRedo.endBatch,
7882
+ formulas,
7883
+ flatColumns,
7884
+ getFormula,
7885
+ hasFormula,
7886
+ setFormula
4279
7887
  });
4280
7888
  const handleCellMouseDown = useCallback(
4281
7889
  (e, rowIndex, globalColIndex) => {
@@ -4286,6 +7894,10 @@ function useDataGridInteraction(params) {
4286
7894
  },
4287
7895
  [handleCellMouseDownBase, clearClipboardRanges, wrapperRef]
4288
7896
  );
7897
+ const fillFormulaOptions = useMemo(() => {
7898
+ if (!formulas || !flatColumns) return void 0;
7899
+ return { flatColumns, getFormula, hasFormula, setFormula };
7900
+ }, [formulas, flatColumns, getFormula, hasFormula, setFormula]);
4289
7901
  const { handleFillHandleMouseDown, fillDown } = useFillHandle({
4290
7902
  items,
4291
7903
  visibleCols,
@@ -4297,7 +7909,8 @@ function useDataGridInteraction(params) {
4297
7909
  colOffset,
4298
7910
  wrapperRef,
4299
7911
  beginBatch: undoRedo.beginBatch,
4300
- endBatch: undoRedo.endBatch
7912
+ endBatch: undoRedo.endBatch,
7913
+ formulaOptions: fillFormulaOptions
4301
7914
  });
4302
7915
  const { handleGridKeyDown } = useKeyboardNavigation({
4303
7916
  data: { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId },
@@ -4477,7 +8090,12 @@ function useDataGridState(params) {
4477
8090
  handleRowCheckboxChange,
4478
8091
  setContextMenuPosition,
4479
8092
  wrapperRef,
4480
- onKeyDown
8093
+ onKeyDown,
8094
+ formulas: props.formulas,
8095
+ flatColumns: layoutResult.layout.flatColumns,
8096
+ getFormula: props.getFormula,
8097
+ hasFormula: props.hasFormula,
8098
+ setFormula: props.setFormula
4481
8099
  });
4482
8100
  const {
4483
8101
  selectionRange,
@@ -4497,7 +8115,11 @@ function useDataGridState(params) {
4497
8115
  onCellValueChanged,
4498
8116
  setActiveCell,
4499
8117
  setSelectionRange,
4500
- colOffset
8118
+ colOffset,
8119
+ setFormula: props.setFormula,
8120
+ onFormulaCellChanged: props.onFormulaCellChanged,
8121
+ formulas: props.formulas,
8122
+ flatColumns: layoutResult.layout.flatColumns
4501
8123
  });
4502
8124
  const {
4503
8125
  sortBy,
@@ -5026,7 +8648,7 @@ function useInlineCellEditorState(params) {
5026
8648
  e.stopPropagation();
5027
8649
  onCancel();
5028
8650
  }
5029
- if (e.key === "Enter" && editorType === "text") {
8651
+ if (e.key === "Enter" && (editorType === "text" || editorType === "date")) {
5030
8652
  e.preventDefault();
5031
8653
  e.stopPropagation();
5032
8654
  onCommit(localValue);
@@ -5035,7 +8657,7 @@ function useInlineCellEditorState(params) {
5035
8657
  [onCancel, onCommit, localValue, editorType]
5036
8658
  );
5037
8659
  const handleBlur = useCallback(() => {
5038
- if (editorType === "text") {
8660
+ if (editorType === "text" || editorType === "date") {
5039
8661
  onCommit(localValue);
5040
8662
  }
5041
8663
  }, [editorType, localValue, onCommit]);
@@ -6287,12 +9909,12 @@ function calculateRange({
6287
9909
  }
6288
9910
 
6289
9911
  // ../../node_modules/@tanstack/react-virtual/dist/esm/index.js
6290
- var useIsomorphicLayoutEffect = typeof document !== "undefined" ? React4.useLayoutEffect : React4.useEffect;
9912
+ var useIsomorphicLayoutEffect = typeof document !== "undefined" ? React5.useLayoutEffect : React5.useEffect;
6291
9913
  function useVirtualizerBase({
6292
9914
  useFlushSync = true,
6293
9915
  ...options
6294
9916
  }) {
6295
- const rerender = React4.useReducer(() => ({}), {})[1];
9917
+ const rerender = React5.useReducer(() => ({}), {})[1];
6296
9918
  const resolvedOptions = {
6297
9919
  ...options,
6298
9920
  onChange: (instance2, sync) => {
@@ -6305,7 +9927,7 @@ function useVirtualizerBase({
6305
9927
  (_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
6306
9928
  }
6307
9929
  };
6308
- const [instance] = React4.useState(
9930
+ const [instance] = React5.useState(
6309
9931
  () => new Virtualizer(resolvedOptions)
6310
9932
  );
6311
9933
  instance.setOptions(resolvedOptions);
@@ -6532,12 +10154,26 @@ function useDataGridTableOrchestration(params) {
6532
10154
  density = "normal",
6533
10155
  pinnedColumns,
6534
10156
  currentPage = 1,
6535
- pageSize: propPageSize = 25
10157
+ pageSize: propPageSize = 25,
10158
+ showColumnLetters = false,
10159
+ showNameBox = false,
10160
+ onActiveCellChange
6536
10161
  } = props;
6537
10162
  const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
6538
10163
  const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
6539
10164
  const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
6540
10165
  const fitToContent = layoutMode === "content";
10166
+ const onActiveCellChangeRef = useRef(onActiveCellChange);
10167
+ onActiveCellChangeRef.current = onActiveCellChange;
10168
+ useEffect(() => {
10169
+ if (!onActiveCellChangeRef.current) return;
10170
+ const ac = interaction.activeCell;
10171
+ if (ac) {
10172
+ onActiveCellChangeRef.current(formatCellReference(ac.columnIndex, rowNumberOffset + ac.rowIndex + 1));
10173
+ } else {
10174
+ onActiveCellChangeRef.current(null);
10175
+ }
10176
+ }, [interaction.activeCell, rowNumberOffset]);
6541
10177
  const { handleResizeStart, handleResizeDoubleClick, getColumnWidth } = useColumnResize({
6542
10178
  columnSizingOverrides,
6543
10179
  setColumnSizingOverrides
@@ -6657,6 +10293,8 @@ function useDataGridTableOrchestration(params) {
6657
10293
  headerRows,
6658
10294
  allowOverflowX,
6659
10295
  fitToContent,
10296
+ showColumnLetters,
10297
+ showNameBox,
6660
10298
  // Memoized callback groups
6661
10299
  editCallbacks,
6662
10300
  interactionHandlers,
@@ -7241,7 +10879,8 @@ var richSelectDropdownStyle = {
7241
10879
  background: "var(--ogrid-bg, #fff)",
7242
10880
  border: "1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))",
7243
10881
  zIndex: 10,
7244
- boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))"
10882
+ boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))",
10883
+ textAlign: "left"
7245
10884
  };
7246
10885
  var richSelectOptionStyle = {
7247
10886
  padding: "6px 8px",
@@ -7283,7 +10922,7 @@ var selectChevronStyle = {
7283
10922
  };
7284
10923
  function BaseInlineCellEditor(props) {
7285
10924
  const { value, column, editorType, onCommit, onCancel, renderCheckbox } = props;
7286
- const wrapperRef = React4.useRef(null);
10925
+ const wrapperRef = React5.useRef(null);
7287
10926
  const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
7288
10927
  const editorValues = column.cellEditorParams?.values ?? [];
7289
10928
  const editorFormatValue = column.cellEditorParams?.formatValue;
@@ -7300,8 +10939,8 @@ function BaseInlineCellEditor(props) {
7300
10939
  onCommit,
7301
10940
  onCancel
7302
10941
  });
7303
- const [fixedDropdownStyle, setFixedDropdownStyle] = React4.useState(null);
7304
- React4.useLayoutEffect(() => {
10942
+ const [fixedDropdownStyle, setFixedDropdownStyle] = React5.useState(null);
10943
+ React5.useLayoutEffect(() => {
7305
10944
  if (editorType !== "select" && editorType !== "richSelect") return;
7306
10945
  const wrapper = wrapperRef.current;
7307
10946
  if (!wrapper) return;
@@ -7319,17 +10958,25 @@ function BaseInlineCellEditor(props) {
7319
10958
  background: "var(--ogrid-bg, #fff)",
7320
10959
  border: "1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))",
7321
10960
  zIndex: 9999,
7322
- boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))"
10961
+ boxShadow: "var(--ogrid-shadow, 0 4px 16px rgba(0,0,0,0.2))",
10962
+ textAlign: "left"
7323
10963
  });
7324
10964
  }, [editorType]);
7325
10965
  const computedDropdownStyle = fixedDropdownStyle ?? richSelectDropdownStyle;
7326
- React4.useEffect(() => {
10966
+ React5.useEffect(() => {
7327
10967
  const wrapper = wrapperRef.current;
7328
10968
  if (!wrapper) return;
7329
10969
  const input = wrapper.querySelector("input");
7330
10970
  if (input) {
7331
10971
  input.focus();
7332
- input.select();
10972
+ if (editorType === "date") {
10973
+ try {
10974
+ input.showPicker();
10975
+ } catch {
10976
+ }
10977
+ } else {
10978
+ input.select();
10979
+ }
7333
10980
  } else {
7334
10981
  wrapper.focus();
7335
10982
  }
@@ -7417,12 +11064,12 @@ function BaseInlineCellEditor(props) {
7417
11064
  var menuPositionStyle = (x, y) => ({ left: x, top: y });
7418
11065
  function GridContextMenu(props) {
7419
11066
  const { x, y, hasSelection, canUndo, canRedo, onClose, onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, classNames } = props;
7420
- const ref = React4.useRef(null);
7421
- const handlers = React4.useMemo(
11067
+ const ref = React5.useRef(null);
11068
+ const handlers = React5.useMemo(
7422
11069
  () => getContextMenuHandlers({ onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose }),
7423
11070
  [onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose]
7424
11071
  );
7425
- const isDisabled = React4.useCallback(
11072
+ const isDisabled = React5.useCallback(
7426
11073
  (item) => {
7427
11074
  if (item.disabledWhenNoSelection && !hasSelection) return true;
7428
11075
  if (item.id === "undo" && !canUndo) return true;
@@ -7431,7 +11078,7 @@ function GridContextMenu(props) {
7431
11078
  },
7432
11079
  [hasSelection, canUndo, canRedo]
7433
11080
  );
7434
- React4.useEffect(() => {
11081
+ React5.useEffect(() => {
7435
11082
  const handleClickOutside = (e) => {
7436
11083
  if (ref.current && !ref.current.contains(e.target)) onClose();
7437
11084
  };
@@ -7453,7 +11100,7 @@ function GridContextMenu(props) {
7453
11100
  role: "menu",
7454
11101
  style: menuPositionStyle(x, y),
7455
11102
  "aria-label": "Grid context menu",
7456
- children: GRID_CONTEXT_MENU_ITEMS.map((item) => /* @__PURE__ */ jsxs(React4.Fragment, { children: [
11103
+ children: GRID_CONTEXT_MENU_ITEMS.map((item) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
7457
11104
  item.dividerBefore && /* @__PURE__ */ jsx("div", { className: classNames?.contextMenuDivider }),
7458
11105
  /* @__PURE__ */ jsxs(
7459
11106
  "button",
@@ -7614,9 +11261,9 @@ function BaseColumnHeaderMenu(props) {
7614
11261
  classNames,
7615
11262
  getPortalTarget
7616
11263
  } = props;
7617
- const [position, setPosition] = React4.useState(null);
7618
- const menuRef = React4.useRef(null);
7619
- React4.useEffect(() => {
11264
+ const [position, setPosition] = React5.useState(null);
11265
+ const menuRef = React5.useRef(null);
11266
+ React5.useEffect(() => {
7620
11267
  if (!isOpen || !anchorElement) {
7621
11268
  setPosition(null);
7622
11269
  return;
@@ -7645,7 +11292,7 @@ function BaseColumnHeaderMenu(props) {
7645
11292
  document.removeEventListener("keydown", handleEscape);
7646
11293
  };
7647
11294
  }, [isOpen, anchorElement, onClose]);
7648
- const menuInput = React4.useMemo(
11295
+ const menuInput = React5.useMemo(
7649
11296
  () => ({
7650
11297
  canPinLeft,
7651
11298
  canPinRight,
@@ -7656,8 +11303,8 @@ function BaseColumnHeaderMenu(props) {
7656
11303
  }),
7657
11304
  [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]
7658
11305
  );
7659
- const items = React4.useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
7660
- const handlers = React4.useMemo(
11306
+ const items = React5.useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
11307
+ const handlers = React5.useMemo(
7661
11308
  () => ({
7662
11309
  pinLeft: onPinLeft,
7663
11310
  pinRight: onPinRight,
@@ -7684,7 +11331,7 @@ function BaseColumnHeaderMenu(props) {
7684
11331
  left: position.left,
7685
11332
  zIndex: 1e3
7686
11333
  },
7687
- children: items.map((item, idx) => /* @__PURE__ */ jsxs(React4.Fragment, { children: [
11334
+ children: items.map((item, idx) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
7688
11335
  /* @__PURE__ */ jsx(
7689
11336
  "button",
7690
11337
  {
@@ -7750,14 +11397,14 @@ function createOGrid(components) {
7750
11397
  );
7751
11398
  });
7752
11399
  OGridInner.displayName = "OGrid";
7753
- return React4.memo(OGridInner);
11400
+ return React5.memo(OGridInner);
7754
11401
  }
7755
11402
  var DEFAULT_FALLBACK_STYLE = {
7756
11403
  color: "var(--ogrid-error, #d32f2f)",
7757
11404
  fontSize: "0.75rem",
7758
11405
  padding: "2px 4px"
7759
11406
  };
7760
- var CellErrorBoundary = class extends React4.Component {
11407
+ var CellErrorBoundary = class extends React5.Component {
7761
11408
  constructor(props) {
7762
11409
  super(props);
7763
11410
  this.state = { hasError: false };
@@ -7933,4 +11580,4 @@ function renderFilterContent(filterType, state, options, isLoadingOptions, selec
7933
11580
  return null;
7934
11581
  }
7935
11582
 
7936
- export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
11583
+ export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellReference, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, indexToColumnLetter, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useFormulaEngine, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };