@alaarab/ogrid-js 2.3.0 → 2.4.1

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
@@ -81,6 +81,17 @@ function getCellValue(item, col) {
81
81
  function isColumnEditable(col, item) {
82
82
  return col.editable === true || typeof col.editable === "function" && col.editable(item);
83
83
  }
84
+ function createGridDataAccessor(items, flatColumns) {
85
+ return {
86
+ getCellValue: (col, row) => {
87
+ if (row < 0 || row >= items.length) return null;
88
+ if (col < 0 || col >= flatColumns.length) return null;
89
+ return getCellValue(items[row], flatColumns[col]);
90
+ },
91
+ getRowCount: () => items.length,
92
+ getColumnCount: () => flatColumns.length
93
+ };
94
+ }
84
95
  function isColumnGroupDef(c) {
85
96
  return "children" in c && Array.isArray(c.children);
86
97
  }
@@ -1107,7 +1118,7 @@ var _CellDescriptorCache = class _CellDescriptorCache2 {
1107
1118
  const sr = input.selectionRange;
1108
1119
  const cr = input.cutRange;
1109
1120
  const cp = input.copyRange;
1110
- return (ec ? `${String(ec.rowId)}\0${ec.columnId}` : "") + "" + (ac ? `${ac.rowIndex}\0${ac.columnIndex}` : "") + "" + (sr ? `${sr.startRow}\0${sr.startCol}\0${sr.endRow}\0${sr.endCol}` : "") + "" + (cr ? `${cr.startRow}\0${cr.startCol}\0${cr.endRow}\0${cr.endCol}` : "") + "" + (cp ? `${cp.startRow}\0${cp.startCol}\0${cp.endRow}\0${cp.endCol}` : "") + "" + (input.isDragging ? "1" : "0") + "" + (input.editable !== false ? "1" : "0") + "" + (input.onCellValueChanged ? "1" : "0");
1121
+ return (ec ? `${String(ec.rowId)}\0${ec.columnId}` : "") + "" + (ac ? `${ac.rowIndex}\0${ac.columnIndex}` : "") + "" + (sr ? `${sr.startRow}\0${sr.startCol}\0${sr.endRow}\0${sr.endCol}` : "") + "" + (cr ? `${cr.startRow}\0${cr.startCol}\0${cr.endRow}\0${cr.endCol}` : "") + "" + (cp ? `${cp.startRow}\0${cp.startCol}\0${cp.endRow}\0${cp.endCol}` : "") + "" + (input.isDragging ? "1" : "0") + "" + (input.editable !== false ? "1" : "0") + "" + (input.onCellValueChanged ? "1" : "0") + "" + (input.formulaVersion ?? 0);
1111
1122
  }
1112
1123
  /**
1113
1124
  * Get a cached descriptor or compute a new one.
@@ -1178,6 +1189,7 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
1178
1189
  const isPinned = col.pinned != null;
1179
1190
  const pinnedSide = col.pinned ?? void 0;
1180
1191
  const cellValue = getCellValue(item, col);
1192
+ const formulaDisplay = input.hasFormula?.(colIdx, rowIndex) ? input.getFormulaValue?.(colIdx, rowIndex) : void 0;
1181
1193
  let mode = "display";
1182
1194
  let editorType;
1183
1195
  if (isEditing && canEditInline) {
@@ -1209,7 +1221,8 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
1209
1221
  globalColIndex,
1210
1222
  rowId,
1211
1223
  rowIndex,
1212
- displayValue: cellValue
1224
+ displayValue: formulaDisplay !== void 0 ? formulaDisplay : cellValue,
1225
+ columnType: col.type
1213
1226
  };
1214
1227
  }
1215
1228
  function resolveCellDisplayContent(col, item, displayValue) {
@@ -1224,7 +1237,7 @@ function resolveCellDisplayContent(col, item, displayValue) {
1224
1237
  if (displayValue == null) return null;
1225
1238
  if (col.type === "date") {
1226
1239
  const d = new Date(String(displayValue));
1227
- if (!Number.isNaN(d.getTime())) return d.toLocaleDateString();
1240
+ if (!Number.isNaN(d.getTime())) return d.toLocaleDateString(void 0, { timeZone: "UTC" });
1228
1241
  }
1229
1242
  if (col.type === "boolean") {
1230
1243
  return displayValue ? "True" : "False";
@@ -1342,6 +1355,8 @@ var CHECKBOX_COLUMN_WIDTH = 48;
1342
1355
  var ROW_NUMBER_COLUMN_WIDTH = 50;
1343
1356
  var DEFAULT_MIN_COLUMN_WIDTH = 80;
1344
1357
  var CELL_PADDING = 16;
1358
+ var ROW_NUMBER_COLUMN_MIN_WIDTH = 30;
1359
+ var ROW_NUMBER_COLUMN_ID = "__row_number__";
1345
1360
  var GRID_BORDER_RADIUS = 6;
1346
1361
  var AUTOSIZE_EXTRA_PX = 16;
1347
1362
  var AUTOSIZE_MAX_PX = 520;
@@ -1994,59 +2009,6 @@ function validateRowIds(items, getRowId) {
1994
2009
  ids.add(id);
1995
2010
  }
1996
2011
  }
1997
- var DEFAULT_DEBOUNCE_MS = 300;
1998
- var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
1999
- var SIDEBAR_TRANSITION_MS = 300;
2000
- var Z_INDEX = {
2001
- /** Column resize drag handle */
2002
- RESIZE_HANDLE: 1,
2003
- /** Active/editing cell outline */
2004
- ACTIVE_CELL: 2,
2005
- /** Fill handle dot */
2006
- FILL_HANDLE: 3,
2007
- /** Selection range overlay (marching ants) */
2008
- SELECTION_OVERLAY: 4,
2009
- /** Row number column */
2010
- ROW_NUMBER: 5,
2011
- /** Clipboard overlay (copy/cut animation) */
2012
- CLIPBOARD_OVERLAY: 5,
2013
- /** Sticky pinned body cells */
2014
- PINNED: 6,
2015
- /** Selection checkbox column in body */
2016
- SELECTION_CELL: 7,
2017
- /** Sticky thead row */
2018
- THEAD: 8,
2019
- /** Pinned header cells (sticky both axes) */
2020
- PINNED_HEADER: 10,
2021
- /** Focused header cell */
2022
- HEADER_FOCUS: 11,
2023
- /** Checkbox column in sticky header (sticky both axes) */
2024
- SELECTION_HEADER_PINNED: 12,
2025
- /** Loading overlay within table */
2026
- LOADING: 2,
2027
- /** Column reorder drop indicator */
2028
- DROP_INDICATOR: 100,
2029
- /** Dropdown menus (column chooser, pagination size select) */
2030
- DROPDOWN: 1e3,
2031
- /** Filter popovers */
2032
- FILTER_POPOVER: 1e3,
2033
- /** Modal dialogs */
2034
- MODAL: 2e3,
2035
- /** Fullscreen grid container */
2036
- FULLSCREEN: 9999,
2037
- /** Context menus (right-click grid menu) */
2038
- CONTEXT_MENU: 1e4
2039
- };
2040
- var REF_ERROR = new FormulaError("#REF!", "Invalid cell reference");
2041
- var DIV_ZERO_ERROR = new FormulaError("#DIV/0!", "Division by zero");
2042
- var VALUE_ERROR = new FormulaError("#VALUE!", "Wrong value type");
2043
- var NAME_ERROR = new FormulaError("#NAME?", "Unknown function or name");
2044
- var CIRC_ERROR = new FormulaError("#CIRC!", "Circular reference");
2045
- var GENERAL_ERROR = new FormulaError("#ERROR!", "Formula error");
2046
- var NA_ERROR = new FormulaError("#N/A", "No match found");
2047
- function isFormulaError(value) {
2048
- return value instanceof FormulaError;
2049
- }
2050
2012
  var CELL_REF_PATTERN = /^\$?[A-Za-z]+\$?\d+$/;
2051
2013
  var SINGLE_CHAR_OPERATORS = {
2052
2014
  "+": "PLUS",
@@ -2168,6 +2130,12 @@ function tokenize(input) {
2168
2130
  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] === "_")) {
2169
2131
  pos++;
2170
2132
  }
2133
+ if (pos < input.length && input[pos] === "." && pos + 1 < input.length && (input[pos + 1] >= "A" && input[pos + 1] <= "Z" || input[pos + 1] >= "a" && input[pos + 1] <= "z")) {
2134
+ pos++;
2135
+ while (pos < input.length && (input[pos] >= "A" && input[pos] <= "Z" || input[pos] >= "a" && input[pos] <= "z" || input[pos] >= "0" && input[pos] <= "9" || input[pos] === "_")) {
2136
+ pos++;
2137
+ }
2138
+ }
2171
2139
  const word = input.slice(start, pos);
2172
2140
  if (pos < input.length && input[pos] === "!") {
2173
2141
  pos++;
@@ -2195,6 +2163,190 @@ function tokenize(input) {
2195
2163
  tokens.push({ type: "EOF", value: "", position: pos });
2196
2164
  return tokens;
2197
2165
  }
2166
+ var CELL_REF_RE2 = /^\$?([A-Za-z]+)\$?(\d+)$/;
2167
+ function parseCellRefCoords(ref) {
2168
+ const m = ref.match(CELL_REF_RE2);
2169
+ if (!m) return null;
2170
+ return { col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 };
2171
+ }
2172
+ function handleFormulaBarKeyDown(key, preventDefault, onCommit, onCancel) {
2173
+ if (key === "Enter") {
2174
+ preventDefault();
2175
+ onCommit();
2176
+ } else if (key === "Escape") {
2177
+ preventDefault();
2178
+ onCancel();
2179
+ }
2180
+ }
2181
+ function processFormulaBarCommit(text, col, row, setFormula, onCellValueChanged) {
2182
+ const trimmed = text.trim();
2183
+ if (trimmed.startsWith("=")) {
2184
+ setFormula(col, row, trimmed);
2185
+ } else {
2186
+ setFormula(col, row, null);
2187
+ onCellValueChanged?.(col, row, trimmed);
2188
+ }
2189
+ }
2190
+ function deriveFormulaBarText(col, row, getFormula, getRawValue) {
2191
+ if (col == null || row == null) return "";
2192
+ const formula = getFormula?.(col, row);
2193
+ if (formula) return formula;
2194
+ const raw = getRawValue?.(col, row);
2195
+ return raw != null ? String(raw) : "";
2196
+ }
2197
+ function extractFormulaReferences(formula) {
2198
+ if (!formula || formula[0] !== "=") return [];
2199
+ const refs = [];
2200
+ let colorIdx = 0;
2201
+ try {
2202
+ const tokens = tokenize(formula.substring(1));
2203
+ for (let i = 0; i < tokens.length; i++) {
2204
+ const tok = tokens[i];
2205
+ if (tok.type === "CELL_REF") {
2206
+ if (i + 2 < tokens.length && tokens[i + 1].type === "COLON" && tokens[i + 2].type === "CELL_REF") {
2207
+ const start = parseCellRefCoords(tok.value);
2208
+ const end = parseCellRefCoords(tokens[i + 2].value);
2209
+ if (start && end) {
2210
+ refs.push({
2211
+ type: "range",
2212
+ col: start.col,
2213
+ row: start.row,
2214
+ endCol: end.col,
2215
+ endRow: end.row,
2216
+ colorIndex: colorIdx++ % 6
2217
+ });
2218
+ i += 2;
2219
+ continue;
2220
+ }
2221
+ }
2222
+ const coords = parseCellRefCoords(tok.value);
2223
+ if (coords) {
2224
+ refs.push({
2225
+ type: "cell",
2226
+ col: coords.col,
2227
+ row: coords.row,
2228
+ colorIndex: colorIdx++ % 6
2229
+ });
2230
+ }
2231
+ }
2232
+ }
2233
+ } catch {
2234
+ }
2235
+ return refs;
2236
+ }
2237
+ var DEFAULT_DEBOUNCE_MS = 300;
2238
+ var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
2239
+ var SIDEBAR_TRANSITION_MS = 300;
2240
+ var Z_INDEX = {
2241
+ /** Column resize drag handle */
2242
+ RESIZE_HANDLE: 1,
2243
+ /** Active/editing cell outline */
2244
+ ACTIVE_CELL: 2,
2245
+ /** Fill handle dot */
2246
+ FILL_HANDLE: 3,
2247
+ /** Selection range overlay (marching ants) */
2248
+ SELECTION_OVERLAY: 4,
2249
+ /** Row number column */
2250
+ ROW_NUMBER: 5,
2251
+ /** Clipboard overlay (copy/cut animation) */
2252
+ CLIPBOARD_OVERLAY: 5,
2253
+ /** Sticky pinned body cells */
2254
+ PINNED: 6,
2255
+ /** Selection checkbox column in body */
2256
+ SELECTION_CELL: 7,
2257
+ /** Sticky thead row */
2258
+ THEAD: 8,
2259
+ /** Pinned header cells (sticky both axes) */
2260
+ PINNED_HEADER: 10,
2261
+ /** Focused header cell */
2262
+ HEADER_FOCUS: 11,
2263
+ /** Checkbox column in sticky header (sticky both axes) */
2264
+ SELECTION_HEADER_PINNED: 12,
2265
+ /** Loading overlay within table */
2266
+ LOADING: 2,
2267
+ /** Column reorder drop indicator */
2268
+ DROP_INDICATOR: 100,
2269
+ /** Dropdown menus (column chooser, pagination size select) */
2270
+ DROPDOWN: 1e3,
2271
+ /** Filter popovers */
2272
+ FILTER_POPOVER: 1e3,
2273
+ /** Modal dialogs */
2274
+ MODAL: 2e3,
2275
+ /** Fullscreen grid container */
2276
+ FULLSCREEN: 9999,
2277
+ /** Context menus (right-click grid menu) */
2278
+ CONTEXT_MENU: 1e4
2279
+ };
2280
+ var FORMULA_REF_COLORS = [
2281
+ "var(--ogrid-formula-ref-0, #4285f4)",
2282
+ "var(--ogrid-formula-ref-1, #ea4335)",
2283
+ "var(--ogrid-formula-ref-2, #34a853)",
2284
+ "var(--ogrid-formula-ref-3, #9334e6)",
2285
+ "var(--ogrid-formula-ref-4, #ff6d01)",
2286
+ "var(--ogrid-formula-ref-5, #46bdc6)"
2287
+ ];
2288
+ var FORMULA_BAR_CSS = {
2289
+ bar: "display:flex;align-items:center;border-bottom:1px solid var(--ogrid-border, #e0e0e0);background:var(--ogrid-bg, #fff);min-height:28px;font-size:13px;",
2290
+ nameBox: "font-family:monospace;font-size:12px;font-weight:500;padding:2px 8px;border-right:1px solid var(--ogrid-border, #e0e0e0);background:var(--ogrid-bg, #fff);color:var(--ogrid-fg, #242424);min-width:52px;text-align:center;line-height:24px;user-select:none;white-space:nowrap;",
2291
+ fxLabel: "padding:2px 8px;font-style:italic;font-weight:600;color:var(--ogrid-muted-fg, #888);user-select:none;border-right:1px solid var(--ogrid-border, #e0e0e0);line-height:24px;font-size:12px;",
2292
+ input: "flex:1;border:none;outline:none;padding:2px 8px;font-family:monospace;font-size:12px;line-height:24px;background:transparent;color:var(--ogrid-fg, #242424);min-width:0;"
2293
+ };
2294
+ var FORMULA_BAR_STYLES = {
2295
+ bar: {
2296
+ display: "flex",
2297
+ alignItems: "center",
2298
+ borderBottom: "1px solid var(--ogrid-border, #e0e0e0)",
2299
+ background: "var(--ogrid-bg, #fff)",
2300
+ minHeight: "28px",
2301
+ fontSize: "13px"
2302
+ },
2303
+ nameBox: {
2304
+ fontFamily: "monospace",
2305
+ fontSize: "12px",
2306
+ fontWeight: 500,
2307
+ padding: "2px 8px",
2308
+ borderRight: "1px solid var(--ogrid-border, #e0e0e0)",
2309
+ background: "var(--ogrid-bg, #fff)",
2310
+ color: "var(--ogrid-fg, #242424)",
2311
+ minWidth: "52px",
2312
+ textAlign: "center",
2313
+ lineHeight: "24px",
2314
+ userSelect: "none",
2315
+ whiteSpace: "nowrap"
2316
+ },
2317
+ fxLabel: {
2318
+ padding: "2px 8px",
2319
+ fontStyle: "italic",
2320
+ fontWeight: 600,
2321
+ color: "var(--ogrid-muted-fg, #888)",
2322
+ userSelect: "none",
2323
+ borderRight: "1px solid var(--ogrid-border, #e0e0e0)",
2324
+ lineHeight: "24px",
2325
+ fontSize: "12px"
2326
+ },
2327
+ input: {
2328
+ flex: 1,
2329
+ border: "none",
2330
+ outline: "none",
2331
+ padding: "2px 8px",
2332
+ fontFamily: "monospace",
2333
+ fontSize: "12px",
2334
+ lineHeight: "24px",
2335
+ background: "transparent",
2336
+ color: "var(--ogrid-fg, #242424)",
2337
+ minWidth: 0
2338
+ }
2339
+ };
2340
+ var REF_ERROR = new FormulaError("#REF!", "Invalid cell reference");
2341
+ var DIV_ZERO_ERROR = new FormulaError("#DIV/0!", "Division by zero");
2342
+ var VALUE_ERROR = new FormulaError("#VALUE!", "Wrong value type");
2343
+ var NAME_ERROR = new FormulaError("#NAME?", "Unknown function or name");
2344
+ var CIRC_ERROR = new FormulaError("#CIRC!", "Circular reference");
2345
+ var GENERAL_ERROR = new FormulaError("#ERROR!", "Formula error");
2346
+ var NA_ERROR = new FormulaError("#N/A", "No match found");
2347
+ function isFormulaError(value) {
2348
+ return value instanceof FormulaError;
2349
+ }
2198
2350
  function parse(tokens, namedRanges) {
2199
2351
  let pos = 0;
2200
2352
  function peek() {
@@ -2839,7 +2991,7 @@ var DependencyGraph = class {
2839
2991
  if (cellDependents) {
2840
2992
  for (const dependent of cellDependents) {
2841
2993
  if (affected.has(dependent)) {
2842
- const newDegree = inDegree.get(dependent) - 1;
2994
+ const newDegree = (inDegree.get(dependent) ?? 0) - 1;
2843
2995
  inDegree.set(dependent, newDegree);
2844
2996
  if (newDegree === 0) {
2845
2997
  queue.push(dependent);
@@ -3149,7 +3301,7 @@ function registerMathFunctions(registry) {
3149
3301
  registry.set("SUMPRODUCT", {
3150
3302
  minArgs: 1,
3151
3303
  maxArgs: -1,
3152
- evaluate(args, context, evaluator) {
3304
+ evaluate(args, context, _evaluator) {
3153
3305
  const arrays = [];
3154
3306
  for (const arg of args) {
3155
3307
  if (arg.kind !== "range") {
@@ -3368,6 +3520,162 @@ function registerMathFunctions(registry) {
3368
3520
  return Math.floor(Math.random() * (hi - lo + 1)) + lo;
3369
3521
  }
3370
3522
  });
3523
+ registry.set("MROUND", {
3524
+ minArgs: 2,
3525
+ maxArgs: 2,
3526
+ evaluate(args, context, evaluator) {
3527
+ const rawNum = evaluator.evaluate(args[0], context);
3528
+ if (rawNum instanceof FormulaError) return rawNum;
3529
+ const num = toNumber(rawNum);
3530
+ if (num instanceof FormulaError) return num;
3531
+ const rawMul = evaluator.evaluate(args[1], context);
3532
+ if (rawMul instanceof FormulaError) return rawMul;
3533
+ const multiple = toNumber(rawMul);
3534
+ if (multiple instanceof FormulaError) return multiple;
3535
+ if (multiple === 0) return 0;
3536
+ if (num > 0 && multiple < 0 || num < 0 && multiple > 0) {
3537
+ return new FormulaError("#NUM!", "MROUND: number and multiple must have the same sign");
3538
+ }
3539
+ return Math.round(num / multiple) * multiple;
3540
+ }
3541
+ });
3542
+ registry.set("QUOTIENT", {
3543
+ minArgs: 2,
3544
+ maxArgs: 2,
3545
+ evaluate(args, context, evaluator) {
3546
+ const rawNum = evaluator.evaluate(args[0], context);
3547
+ if (rawNum instanceof FormulaError) return rawNum;
3548
+ const num = toNumber(rawNum);
3549
+ if (num instanceof FormulaError) return num;
3550
+ const rawDen = evaluator.evaluate(args[1], context);
3551
+ if (rawDen instanceof FormulaError) return rawDen;
3552
+ const den = toNumber(rawDen);
3553
+ if (den instanceof FormulaError) return den;
3554
+ if (den === 0) return new FormulaError("#DIV/0!", "QUOTIENT: division by zero");
3555
+ return Math.trunc(num / den);
3556
+ }
3557
+ });
3558
+ registry.set("COMBIN", {
3559
+ minArgs: 2,
3560
+ maxArgs: 2,
3561
+ evaluate(args, context, evaluator) {
3562
+ const rawN = evaluator.evaluate(args[0], context);
3563
+ if (rawN instanceof FormulaError) return rawN;
3564
+ const n = toNumber(rawN);
3565
+ if (n instanceof FormulaError) return n;
3566
+ const rawK = evaluator.evaluate(args[1], context);
3567
+ if (rawK instanceof FormulaError) return rawK;
3568
+ const k = toNumber(rawK);
3569
+ if (k instanceof FormulaError) return k;
3570
+ const ni = Math.trunc(n);
3571
+ const ki = Math.trunc(k);
3572
+ if (ni < 0 || ki < 0) return new FormulaError("#NUM!", "COMBIN: n and k must be non-negative");
3573
+ if (ki > ni) return new FormulaError("#NUM!", "COMBIN: k must be <= n");
3574
+ if (ki === 0 || ki === ni) return 1;
3575
+ const kk = Math.min(ki, ni - ki);
3576
+ let result = 1;
3577
+ for (let i = 0; i < kk; i++) {
3578
+ result = result * (ni - i) / (i + 1);
3579
+ }
3580
+ return Math.round(result);
3581
+ }
3582
+ });
3583
+ registry.set("PERMUT", {
3584
+ minArgs: 2,
3585
+ maxArgs: 2,
3586
+ evaluate(args, context, evaluator) {
3587
+ const rawN = evaluator.evaluate(args[0], context);
3588
+ if (rawN instanceof FormulaError) return rawN;
3589
+ const n = toNumber(rawN);
3590
+ if (n instanceof FormulaError) return n;
3591
+ const rawK = evaluator.evaluate(args[1], context);
3592
+ if (rawK instanceof FormulaError) return rawK;
3593
+ const k = toNumber(rawK);
3594
+ if (k instanceof FormulaError) return k;
3595
+ const ni = Math.trunc(n);
3596
+ const ki = Math.trunc(k);
3597
+ if (ni < 0 || ki < 0) return new FormulaError("#NUM!", "PERMUT: n and k must be non-negative");
3598
+ if (ki > ni) return new FormulaError("#NUM!", "PERMUT: k must be <= n");
3599
+ let result = 1;
3600
+ for (let i = 0; i < ki; i++) {
3601
+ result *= ni - i;
3602
+ }
3603
+ return result;
3604
+ }
3605
+ });
3606
+ registry.set("FACT", {
3607
+ minArgs: 1,
3608
+ maxArgs: 1,
3609
+ evaluate(args, context, evaluator) {
3610
+ const rawVal = evaluator.evaluate(args[0], context);
3611
+ if (rawVal instanceof FormulaError) return rawVal;
3612
+ const num = toNumber(rawVal);
3613
+ if (num instanceof FormulaError) return num;
3614
+ const n = Math.trunc(num);
3615
+ if (n < 0) return new FormulaError("#NUM!", "FACT: argument must be non-negative");
3616
+ if (n > 170) return new FormulaError("#NUM!", "FACT: argument too large (>170)");
3617
+ let result = 1;
3618
+ for (let i = 2; i <= n; i++) {
3619
+ result *= i;
3620
+ }
3621
+ return result;
3622
+ }
3623
+ });
3624
+ registry.set("GCD", {
3625
+ minArgs: 1,
3626
+ maxArgs: -1,
3627
+ evaluate(args, context, evaluator) {
3628
+ const nums = [];
3629
+ for (const arg of args) {
3630
+ const rawVal = evaluator.evaluate(arg, context);
3631
+ if (rawVal instanceof FormulaError) return rawVal;
3632
+ const v = toNumber(rawVal);
3633
+ if (v instanceof FormulaError) return v;
3634
+ const n = Math.trunc(Math.abs(v));
3635
+ nums.push(n);
3636
+ }
3637
+ if (nums.length === 0) return new FormulaError("#NUM!", "GCD: no arguments");
3638
+ let result = nums[0];
3639
+ for (let i = 1; i < nums.length; i++) {
3640
+ result = gcdTwo(result, nums[i]);
3641
+ }
3642
+ return result;
3643
+ }
3644
+ });
3645
+ registry.set("LCM", {
3646
+ minArgs: 1,
3647
+ maxArgs: -1,
3648
+ evaluate(args, context, evaluator) {
3649
+ const nums = [];
3650
+ for (const arg of args) {
3651
+ const rawVal = evaluator.evaluate(arg, context);
3652
+ if (rawVal instanceof FormulaError) return rawVal;
3653
+ const v = toNumber(rawVal);
3654
+ if (v instanceof FormulaError) return v;
3655
+ const n = Math.trunc(Math.abs(v));
3656
+ nums.push(n);
3657
+ }
3658
+ if (nums.length === 0) return new FormulaError("#NUM!", "LCM: no arguments");
3659
+ let result = nums[0];
3660
+ for (let i = 1; i < nums.length; i++) {
3661
+ const g = gcdTwo(result, nums[i]);
3662
+ if (g === 0) {
3663
+ result = 0;
3664
+ break;
3665
+ }
3666
+ result = result / g * nums[i];
3667
+ }
3668
+ return result;
3669
+ }
3670
+ });
3671
+ }
3672
+ function gcdTwo(a, b) {
3673
+ while (b !== 0) {
3674
+ const t = b;
3675
+ b = a % b;
3676
+ a = t;
3677
+ }
3678
+ return a;
3371
3679
  }
3372
3680
  function flattenArgs2(args, context, evaluator) {
3373
3681
  const result = [];
@@ -4202,48 +4510,194 @@ function registerTextFunctions(registry) {
4202
4510
  return parts.join(delimiter);
4203
4511
  }
4204
4512
  });
4205
- }
4206
- function toDate(val) {
4207
- if (val instanceof FormulaError) return val;
4208
- if (val instanceof Date) {
4209
- if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
4210
- return val;
4211
- }
4212
- if (typeof val === "string") {
4213
- const d = new Date(val);
4214
- if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
4215
- return d;
4216
- }
4217
- if (typeof val === "number") {
4218
- const d = new Date(val);
4219
- if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
4220
- return d;
4221
- }
4222
- return new FormulaError("#VALUE!", "Cannot convert value to date");
4223
- }
4224
- function registerDateFunctions(registry) {
4225
- registry.set("TODAY", {
4226
- minArgs: 0,
4227
- maxArgs: 0,
4228
- evaluate(_args, context) {
4229
- const now = context.now();
4230
- return new Date(now.getFullYear(), now.getMonth(), now.getDate());
4231
- }
4232
- });
4233
- registry.set("NOW", {
4234
- minArgs: 0,
4235
- maxArgs: 0,
4236
- evaluate(_args, context) {
4237
- return context.now();
4238
- }
4239
- });
4240
- registry.set("YEAR", {
4513
+ registry.set("DOLLAR", {
4241
4514
  minArgs: 1,
4242
- maxArgs: 1,
4515
+ maxArgs: 2,
4243
4516
  evaluate(args, context, evaluator) {
4244
- const val = evaluator.evaluate(args[0], context);
4245
- if (val instanceof FormulaError) return val;
4246
- const date = toDate(val);
4517
+ const rawNum = evaluator.evaluate(args[0], context);
4518
+ if (rawNum instanceof FormulaError) return rawNum;
4519
+ const num = toNumber(rawNum);
4520
+ if (num instanceof FormulaError) return num;
4521
+ let decimals = 2;
4522
+ if (args.length >= 2) {
4523
+ const rawDec = evaluator.evaluate(args[1], context);
4524
+ if (rawDec instanceof FormulaError) return rawDec;
4525
+ const d = toNumber(rawDec);
4526
+ if (d instanceof FormulaError) return d;
4527
+ decimals = Math.trunc(d);
4528
+ }
4529
+ const absNum = Math.abs(num);
4530
+ const rounded = decimals >= 0 ? absNum.toFixed(decimals) : (Math.round(absNum / Math.pow(10, -decimals)) * Math.pow(10, -decimals)).toFixed(0);
4531
+ const [intPart, decPart] = rounded.split(".");
4532
+ const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
4533
+ const formatted = decPart !== void 0 ? `${withCommas}.${decPart}` : withCommas;
4534
+ return num < 0 ? `($${formatted})` : `$${formatted}`;
4535
+ }
4536
+ });
4537
+ registry.set("FIXED", {
4538
+ minArgs: 1,
4539
+ maxArgs: 3,
4540
+ evaluate(args, context, evaluator) {
4541
+ const rawNum = evaluator.evaluate(args[0], context);
4542
+ if (rawNum instanceof FormulaError) return rawNum;
4543
+ const num = toNumber(rawNum);
4544
+ if (num instanceof FormulaError) return num;
4545
+ let decimals = 2;
4546
+ if (args.length >= 2) {
4547
+ const rawDec = evaluator.evaluate(args[1], context);
4548
+ if (rawDec instanceof FormulaError) return rawDec;
4549
+ const d = toNumber(rawDec);
4550
+ if (d instanceof FormulaError) return d;
4551
+ decimals = Math.trunc(d);
4552
+ }
4553
+ let noCommas = false;
4554
+ if (args.length >= 3) {
4555
+ const rawNoCommas = evaluator.evaluate(args[2], context);
4556
+ if (rawNoCommas instanceof FormulaError) return rawNoCommas;
4557
+ noCommas = !!rawNoCommas;
4558
+ }
4559
+ const absNum = Math.abs(num);
4560
+ const rounded = decimals >= 0 ? absNum.toFixed(decimals) : (Math.round(absNum / Math.pow(10, -decimals)) * Math.pow(10, -decimals)).toFixed(0);
4561
+ if (noCommas) {
4562
+ return num < 0 ? `-${rounded}` : rounded;
4563
+ }
4564
+ const [intPart, decPart] = rounded.split(".");
4565
+ const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
4566
+ const formatted = decPart !== void 0 ? `${withCommas}.${decPart}` : withCommas;
4567
+ return num < 0 ? `-${formatted}` : formatted;
4568
+ }
4569
+ });
4570
+ registry.set("T", {
4571
+ minArgs: 1,
4572
+ maxArgs: 1,
4573
+ evaluate(args, context, evaluator) {
4574
+ const val = evaluator.evaluate(args[0], context);
4575
+ if (val instanceof FormulaError) return val;
4576
+ return typeof val === "string" ? val : "";
4577
+ }
4578
+ });
4579
+ registry.set("N", {
4580
+ minArgs: 1,
4581
+ maxArgs: 1,
4582
+ evaluate(args, context, evaluator) {
4583
+ const val = evaluator.evaluate(args[0], context);
4584
+ if (val instanceof FormulaError) return val;
4585
+ if (typeof val === "number") return val;
4586
+ if (typeof val === "boolean") return val ? 1 : 0;
4587
+ if (val instanceof Date) return val.getTime();
4588
+ return 0;
4589
+ }
4590
+ });
4591
+ registry.set("FORMULATEXT", {
4592
+ minArgs: 1,
4593
+ maxArgs: 1,
4594
+ evaluate(args, context, _evaluator) {
4595
+ const arg = args[0];
4596
+ if (arg.kind !== "cellRef") {
4597
+ return new FormulaError("#N/A", "FORMULATEXT requires a cell reference");
4598
+ }
4599
+ if (!context.getCellFormula) {
4600
+ return new FormulaError("#N/A", "FORMULATEXT not supported in this context");
4601
+ }
4602
+ const formula = context.getCellFormula(arg.address);
4603
+ if (formula === void 0) {
4604
+ return new FormulaError("#N/A", "Cell does not contain a formula");
4605
+ }
4606
+ return formula;
4607
+ }
4608
+ });
4609
+ registry.set("NUMBERVALUE", {
4610
+ minArgs: 1,
4611
+ maxArgs: 3,
4612
+ evaluate(args, context, evaluator) {
4613
+ const rawText = evaluator.evaluate(args[0], context);
4614
+ if (rawText instanceof FormulaError) return rawText;
4615
+ if (typeof rawText === "number") return rawText;
4616
+ let text = toString(rawText).trim();
4617
+ let decimalSep = ".";
4618
+ let groupSep = ",";
4619
+ const hasDecimalArg = args.length >= 2;
4620
+ const hasGroupArg = args.length >= 3;
4621
+ if (hasDecimalArg) {
4622
+ const rawDec = evaluator.evaluate(args[1], context);
4623
+ if (rawDec instanceof FormulaError) return rawDec;
4624
+ decimalSep = toString(rawDec);
4625
+ if (decimalSep.length !== 1) return new FormulaError("#VALUE!", "NUMBERVALUE decimal separator must be 1 character");
4626
+ if (!hasGroupArg) {
4627
+ groupSep = decimalSep === "," ? "." : ",";
4628
+ }
4629
+ }
4630
+ if (hasGroupArg) {
4631
+ const rawGrp = evaluator.evaluate(args[2], context);
4632
+ if (rawGrp instanceof FormulaError) return rawGrp;
4633
+ groupSep = toString(rawGrp);
4634
+ if (groupSep.length !== 1) return new FormulaError("#VALUE!", "NUMBERVALUE group separator must be 1 character");
4635
+ }
4636
+ if (decimalSep === groupSep) return new FormulaError("#VALUE!", "NUMBERVALUE separators must be different");
4637
+ const isPercent = text.endsWith("%");
4638
+ if (isPercent) text = text.slice(0, -1).trim();
4639
+ const escapedGroup = groupSep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4640
+ text = text.replace(new RegExp(escapedGroup, "g"), "");
4641
+ if (decimalSep !== ".") {
4642
+ const escapedDec = decimalSep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4643
+ text = text.replace(new RegExp(escapedDec), ".");
4644
+ }
4645
+ const n = Number(text);
4646
+ if (isNaN(n)) return new FormulaError("#VALUE!", `NUMBERVALUE cannot parse "${toString(rawText)}"`);
4647
+ return isPercent ? n / 100 : n;
4648
+ }
4649
+ });
4650
+ registry.set("PHONETIC", {
4651
+ minArgs: 1,
4652
+ maxArgs: 1,
4653
+ evaluate(args, context, evaluator) {
4654
+ const val = evaluator.evaluate(args[0], context);
4655
+ if (val instanceof FormulaError) return val;
4656
+ return toString(val);
4657
+ }
4658
+ });
4659
+ }
4660
+ function toDate(val) {
4661
+ if (val instanceof FormulaError) return val;
4662
+ if (val instanceof Date) {
4663
+ if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
4664
+ return val;
4665
+ }
4666
+ if (typeof val === "string") {
4667
+ const d = new Date(val);
4668
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
4669
+ return d;
4670
+ }
4671
+ if (typeof val === "number") {
4672
+ const d = new Date(val);
4673
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
4674
+ return d;
4675
+ }
4676
+ return new FormulaError("#VALUE!", "Cannot convert value to date");
4677
+ }
4678
+ function registerDateFunctions(registry) {
4679
+ registry.set("TODAY", {
4680
+ minArgs: 0,
4681
+ maxArgs: 0,
4682
+ evaluate(_args, context) {
4683
+ const now = context.now();
4684
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
4685
+ }
4686
+ });
4687
+ registry.set("NOW", {
4688
+ minArgs: 0,
4689
+ maxArgs: 0,
4690
+ evaluate(_args, context) {
4691
+ return context.now();
4692
+ }
4693
+ });
4694
+ registry.set("YEAR", {
4695
+ minArgs: 1,
4696
+ maxArgs: 1,
4697
+ evaluate(args, context, evaluator) {
4698
+ const val = evaluator.evaluate(args[0], context);
4699
+ if (val instanceof FormulaError) return val;
4700
+ const date = toDate(val);
4247
4701
  if (date instanceof FormulaError) return date;
4248
4702
  return date.getFullYear();
4249
4703
  }
@@ -4449,6 +4903,311 @@ function registerDateFunctions(registry) {
4449
4903
  return count * sign;
4450
4904
  }
4451
4905
  });
4906
+ registry.set("DAYS", {
4907
+ minArgs: 2,
4908
+ maxArgs: 2,
4909
+ evaluate(args, context, evaluator) {
4910
+ const rawEnd = evaluator.evaluate(args[0], context);
4911
+ if (rawEnd instanceof FormulaError) return rawEnd;
4912
+ const endDate = toDate(rawEnd);
4913
+ if (endDate instanceof FormulaError) return endDate;
4914
+ const rawStart = evaluator.evaluate(args[1], context);
4915
+ if (rawStart instanceof FormulaError) return rawStart;
4916
+ const startDate = toDate(rawStart);
4917
+ if (startDate instanceof FormulaError) return startDate;
4918
+ const endMs = Date.UTC(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
4919
+ const startMs = Date.UTC(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
4920
+ return Math.round((endMs - startMs) / 864e5);
4921
+ }
4922
+ });
4923
+ registry.set("DAYS360", {
4924
+ minArgs: 2,
4925
+ maxArgs: 3,
4926
+ evaluate(args, context, evaluator) {
4927
+ const rawStart = evaluator.evaluate(args[0], context);
4928
+ if (rawStart instanceof FormulaError) return rawStart;
4929
+ const startDate = toDate(rawStart);
4930
+ if (startDate instanceof FormulaError) return startDate;
4931
+ const rawEnd = evaluator.evaluate(args[1], context);
4932
+ if (rawEnd instanceof FormulaError) return rawEnd;
4933
+ const endDate = toDate(rawEnd);
4934
+ if (endDate instanceof FormulaError) return endDate;
4935
+ let method = false;
4936
+ if (args.length >= 3) {
4937
+ const rawMethod = evaluator.evaluate(args[2], context);
4938
+ if (rawMethod instanceof FormulaError) return rawMethod;
4939
+ method = !!rawMethod;
4940
+ }
4941
+ const sm = startDate.getMonth() + 1;
4942
+ const em = endDate.getMonth() + 1;
4943
+ let sd = startDate.getDate();
4944
+ let ed = endDate.getDate();
4945
+ const sy = startDate.getFullYear();
4946
+ const ey = endDate.getFullYear();
4947
+ if (!method) {
4948
+ if (sd === 31) sd = 30;
4949
+ if (ed === 31 && sd === 30) ed = 30;
4950
+ } else {
4951
+ if (sd === 31) sd = 30;
4952
+ if (ed === 31) ed = 30;
4953
+ }
4954
+ return (ey - sy) * 360 + (em - sm) * 30 + (ed - sd);
4955
+ }
4956
+ });
4957
+ registry.set("ISOWEEKNUM", {
4958
+ minArgs: 1,
4959
+ maxArgs: 1,
4960
+ evaluate(args, context, evaluator) {
4961
+ const rawDate = evaluator.evaluate(args[0], context);
4962
+ if (rawDate instanceof FormulaError) return rawDate;
4963
+ const date = toDate(rawDate);
4964
+ if (date instanceof FormulaError) return date;
4965
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
4966
+ const day = d.getUTCDay() || 7;
4967
+ d.setUTCDate(d.getUTCDate() + 4 - day);
4968
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
4969
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
4970
+ }
4971
+ });
4972
+ registry.set("YEARFRAC", {
4973
+ minArgs: 2,
4974
+ maxArgs: 3,
4975
+ evaluate(args, context, evaluator) {
4976
+ const rawStart = evaluator.evaluate(args[0], context);
4977
+ if (rawStart instanceof FormulaError) return rawStart;
4978
+ const startDate = toDate(rawStart);
4979
+ if (startDate instanceof FormulaError) return startDate;
4980
+ const rawEnd = evaluator.evaluate(args[1], context);
4981
+ if (rawEnd instanceof FormulaError) return rawEnd;
4982
+ const endDate = toDate(rawEnd);
4983
+ if (endDate instanceof FormulaError) return endDate;
4984
+ let basis = 0;
4985
+ if (args.length >= 3) {
4986
+ const rawBasis = evaluator.evaluate(args[2], context);
4987
+ if (rawBasis instanceof FormulaError) return rawBasis;
4988
+ const b = toNumber(rawBasis);
4989
+ if (b instanceof FormulaError) return b;
4990
+ basis = Math.trunc(b);
4991
+ }
4992
+ const sy = startDate.getFullYear();
4993
+ const sm = startDate.getMonth() + 1;
4994
+ const sd = startDate.getDate();
4995
+ const ey = endDate.getFullYear();
4996
+ const em = endDate.getMonth() + 1;
4997
+ const ed = endDate.getDate();
4998
+ switch (basis) {
4999
+ case 0: {
5000
+ const startDay = sd === 31 ? 30 : sd;
5001
+ const endDay = ed === 31 && startDay === 30 ? 30 : ed;
5002
+ const days360 = (ey - sy) * 360 + (em - sm) * 30 + (endDay - startDay);
5003
+ return days360 / 360;
5004
+ }
5005
+ case 1: {
5006
+ const diffMs = Date.UTC(ey, em - 1, ed) - Date.UTC(sy, sm - 1, sd);
5007
+ const diffDays = diffMs / 864e5;
5008
+ const avgYear = ey === sy ? isLeapYear(sy) ? 366 : 365 : (Date.UTC(ey + 1, 0, 1) - Date.UTC(sy, 0, 1)) / 864e5 / (ey - sy + 1);
5009
+ return diffDays / avgYear;
5010
+ }
5011
+ case 3: {
5012
+ const diffMs = Date.UTC(ey, em - 1, ed) - Date.UTC(sy, sm - 1, sd);
5013
+ return diffMs / 864e5 / 365;
5014
+ }
5015
+ default:
5016
+ return new FormulaError("#VALUE!", "YEARFRAC basis must be 0, 1, or 3");
5017
+ }
5018
+ }
5019
+ });
5020
+ registry.set("DATEVALUE", {
5021
+ minArgs: 1,
5022
+ maxArgs: 1,
5023
+ evaluate(args, context, evaluator) {
5024
+ const rawVal = evaluator.evaluate(args[0], context);
5025
+ if (rawVal instanceof FormulaError) return rawVal;
5026
+ const str = typeof rawVal === "string" ? rawVal : String(rawVal);
5027
+ const d = new Date(str);
5028
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `DATEVALUE cannot parse "${str}"`);
5029
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
5030
+ }
5031
+ });
5032
+ registry.set("TIMEVALUE", {
5033
+ minArgs: 1,
5034
+ maxArgs: 1,
5035
+ evaluate(args, context, evaluator) {
5036
+ const rawVal = evaluator.evaluate(args[0], context);
5037
+ if (rawVal instanceof FormulaError) return rawVal;
5038
+ const str = typeof rawVal === "string" ? rawVal : String(rawVal);
5039
+ const match = str.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*(AM|PM))?$/i);
5040
+ if (!match) return new FormulaError("#VALUE!", `TIMEVALUE cannot parse "${str}"`);
5041
+ let hours = parseInt(match[1], 10);
5042
+ const minutes = parseInt(match[2], 10);
5043
+ const seconds = match[3] ? parseInt(match[3], 10) : 0;
5044
+ const ampm = match[4] ? match[4].toUpperCase() : null;
5045
+ if (ampm === "PM" && hours < 12) hours += 12;
5046
+ if (ampm === "AM" && hours === 12) hours = 0;
5047
+ if (hours > 23 || minutes > 59 || seconds > 59) {
5048
+ return new FormulaError("#VALUE!", "TIMEVALUE: invalid time component");
5049
+ }
5050
+ return (hours * 3600 + minutes * 60 + seconds) / 86400;
5051
+ }
5052
+ });
5053
+ registry.set("TIME", {
5054
+ minArgs: 3,
5055
+ maxArgs: 3,
5056
+ evaluate(args, context, evaluator) {
5057
+ const rawH = evaluator.evaluate(args[0], context);
5058
+ if (rawH instanceof FormulaError) return rawH;
5059
+ const h = toNumber(rawH);
5060
+ if (h instanceof FormulaError) return h;
5061
+ const rawM = evaluator.evaluate(args[1], context);
5062
+ if (rawM instanceof FormulaError) return rawM;
5063
+ const m = toNumber(rawM);
5064
+ if (m instanceof FormulaError) return m;
5065
+ const rawS = evaluator.evaluate(args[2], context);
5066
+ if (rawS instanceof FormulaError) return rawS;
5067
+ const s = toNumber(rawS);
5068
+ if (s instanceof FormulaError) return s;
5069
+ const totalSeconds = Math.trunc(h) * 3600 + Math.trunc(m) * 60 + Math.trunc(s);
5070
+ return totalSeconds % 86400 / 86400;
5071
+ }
5072
+ });
5073
+ registry.set("WORKDAY", {
5074
+ minArgs: 2,
5075
+ maxArgs: 3,
5076
+ evaluate(args, context, evaluator) {
5077
+ const rawStart = evaluator.evaluate(args[0], context);
5078
+ if (rawStart instanceof FormulaError) return rawStart;
5079
+ const startDate = toDate(rawStart);
5080
+ if (startDate instanceof FormulaError) return startDate;
5081
+ const rawDays = evaluator.evaluate(args[1], context);
5082
+ if (rawDays instanceof FormulaError) return rawDays;
5083
+ const daysNum = toNumber(rawDays);
5084
+ if (daysNum instanceof FormulaError) return daysNum;
5085
+ const days = Math.trunc(daysNum);
5086
+ const holidaySet = /* @__PURE__ */ new Set();
5087
+ if (args.length >= 3) {
5088
+ const rawHol = args[2];
5089
+ let holVals;
5090
+ if (rawHol.kind === "range") {
5091
+ holVals = context.getRangeValues({ start: rawHol.start, end: rawHol.end }).flat();
5092
+ } else {
5093
+ holVals = [evaluator.evaluate(rawHol, context)];
5094
+ }
5095
+ for (const hv of holVals) {
5096
+ if (hv === null || hv === void 0) continue;
5097
+ const hd = toDate(hv);
5098
+ if (!(hd instanceof FormulaError)) {
5099
+ holidaySet.add(`${hd.getFullYear()}-${hd.getMonth()}-${hd.getDate()}`);
5100
+ }
5101
+ }
5102
+ }
5103
+ const current = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
5104
+ const step = days >= 0 ? 1 : -1;
5105
+ let remaining = Math.abs(days);
5106
+ while (remaining > 0) {
5107
+ current.setDate(current.getDate() + step);
5108
+ const dow = current.getDay();
5109
+ if (dow === 0 || dow === 6) continue;
5110
+ const key = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}`;
5111
+ if (holidaySet.has(key)) continue;
5112
+ remaining--;
5113
+ }
5114
+ return current;
5115
+ }
5116
+ });
5117
+ registry.set("WORKDAY.INTL", {
5118
+ minArgs: 2,
5119
+ maxArgs: 4,
5120
+ evaluate(args, context, evaluator) {
5121
+ const rawStart = evaluator.evaluate(args[0], context);
5122
+ if (rawStart instanceof FormulaError) return rawStart;
5123
+ const startDate = toDate(rawStart);
5124
+ if (startDate instanceof FormulaError) return startDate;
5125
+ const rawDays = evaluator.evaluate(args[1], context);
5126
+ if (rawDays instanceof FormulaError) return rawDays;
5127
+ const daysNum = toNumber(rawDays);
5128
+ if (daysNum instanceof FormulaError) return daysNum;
5129
+ const days = Math.trunc(daysNum);
5130
+ let weekendMask = [false, false, false, false, false, true, true];
5131
+ if (args.length >= 3) {
5132
+ const rawWeekend = evaluator.evaluate(args[2], context);
5133
+ if (rawWeekend instanceof FormulaError) return rawWeekend;
5134
+ if (typeof rawWeekend === "string" && /^[01]{7}$/.test(rawWeekend)) {
5135
+ weekendMask = rawWeekend.split("").map((c) => c === "1");
5136
+ } else {
5137
+ const wn = toNumber(rawWeekend);
5138
+ if (wn instanceof FormulaError) return wn;
5139
+ const parsed = parseWeekendNumber(Math.trunc(wn));
5140
+ if (!parsed) return new FormulaError("#VALUE!", "WORKDAY.INTL invalid weekend number");
5141
+ weekendMask = parsed;
5142
+ }
5143
+ }
5144
+ const holidaySet = /* @__PURE__ */ new Set();
5145
+ if (args.length >= 4) {
5146
+ const rawHol = args[3];
5147
+ let holVals;
5148
+ if (rawHol.kind === "range") {
5149
+ holVals = context.getRangeValues({ start: rawHol.start, end: rawHol.end }).flat();
5150
+ } else {
5151
+ holVals = [evaluator.evaluate(rawHol, context)];
5152
+ }
5153
+ for (const hv of holVals) {
5154
+ if (hv === null || hv === void 0) continue;
5155
+ const hd = toDate(hv);
5156
+ if (!(hd instanceof FormulaError)) {
5157
+ holidaySet.add(`${hd.getFullYear()}-${hd.getMonth()}-${hd.getDate()}`);
5158
+ }
5159
+ }
5160
+ }
5161
+ const current = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
5162
+ const step = days >= 0 ? 1 : -1;
5163
+ let remaining = Math.abs(days);
5164
+ while (remaining > 0) {
5165
+ current.setDate(current.getDate() + step);
5166
+ const dow = current.getDay();
5167
+ const maskIndex = dow === 0 ? 6 : dow - 1;
5168
+ if (weekendMask[maskIndex]) continue;
5169
+ const key = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}`;
5170
+ if (holidaySet.has(key)) continue;
5171
+ remaining--;
5172
+ }
5173
+ return current;
5174
+ }
5175
+ });
5176
+ }
5177
+ function isLeapYear(year) {
5178
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
5179
+ }
5180
+ function parseWeekendNumber(n) {
5181
+ const twoDay = [
5182
+ [5, 6],
5183
+ // 1: Sat+Sun
5184
+ [6, 0],
5185
+ // 2: Sun+Mon (Sun=index6, Mon=index0)
5186
+ [0, 1],
5187
+ // 3: Mon+Tue
5188
+ [1, 2],
5189
+ // 4: Tue+Wed
5190
+ [2, 3],
5191
+ // 5: Wed+Thu
5192
+ [3, 4],
5193
+ // 6: Thu+Fri
5194
+ [4, 5]
5195
+ // 7: Fri+Sat
5196
+ ];
5197
+ if (n >= 1 && n <= 7) {
5198
+ const mask = [false, false, false, false, false, false, false];
5199
+ const [a, b] = twoDay[n - 1];
5200
+ mask[a] = true;
5201
+ mask[b] = true;
5202
+ return mask;
5203
+ }
5204
+ if (n >= 11 && n <= 17) {
5205
+ const mask = [false, false, false, false, false, false, false];
5206
+ const singleDay = [6, 0, 1, 2, 3, 4, 5];
5207
+ mask[singleDay[n - 11]] = true;
5208
+ return mask;
5209
+ }
5210
+ return null;
4452
5211
  }
4453
5212
  function makeCriteria(op, value) {
4454
5213
  return { op, value, valueLower: typeof value === "string" ? value.toLowerCase() : null };
@@ -4779,36 +5538,1076 @@ function registerInfoFunctions(registry) {
4779
5538
  return typeof val === "string";
4780
5539
  }
4781
5540
  });
4782
- registry.set("ISERROR", {
5541
+ registry.set("ISERROR", {
5542
+ minArgs: 1,
5543
+ maxArgs: 1,
5544
+ evaluate(args, context, evaluator) {
5545
+ const val = evaluator.evaluate(args[0], context);
5546
+ return val instanceof FormulaError;
5547
+ }
5548
+ });
5549
+ registry.set("ISNA", {
5550
+ minArgs: 1,
5551
+ maxArgs: 1,
5552
+ evaluate(args, context, evaluator) {
5553
+ const val = evaluator.evaluate(args[0], context);
5554
+ return val instanceof FormulaError && val.type === "#N/A";
5555
+ }
5556
+ });
5557
+ registry.set("TYPE", {
5558
+ minArgs: 1,
5559
+ maxArgs: 1,
5560
+ evaluate(args, context, evaluator) {
5561
+ const val = evaluator.evaluate(args[0], context);
5562
+ if (val instanceof FormulaError) return 16;
5563
+ if (typeof val === "number") return 1;
5564
+ if (typeof val === "string") return 2;
5565
+ if (typeof val === "boolean") return 4;
5566
+ if (val === null || val === void 0) return 1;
5567
+ return 1;
5568
+ }
5569
+ });
5570
+ registry.set("ISODD", {
5571
+ minArgs: 1,
5572
+ maxArgs: 1,
5573
+ evaluate(args, context, evaluator) {
5574
+ const val = evaluator.evaluate(args[0], context);
5575
+ if (val instanceof FormulaError) return val;
5576
+ if (typeof val === "boolean") return new FormulaError("#VALUE!", "ISODD requires a number");
5577
+ const n = toNumber(val);
5578
+ if (n instanceof FormulaError) return n;
5579
+ return Math.trunc(n) % 2 !== 0;
5580
+ }
5581
+ });
5582
+ registry.set("ISEVEN", {
5583
+ minArgs: 1,
5584
+ maxArgs: 1,
5585
+ evaluate(args, context, evaluator) {
5586
+ const val = evaluator.evaluate(args[0], context);
5587
+ if (val instanceof FormulaError) return val;
5588
+ if (typeof val === "boolean") return new FormulaError("#VALUE!", "ISEVEN requires a number");
5589
+ const n = toNumber(val);
5590
+ if (n instanceof FormulaError) return n;
5591
+ return Math.trunc(n) % 2 === 0;
5592
+ }
5593
+ });
5594
+ registry.set("ISFORMULA", {
5595
+ minArgs: 1,
5596
+ maxArgs: 1,
5597
+ evaluate(args, context, _evaluator) {
5598
+ const arg = args[0];
5599
+ if (arg.kind !== "cellRef") return false;
5600
+ if (!context.getCellFormula) return false;
5601
+ const formula = context.getCellFormula(arg.address);
5602
+ return formula !== void 0;
5603
+ }
5604
+ });
5605
+ registry.set("ISLOGICAL", {
5606
+ minArgs: 1,
5607
+ maxArgs: 1,
5608
+ evaluate(args, context, evaluator) {
5609
+ const val = evaluator.evaluate(args[0], context);
5610
+ if (val instanceof FormulaError) return false;
5611
+ return typeof val === "boolean";
5612
+ }
5613
+ });
5614
+ registry.set("ISNONTEXT", {
5615
+ minArgs: 1,
5616
+ maxArgs: 1,
5617
+ evaluate(args, context, evaluator) {
5618
+ const val = evaluator.evaluate(args[0], context);
5619
+ if (val instanceof FormulaError) return true;
5620
+ return typeof val !== "string";
5621
+ }
5622
+ });
5623
+ registry.set("ISREF", {
5624
+ minArgs: 1,
5625
+ maxArgs: 1,
5626
+ evaluate(args, _context, _evaluator) {
5627
+ const arg = args[0];
5628
+ return arg.kind === "cellRef" || arg.kind === "range";
5629
+ }
5630
+ });
5631
+ }
5632
+ function registerFinancialFunctions(registry) {
5633
+ registry.set("PMT", {
5634
+ minArgs: 3,
5635
+ maxArgs: 5,
5636
+ evaluate(args, context, evaluator) {
5637
+ const rawRate = evaluator.evaluate(args[0], context);
5638
+ if (rawRate instanceof FormulaError) return rawRate;
5639
+ const rate = toNumber(rawRate);
5640
+ if (rate instanceof FormulaError) return rate;
5641
+ const rawNper = evaluator.evaluate(args[1], context);
5642
+ if (rawNper instanceof FormulaError) return rawNper;
5643
+ const nper = toNumber(rawNper);
5644
+ if (nper instanceof FormulaError) return nper;
5645
+ const rawPv = evaluator.evaluate(args[2], context);
5646
+ if (rawPv instanceof FormulaError) return rawPv;
5647
+ const pv = toNumber(rawPv);
5648
+ if (pv instanceof FormulaError) return pv;
5649
+ let fv = 0;
5650
+ if (args.length >= 4) {
5651
+ const rawFv = evaluator.evaluate(args[3], context);
5652
+ if (rawFv instanceof FormulaError) return rawFv;
5653
+ const fvNum = toNumber(rawFv);
5654
+ if (fvNum instanceof FormulaError) return fvNum;
5655
+ fv = fvNum;
5656
+ }
5657
+ let type = 0;
5658
+ if (args.length >= 5) {
5659
+ const rawType = evaluator.evaluate(args[4], context);
5660
+ if (rawType instanceof FormulaError) return rawType;
5661
+ const typeNum = toNumber(rawType);
5662
+ if (typeNum instanceof FormulaError) return typeNum;
5663
+ type = typeNum;
5664
+ }
5665
+ if (nper === 0) return new FormulaError("#NUM!", "PMT: nper cannot be 0");
5666
+ if (rate === 0) {
5667
+ return -(pv + fv) / nper;
5668
+ }
5669
+ const factor = Math.pow(1 + rate, nper);
5670
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5671
+ return -(pv * factor + fv) / ((factor - 1) / rate * typeAdj);
5672
+ }
5673
+ });
5674
+ registry.set("FV", {
5675
+ minArgs: 3,
5676
+ maxArgs: 5,
5677
+ evaluate(args, context, evaluator) {
5678
+ const rawRate = evaluator.evaluate(args[0], context);
5679
+ if (rawRate instanceof FormulaError) return rawRate;
5680
+ const rate = toNumber(rawRate);
5681
+ if (rate instanceof FormulaError) return rate;
5682
+ const rawNper = evaluator.evaluate(args[1], context);
5683
+ if (rawNper instanceof FormulaError) return rawNper;
5684
+ const nper = toNumber(rawNper);
5685
+ if (nper instanceof FormulaError) return nper;
5686
+ const rawPmt = evaluator.evaluate(args[2], context);
5687
+ if (rawPmt instanceof FormulaError) return rawPmt;
5688
+ const pmt = toNumber(rawPmt);
5689
+ if (pmt instanceof FormulaError) return pmt;
5690
+ let pv = 0;
5691
+ if (args.length >= 4) {
5692
+ const rawPv = evaluator.evaluate(args[3], context);
5693
+ if (rawPv instanceof FormulaError) return rawPv;
5694
+ const pvNum = toNumber(rawPv);
5695
+ if (pvNum instanceof FormulaError) return pvNum;
5696
+ pv = pvNum;
5697
+ }
5698
+ let type = 0;
5699
+ if (args.length >= 5) {
5700
+ const rawType = evaluator.evaluate(args[4], context);
5701
+ if (rawType instanceof FormulaError) return rawType;
5702
+ const typeNum = toNumber(rawType);
5703
+ if (typeNum instanceof FormulaError) return typeNum;
5704
+ type = typeNum;
5705
+ }
5706
+ if (rate === 0) {
5707
+ return -(pv + pmt * nper);
5708
+ }
5709
+ const factor = Math.pow(1 + rate, nper);
5710
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5711
+ return -(pv * factor + pmt * typeAdj * (factor - 1) / rate);
5712
+ }
5713
+ });
5714
+ registry.set("PV", {
5715
+ minArgs: 3,
5716
+ maxArgs: 5,
5717
+ evaluate(args, context, evaluator) {
5718
+ const rawRate = evaluator.evaluate(args[0], context);
5719
+ if (rawRate instanceof FormulaError) return rawRate;
5720
+ const rate = toNumber(rawRate);
5721
+ if (rate instanceof FormulaError) return rate;
5722
+ const rawNper = evaluator.evaluate(args[1], context);
5723
+ if (rawNper instanceof FormulaError) return rawNper;
5724
+ const nper = toNumber(rawNper);
5725
+ if (nper instanceof FormulaError) return nper;
5726
+ const rawPmt = evaluator.evaluate(args[2], context);
5727
+ if (rawPmt instanceof FormulaError) return rawPmt;
5728
+ const pmt = toNumber(rawPmt);
5729
+ if (pmt instanceof FormulaError) return pmt;
5730
+ let fv = 0;
5731
+ if (args.length >= 4) {
5732
+ const rawFv = evaluator.evaluate(args[3], context);
5733
+ if (rawFv instanceof FormulaError) return rawFv;
5734
+ const fvNum = toNumber(rawFv);
5735
+ if (fvNum instanceof FormulaError) return fvNum;
5736
+ fv = fvNum;
5737
+ }
5738
+ let type = 0;
5739
+ if (args.length >= 5) {
5740
+ const rawType = evaluator.evaluate(args[4], context);
5741
+ if (rawType instanceof FormulaError) return rawType;
5742
+ const typeNum = toNumber(rawType);
5743
+ if (typeNum instanceof FormulaError) return typeNum;
5744
+ type = typeNum;
5745
+ }
5746
+ if (rate === 0) {
5747
+ return -pmt * nper - fv;
5748
+ }
5749
+ const factor = Math.pow(1 + rate, nper);
5750
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5751
+ return -(fv + pmt * typeAdj * (factor - 1) / rate) / factor;
5752
+ }
5753
+ });
5754
+ registry.set("NPER", {
5755
+ minArgs: 3,
5756
+ maxArgs: 5,
5757
+ evaluate(args, context, evaluator) {
5758
+ const rawRate = evaluator.evaluate(args[0], context);
5759
+ if (rawRate instanceof FormulaError) return rawRate;
5760
+ const rate = toNumber(rawRate);
5761
+ if (rate instanceof FormulaError) return rate;
5762
+ const rawPmt = evaluator.evaluate(args[1], context);
5763
+ if (rawPmt instanceof FormulaError) return rawPmt;
5764
+ const pmt = toNumber(rawPmt);
5765
+ if (pmt instanceof FormulaError) return pmt;
5766
+ const rawPv = evaluator.evaluate(args[2], context);
5767
+ if (rawPv instanceof FormulaError) return rawPv;
5768
+ const pv = toNumber(rawPv);
5769
+ if (pv instanceof FormulaError) return pv;
5770
+ let fv = 0;
5771
+ if (args.length >= 4) {
5772
+ const rawFv = evaluator.evaluate(args[3], context);
5773
+ if (rawFv instanceof FormulaError) return rawFv;
5774
+ const fvNum = toNumber(rawFv);
5775
+ if (fvNum instanceof FormulaError) return fvNum;
5776
+ fv = fvNum;
5777
+ }
5778
+ let type = 0;
5779
+ if (args.length >= 5) {
5780
+ const rawType = evaluator.evaluate(args[4], context);
5781
+ if (rawType instanceof FormulaError) return rawType;
5782
+ const typeNum = toNumber(rawType);
5783
+ if (typeNum instanceof FormulaError) return typeNum;
5784
+ type = typeNum;
5785
+ }
5786
+ if (rate === 0) {
5787
+ if (pmt === 0) return new FormulaError("#NUM!", "NPER: pmt cannot be 0 when rate is 0");
5788
+ return -(pv + fv) / pmt;
5789
+ }
5790
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5791
+ const pmtAdj = pmt * typeAdj;
5792
+ const num = pmtAdj - fv * rate;
5793
+ const den = pmtAdj + pv * rate;
5794
+ if (den === 0) return new FormulaError("#DIV/0!", "NPER: division by zero");
5795
+ if (num / den <= 0) return new FormulaError("#NUM!", "NPER: logarithm of non-positive number");
5796
+ return Math.log(num / den) / Math.log(1 + rate);
5797
+ }
5798
+ });
5799
+ registry.set("RATE", {
5800
+ minArgs: 3,
5801
+ maxArgs: 6,
5802
+ evaluate(args, context, evaluator) {
5803
+ const rawNper = evaluator.evaluate(args[0], context);
5804
+ if (rawNper instanceof FormulaError) return rawNper;
5805
+ const nper = toNumber(rawNper);
5806
+ if (nper instanceof FormulaError) return nper;
5807
+ const rawPmt = evaluator.evaluate(args[1], context);
5808
+ if (rawPmt instanceof FormulaError) return rawPmt;
5809
+ const pmt = toNumber(rawPmt);
5810
+ if (pmt instanceof FormulaError) return pmt;
5811
+ const rawPv = evaluator.evaluate(args[2], context);
5812
+ if (rawPv instanceof FormulaError) return rawPv;
5813
+ const pv = toNumber(rawPv);
5814
+ if (pv instanceof FormulaError) return pv;
5815
+ let fv = 0;
5816
+ if (args.length >= 4) {
5817
+ const rawFv = evaluator.evaluate(args[3], context);
5818
+ if (rawFv instanceof FormulaError) return rawFv;
5819
+ const fvNum = toNumber(rawFv);
5820
+ if (fvNum instanceof FormulaError) return fvNum;
5821
+ fv = fvNum;
5822
+ }
5823
+ let type = 0;
5824
+ if (args.length >= 5) {
5825
+ const rawType = evaluator.evaluate(args[4], context);
5826
+ if (rawType instanceof FormulaError) return rawType;
5827
+ const typeNum = toNumber(rawType);
5828
+ if (typeNum instanceof FormulaError) return typeNum;
5829
+ type = typeNum;
5830
+ }
5831
+ let guess = 0.1;
5832
+ if (args.length >= 6) {
5833
+ const rawGuess = evaluator.evaluate(args[5], context);
5834
+ if (rawGuess instanceof FormulaError) return rawGuess;
5835
+ const guessNum = toNumber(rawGuess);
5836
+ if (guessNum instanceof FormulaError) return guessNum;
5837
+ guess = guessNum;
5838
+ }
5839
+ const MAX_ITER = 20;
5840
+ const TOLERANCE = 1e-7;
5841
+ let r = guess;
5842
+ for (let i = 0; i < MAX_ITER; i++) {
5843
+ if (r <= -1) return new FormulaError("#NUM!", "RATE: rate converged to invalid value");
5844
+ const factor = Math.pow(1 + r, nper);
5845
+ const typeAdj = type !== 0 ? 1 + r : 1;
5846
+ let f;
5847
+ let df;
5848
+ if (Math.abs(r) < 1e-10) {
5849
+ f = pv + pmt * nper + fv;
5850
+ df = pv * nper + pmt * nper * (nper - 1) / 2;
5851
+ } else {
5852
+ f = pv * factor + pmt * typeAdj * (factor - 1) / r + fv;
5853
+ const dfactor = nper * Math.pow(1 + r, nper - 1);
5854
+ const dTypeAdj = type !== 0 ? 1 : 0;
5855
+ df = pv * dfactor + pmt * (dTypeAdj * (factor - 1) / r + typeAdj * (dfactor * r - (factor - 1)) / (r * r));
5856
+ }
5857
+ if (Math.abs(df) < 1e-15) return new FormulaError("#NUM!", "RATE: derivative too small, no convergence");
5858
+ const delta = f / df;
5859
+ r = r - delta;
5860
+ if (Math.abs(delta) < TOLERANCE) return r;
5861
+ }
5862
+ return new FormulaError("#NUM!", "RATE: did not converge");
5863
+ }
5864
+ });
5865
+ registry.set("NPV", {
5866
+ minArgs: 2,
5867
+ maxArgs: -1,
5868
+ evaluate(args, context, evaluator) {
5869
+ const rawRate = evaluator.evaluate(args[0], context);
5870
+ if (rawRate instanceof FormulaError) return rawRate;
5871
+ const rate = toNumber(rawRate);
5872
+ if (rate instanceof FormulaError) return rate;
5873
+ const values = flattenArgs(args.slice(1), context, evaluator);
5874
+ let npv = 0;
5875
+ let period = 1;
5876
+ for (const val of values) {
5877
+ if (val instanceof FormulaError) return val;
5878
+ if (typeof val === "number" || typeof val === "boolean") {
5879
+ const n = typeof val === "boolean" ? val ? 1 : 0 : val;
5880
+ npv += n / Math.pow(1 + rate, period);
5881
+ period++;
5882
+ }
5883
+ }
5884
+ return npv;
5885
+ }
5886
+ });
5887
+ registry.set("IRR", {
5888
+ minArgs: 1,
5889
+ maxArgs: 2,
5890
+ evaluate(args, context, evaluator) {
5891
+ const cashFlows = flattenArgs([args[0]], context, evaluator);
5892
+ const nums = [];
5893
+ for (const val of cashFlows) {
5894
+ if (val instanceof FormulaError) return val;
5895
+ if (typeof val === "number") nums.push(val);
5896
+ else if (typeof val === "boolean") nums.push(val ? 1 : 0);
5897
+ }
5898
+ if (nums.length === 0) return new FormulaError("#NUM!", "IRR: no values");
5899
+ let guess = 0.1;
5900
+ if (args.length >= 2) {
5901
+ const rawGuess = evaluator.evaluate(args[1], context);
5902
+ if (rawGuess instanceof FormulaError) return rawGuess;
5903
+ const guessNum = toNumber(rawGuess);
5904
+ if (guessNum instanceof FormulaError) return guessNum;
5905
+ guess = guessNum;
5906
+ }
5907
+ const MAX_ITER = 20;
5908
+ const TOLERANCE = 1e-7;
5909
+ let r = guess;
5910
+ for (let i = 0; i < MAX_ITER; i++) {
5911
+ if (r <= -1) return new FormulaError("#NUM!", "IRR: rate converged below -1");
5912
+ let f = 0;
5913
+ let df = 0;
5914
+ for (let j = 0; j < nums.length; j++) {
5915
+ const factor = Math.pow(1 + r, j);
5916
+ f += nums[j] / factor;
5917
+ if (j > 0) {
5918
+ df -= j * nums[j] / Math.pow(1 + r, j + 1);
5919
+ }
5920
+ }
5921
+ if (Math.abs(df) < 1e-15) return new FormulaError("#NUM!", "IRR: derivative too small");
5922
+ const delta = f / df;
5923
+ r = r - delta;
5924
+ if (Math.abs(delta) < TOLERANCE) return r;
5925
+ }
5926
+ return new FormulaError("#NUM!", "IRR: did not converge");
5927
+ }
5928
+ });
5929
+ registry.set("SLN", {
5930
+ minArgs: 3,
5931
+ maxArgs: 3,
5932
+ evaluate(args, context, evaluator) {
5933
+ const rawCost = evaluator.evaluate(args[0], context);
5934
+ if (rawCost instanceof FormulaError) return rawCost;
5935
+ const cost = toNumber(rawCost);
5936
+ if (cost instanceof FormulaError) return cost;
5937
+ const rawSalvage = evaluator.evaluate(args[1], context);
5938
+ if (rawSalvage instanceof FormulaError) return rawSalvage;
5939
+ const salvage = toNumber(rawSalvage);
5940
+ if (salvage instanceof FormulaError) return salvage;
5941
+ const rawLife = evaluator.evaluate(args[2], context);
5942
+ if (rawLife instanceof FormulaError) return rawLife;
5943
+ const life = toNumber(rawLife);
5944
+ if (life instanceof FormulaError) return life;
5945
+ if (life === 0) return new FormulaError("#DIV/0!", "SLN: life cannot be 0");
5946
+ return (cost - salvage) / life;
5947
+ }
5948
+ });
5949
+ }
5950
+ function extractNums(args, context, evaluator) {
5951
+ const values = flattenArgs(args, context, evaluator);
5952
+ const nums = [];
5953
+ for (const val of values) {
5954
+ if (val instanceof FormulaError) return val;
5955
+ if (typeof val === "number") nums.push(val);
5956
+ else if (typeof val === "boolean") nums.push(val ? 1 : 0);
5957
+ }
5958
+ return nums;
5959
+ }
5960
+ function mean(nums) {
5961
+ let sum = 0;
5962
+ for (const n of nums) sum += n;
5963
+ return sum / nums.length;
5964
+ }
5965
+ function registerStatisticalExtendedFunctions(registry) {
5966
+ const stdevImpl = {
5967
+ minArgs: 1,
5968
+ maxArgs: -1,
5969
+ evaluate(args, context, evaluator) {
5970
+ const nums = extractNums(args, context, evaluator);
5971
+ if (nums instanceof FormulaError) return nums;
5972
+ if (nums.length < 2) return new FormulaError("#DIV/0!", "STDEV requires at least 2 values");
5973
+ const m = mean(nums);
5974
+ let sum = 0;
5975
+ for (const n of nums) sum += (n - m) * (n - m);
5976
+ return Math.sqrt(sum / (nums.length - 1));
5977
+ }
5978
+ };
5979
+ registry.set("STDEV", stdevImpl);
5980
+ registry.set("STDEV.S", stdevImpl);
5981
+ const stdevpImpl = {
5982
+ minArgs: 1,
5983
+ maxArgs: -1,
5984
+ evaluate(args, context, evaluator) {
5985
+ const nums = extractNums(args, context, evaluator);
5986
+ if (nums instanceof FormulaError) return nums;
5987
+ if (nums.length === 0) return new FormulaError("#DIV/0!", "STDEVP requires at least 1 value");
5988
+ const m = mean(nums);
5989
+ let sum = 0;
5990
+ for (const n of nums) sum += (n - m) * (n - m);
5991
+ return Math.sqrt(sum / nums.length);
5992
+ }
5993
+ };
5994
+ registry.set("STDEVP", stdevpImpl);
5995
+ registry.set("STDEV.P", stdevpImpl);
5996
+ const varImpl = {
5997
+ minArgs: 1,
5998
+ maxArgs: -1,
5999
+ evaluate(args, context, evaluator) {
6000
+ const nums = extractNums(args, context, evaluator);
6001
+ if (nums instanceof FormulaError) return nums;
6002
+ if (nums.length < 2) return new FormulaError("#DIV/0!", "VAR requires at least 2 values");
6003
+ const m = mean(nums);
6004
+ let sum = 0;
6005
+ for (const n of nums) sum += (n - m) * (n - m);
6006
+ return sum / (nums.length - 1);
6007
+ }
6008
+ };
6009
+ registry.set("VAR", varImpl);
6010
+ registry.set("VAR.S", varImpl);
6011
+ const varpImpl = {
6012
+ minArgs: 1,
6013
+ maxArgs: -1,
6014
+ evaluate(args, context, evaluator) {
6015
+ const nums = extractNums(args, context, evaluator);
6016
+ if (nums instanceof FormulaError) return nums;
6017
+ if (nums.length === 0) return new FormulaError("#DIV/0!", "VARP requires at least 1 value");
6018
+ const m = mean(nums);
6019
+ let sum = 0;
6020
+ for (const n of nums) sum += (n - m) * (n - m);
6021
+ return sum / nums.length;
6022
+ }
6023
+ };
6024
+ registry.set("VARP", varpImpl);
6025
+ registry.set("VAR.P", varpImpl);
6026
+ registry.set("CORREL", {
6027
+ minArgs: 2,
6028
+ maxArgs: 2,
6029
+ evaluate(args, context, evaluator) {
6030
+ const nums1 = extractNums([args[0]], context, evaluator);
6031
+ if (nums1 instanceof FormulaError) return nums1;
6032
+ const nums2 = extractNums([args[1]], context, evaluator);
6033
+ if (nums2 instanceof FormulaError) return nums2;
6034
+ if (nums1.length !== nums2.length) {
6035
+ return new FormulaError("#N/A", "CORREL: arrays must have same number of values");
6036
+ }
6037
+ if (nums1.length < 2) {
6038
+ return new FormulaError("#DIV/0!", "CORREL requires at least 2 paired values");
6039
+ }
6040
+ const m1 = mean(nums1);
6041
+ const m2 = mean(nums2);
6042
+ let cov = 0;
6043
+ let var1 = 0;
6044
+ let var2 = 0;
6045
+ for (let i = 0; i < nums1.length; i++) {
6046
+ const d1 = nums1[i] - m1;
6047
+ const d2 = nums2[i] - m2;
6048
+ cov += d1 * d2;
6049
+ var1 += d1 * d1;
6050
+ var2 += d2 * d2;
6051
+ }
6052
+ const denom = Math.sqrt(var1 * var2);
6053
+ if (denom === 0) return new FormulaError("#DIV/0!", "CORREL: zero variance");
6054
+ return cov / denom;
6055
+ }
6056
+ });
6057
+ function percentileCalc(nums, k) {
6058
+ if (k < 0 || k > 1) return new FormulaError("#NUM!", "PERCENTILE: k must be between 0 and 1");
6059
+ if (nums.length === 0) return new FormulaError("#NUM!", "PERCENTILE: empty array");
6060
+ const sorted = [...nums].sort((a, b) => a - b);
6061
+ const idx = k * (sorted.length - 1);
6062
+ const low = Math.floor(idx);
6063
+ const high = Math.ceil(idx);
6064
+ if (low === high) return sorted[low];
6065
+ const frac = idx - low;
6066
+ return sorted[low] + frac * (sorted[high] - sorted[low]);
6067
+ }
6068
+ const percentileImpl = {
6069
+ minArgs: 2,
6070
+ maxArgs: 2,
6071
+ evaluate(args, context, evaluator) {
6072
+ const nums = extractNums([args[0]], context, evaluator);
6073
+ if (nums instanceof FormulaError) return nums;
6074
+ const rawK = evaluator.evaluate(args[1], context);
6075
+ if (rawK instanceof FormulaError) return rawK;
6076
+ const k = toNumber(rawK);
6077
+ if (k instanceof FormulaError) return k;
6078
+ return percentileCalc(nums, k);
6079
+ }
6080
+ };
6081
+ registry.set("PERCENTILE", percentileImpl);
6082
+ registry.set("PERCENTILE.INC", percentileImpl);
6083
+ const quartileImpl = {
6084
+ minArgs: 2,
6085
+ maxArgs: 2,
6086
+ evaluate(args, context, evaluator) {
6087
+ const nums = extractNums([args[0]], context, evaluator);
6088
+ if (nums instanceof FormulaError) return nums;
6089
+ const rawQuart = evaluator.evaluate(args[1], context);
6090
+ if (rawQuart instanceof FormulaError) return rawQuart;
6091
+ const quart = toNumber(rawQuart);
6092
+ if (quart instanceof FormulaError) return quart;
6093
+ const quartInt = Math.trunc(quart);
6094
+ if (quartInt < 0 || quartInt > 4) {
6095
+ return new FormulaError("#NUM!", "QUARTILE: quart must be 0, 1, 2, 3, or 4");
6096
+ }
6097
+ return percentileCalc(nums, quartInt * 0.25);
6098
+ }
6099
+ };
6100
+ registry.set("QUARTILE", quartileImpl);
6101
+ registry.set("QUARTILE.INC", quartileImpl);
6102
+ const modeImpl = {
6103
+ minArgs: 1,
6104
+ maxArgs: -1,
6105
+ evaluate(args, context, evaluator) {
6106
+ const nums = extractNums(args, context, evaluator);
6107
+ if (nums instanceof FormulaError) return nums;
6108
+ if (nums.length === 0) return new FormulaError("#N/A", "MODE: no numeric values");
6109
+ const freq = /* @__PURE__ */ new Map();
6110
+ for (const n of nums) {
6111
+ freq.set(n, (freq.get(n) ?? 0) + 1);
6112
+ }
6113
+ let maxFreq = 0;
6114
+ let modeVal = null;
6115
+ for (const n of nums) {
6116
+ const f = freq.get(n) ?? 0;
6117
+ if (f > maxFreq) {
6118
+ maxFreq = f;
6119
+ modeVal = n;
6120
+ }
6121
+ }
6122
+ if (maxFreq < 1 || modeVal === null) return new FormulaError("#N/A", "MODE: no values");
6123
+ return modeVal;
6124
+ }
6125
+ };
6126
+ registry.set("MODE", modeImpl);
6127
+ registry.set("MODE.SNGL", modeImpl);
6128
+ registry.set("GEOMEAN", {
6129
+ minArgs: 1,
6130
+ maxArgs: -1,
6131
+ evaluate(args, context, evaluator) {
6132
+ const nums = extractNums(args, context, evaluator);
6133
+ if (nums instanceof FormulaError) return nums;
6134
+ if (nums.length === 0) return new FormulaError("#NUM!", "GEOMEAN: no numeric values");
6135
+ let sumLn = 0;
6136
+ for (const n of nums) {
6137
+ if (n <= 0) return new FormulaError("#NUM!", "GEOMEAN: all values must be positive");
6138
+ sumLn += Math.log(n);
6139
+ }
6140
+ return Math.exp(sumLn / nums.length);
6141
+ }
6142
+ });
6143
+ registry.set("HARMEAN", {
6144
+ minArgs: 1,
6145
+ maxArgs: -1,
6146
+ evaluate(args, context, evaluator) {
6147
+ const nums = extractNums(args, context, evaluator);
6148
+ if (nums instanceof FormulaError) return nums;
6149
+ if (nums.length === 0) return new FormulaError("#NUM!", "HARMEAN: no numeric values");
6150
+ let sumRecip = 0;
6151
+ for (const n of nums) {
6152
+ if (n <= 0) return new FormulaError("#NUM!", "HARMEAN: all values must be positive");
6153
+ sumRecip += 1 / n;
6154
+ }
6155
+ if (sumRecip === 0) return new FormulaError("#DIV/0!", "HARMEAN: sum of reciprocals is zero");
6156
+ return nums.length / sumRecip;
6157
+ }
6158
+ });
6159
+ }
6160
+ function registerReferenceFunctions(registry) {
6161
+ registry.set("INDIRECT", {
6162
+ minArgs: 1,
6163
+ maxArgs: 2,
6164
+ evaluate(args, context, evaluator) {
6165
+ const rawRef = evaluator.evaluate(args[0], context);
6166
+ if (rawRef instanceof FormulaError) return rawRef;
6167
+ const refText = String(rawRef ?? "");
6168
+ const range = parseRange(refText);
6169
+ if (range) {
6170
+ const start = range.start;
6171
+ const end = range.end;
6172
+ if (start.row === end.row && start.col === end.col) {
6173
+ return context.getCellValue(start);
6174
+ }
6175
+ return context.getCellValue(start);
6176
+ }
6177
+ const addr = parseCellRef(refText);
6178
+ if (!addr) {
6179
+ return new FormulaError("#REF!", `INDIRECT: invalid reference "${refText}"`);
6180
+ }
6181
+ return context.getCellValue(addr);
6182
+ }
6183
+ });
6184
+ registry.set("OFFSET", {
6185
+ minArgs: 3,
6186
+ maxArgs: 5,
6187
+ evaluate(args, context, evaluator) {
6188
+ let baseCol;
6189
+ let baseRow;
6190
+ if (args[0].kind === "cellRef") {
6191
+ baseCol = args[0].address.col;
6192
+ baseRow = args[0].address.row;
6193
+ } else if (args[0].kind === "range") {
6194
+ baseCol = args[0].start.col;
6195
+ baseRow = args[0].start.row;
6196
+ } else {
6197
+ return new FormulaError("#VALUE!", "OFFSET: first argument must be a cell reference");
6198
+ }
6199
+ const rawRows = evaluator.evaluate(args[1], context);
6200
+ if (rawRows instanceof FormulaError) return rawRows;
6201
+ const rowOffset = toNumber(rawRows);
6202
+ if (rowOffset instanceof FormulaError) return rowOffset;
6203
+ const rawCols = evaluator.evaluate(args[2], context);
6204
+ if (rawCols instanceof FormulaError) return rawCols;
6205
+ const colOffset = toNumber(rawCols);
6206
+ if (colOffset instanceof FormulaError) return colOffset;
6207
+ const targetRow = baseRow + Math.trunc(rowOffset);
6208
+ const targetCol = baseCol + Math.trunc(colOffset);
6209
+ if (targetRow < 0 || targetCol < 0) {
6210
+ return new FormulaError("#REF!", "OFFSET: reference out of bounds");
6211
+ }
6212
+ let height = 1;
6213
+ if (args.length >= 4) {
6214
+ const rawH = evaluator.evaluate(args[3], context);
6215
+ if (rawH instanceof FormulaError) return rawH;
6216
+ const h = toNumber(rawH);
6217
+ if (h instanceof FormulaError) return h;
6218
+ height = Math.trunc(h);
6219
+ }
6220
+ let width = 1;
6221
+ if (args.length >= 5) {
6222
+ const rawW = evaluator.evaluate(args[4], context);
6223
+ if (rawW instanceof FormulaError) return rawW;
6224
+ const w = toNumber(rawW);
6225
+ if (w instanceof FormulaError) return w;
6226
+ width = Math.trunc(w);
6227
+ }
6228
+ if (height <= 0 || width <= 0) {
6229
+ return new FormulaError("#VALUE!", "OFFSET: height and width must be >= 1");
6230
+ }
6231
+ if (height === 1 && width === 1) {
6232
+ return context.getCellValue({ col: targetCol, row: targetRow, absCol: false, absRow: false });
6233
+ }
6234
+ return context.getCellValue({ col: targetCol, row: targetRow, absCol: false, absRow: false });
6235
+ }
6236
+ });
6237
+ registry.set("ADDRESS", {
6238
+ minArgs: 2,
6239
+ maxArgs: 5,
6240
+ evaluate(args, context, evaluator) {
6241
+ const rawRow = evaluator.evaluate(args[0], context);
6242
+ if (rawRow instanceof FormulaError) return rawRow;
6243
+ const rowNum = toNumber(rawRow);
6244
+ if (rowNum instanceof FormulaError) return rowNum;
6245
+ const rawCol = evaluator.evaluate(args[1], context);
6246
+ if (rawCol instanceof FormulaError) return rawCol;
6247
+ const colNum = toNumber(rawCol);
6248
+ if (colNum instanceof FormulaError) return colNum;
6249
+ const row = Math.trunc(rowNum);
6250
+ const col = Math.trunc(colNum);
6251
+ if (row < 1 || col < 1) {
6252
+ return new FormulaError("#VALUE!", "ADDRESS: row and column must be >= 1");
6253
+ }
6254
+ let absNum = 1;
6255
+ if (args.length >= 3) {
6256
+ const rawAbs = evaluator.evaluate(args[2], context);
6257
+ if (rawAbs instanceof FormulaError) return rawAbs;
6258
+ const a = toNumber(rawAbs);
6259
+ if (a instanceof FormulaError) return a;
6260
+ absNum = Math.trunc(a);
6261
+ }
6262
+ let sheetText = "";
6263
+ if (args.length >= 5) {
6264
+ const rawSheet = evaluator.evaluate(args[4], context);
6265
+ if (rawSheet instanceof FormulaError) return rawSheet;
6266
+ if (rawSheet !== null && rawSheet !== void 0 && rawSheet !== false) {
6267
+ sheetText = String(rawSheet);
6268
+ }
6269
+ }
6270
+ const colLetter = indexToColumnLetter(col - 1);
6271
+ let address;
6272
+ switch (absNum) {
6273
+ case 1:
6274
+ address = `$${colLetter}$${row}`;
6275
+ break;
6276
+ // $A$1
6277
+ case 2:
6278
+ address = `${colLetter}$${row}`;
6279
+ break;
6280
+ // A$1
6281
+ case 3:
6282
+ address = `$${colLetter}${row}`;
6283
+ break;
6284
+ // $A1
6285
+ case 4:
6286
+ address = `${colLetter}${row}`;
6287
+ break;
6288
+ // A1
6289
+ default:
6290
+ address = `$${colLetter}$${row}`;
6291
+ }
6292
+ if (sheetText) {
6293
+ const quoted = sheetText.includes(" ") ? `'${sheetText}'` : sheetText;
6294
+ return `${quoted}!${address}`;
6295
+ }
6296
+ return address;
6297
+ }
6298
+ });
6299
+ registry.set("ROW", {
6300
+ minArgs: 0,
6301
+ maxArgs: 1,
6302
+ evaluate(args, context, evaluator) {
6303
+ if (args.length === 0) {
6304
+ return 1;
6305
+ }
6306
+ const arg = args[0];
6307
+ if (arg.kind === "cellRef") {
6308
+ return arg.address.row + 1;
6309
+ }
6310
+ if (arg.kind === "range") {
6311
+ return arg.start.row + 1;
6312
+ }
6313
+ const rawRef = evaluator.evaluate(arg, context);
6314
+ if (rawRef instanceof FormulaError) return rawRef;
6315
+ const refText = String(rawRef ?? "");
6316
+ const addr = parseCellRef(refText);
6317
+ if (!addr) {
6318
+ const rng = parseRange(refText);
6319
+ if (rng) return rng.start.row + 1;
6320
+ return new FormulaError("#VALUE!", "ROW: invalid reference");
6321
+ }
6322
+ return addr.row + 1;
6323
+ }
6324
+ });
6325
+ registry.set("COLUMN", {
6326
+ minArgs: 0,
6327
+ maxArgs: 1,
6328
+ evaluate(args, context, evaluator) {
6329
+ if (args.length === 0) {
6330
+ return 1;
6331
+ }
6332
+ const arg = args[0];
6333
+ if (arg.kind === "cellRef") {
6334
+ return arg.address.col + 1;
6335
+ }
6336
+ if (arg.kind === "range") {
6337
+ return arg.start.col + 1;
6338
+ }
6339
+ const rawRef = evaluator.evaluate(arg, context);
6340
+ if (rawRef instanceof FormulaError) return rawRef;
6341
+ const refText = String(rawRef ?? "");
6342
+ const addr = parseCellRef(refText);
6343
+ if (!addr) {
6344
+ const rng = parseRange(refText);
6345
+ if (rng) return rng.start.col + 1;
6346
+ return new FormulaError("#VALUE!", "COLUMN: invalid reference");
6347
+ }
6348
+ return addr.col + 1;
6349
+ }
6350
+ });
6351
+ registry.set("ROWS", {
6352
+ minArgs: 1,
6353
+ maxArgs: 1,
6354
+ evaluate(args, _context, _evaluator) {
6355
+ const arg = args[0];
6356
+ if (arg.kind === "range") {
6357
+ return Math.abs(arg.end.row - arg.start.row) + 1;
6358
+ }
6359
+ if (arg.kind === "cellRef") {
6360
+ return 1;
6361
+ }
6362
+ return new FormulaError("#VALUE!", "ROWS: argument must be a range reference");
6363
+ }
6364
+ });
6365
+ registry.set("COLUMNS", {
6366
+ minArgs: 1,
6367
+ maxArgs: 1,
6368
+ evaluate(args, _context, _evaluator) {
6369
+ const arg = args[0];
6370
+ if (arg.kind === "range") {
6371
+ return Math.abs(arg.end.col - arg.start.col) + 1;
6372
+ }
6373
+ if (arg.kind === "cellRef") {
6374
+ return 1;
6375
+ }
6376
+ return new FormulaError("#VALUE!", "COLUMNS: argument must be a range reference");
6377
+ }
6378
+ });
6379
+ registry.set("SEQUENCE", {
6380
+ minArgs: 1,
6381
+ maxArgs: 4,
6382
+ evaluate(args, context, evaluator) {
6383
+ const rawRows = evaluator.evaluate(args[0], context);
6384
+ if (rawRows instanceof FormulaError) return rawRows;
6385
+ const rows = toNumber(rawRows);
6386
+ if (rows instanceof FormulaError) return rows;
6387
+ let cols = 1;
6388
+ if (args.length >= 2) {
6389
+ const rawCols = evaluator.evaluate(args[1], context);
6390
+ if (rawCols instanceof FormulaError) return rawCols;
6391
+ const c = toNumber(rawCols);
6392
+ if (c instanceof FormulaError) return c;
6393
+ cols = Math.trunc(c);
6394
+ }
6395
+ let start = 1;
6396
+ if (args.length >= 3) {
6397
+ const rawStart = evaluator.evaluate(args[2], context);
6398
+ if (rawStart instanceof FormulaError) return rawStart;
6399
+ const s = toNumber(rawStart);
6400
+ if (s instanceof FormulaError) return s;
6401
+ start = s;
6402
+ }
6403
+ let step = 1;
6404
+ if (args.length >= 4) {
6405
+ const rawStep = evaluator.evaluate(args[3], context);
6406
+ if (rawStep instanceof FormulaError) return rawStep;
6407
+ const st = toNumber(rawStep);
6408
+ if (st instanceof FormulaError) return st;
6409
+ step = st;
6410
+ }
6411
+ const rowCount = Math.trunc(rows);
6412
+ const colCount = Math.max(1, cols);
6413
+ if (rowCount < 1) {
6414
+ return new FormulaError("#VALUE!", "SEQUENCE: rows must be >= 1");
6415
+ }
6416
+ const result = [];
6417
+ let current = start;
6418
+ for (let r = 0; r < rowCount; r++) {
6419
+ const row = [];
6420
+ for (let c = 0; c < colCount; c++) {
6421
+ row.push(current);
6422
+ current += step;
6423
+ }
6424
+ result.push(row);
6425
+ }
6426
+ if (rowCount === 1 && colCount === 1) {
6427
+ return result[0][0];
6428
+ }
6429
+ return result[0][0];
6430
+ }
6431
+ });
6432
+ registry.set("TRANSPOSE", {
4783
6433
  minArgs: 1,
4784
6434
  maxArgs: 1,
4785
- evaluate(args, context, evaluator) {
4786
- const val = evaluator.evaluate(args[0], context);
4787
- return val instanceof FormulaError;
6435
+ evaluate(args, context, _evaluator) {
6436
+ if (args[0].kind !== "range") {
6437
+ return new FormulaError("#VALUE!", "TRANSPOSE: argument must be a range");
6438
+ }
6439
+ const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
6440
+ if (data.length === 0) return null;
6441
+ const rows = data.length;
6442
+ const cols = data[0].length;
6443
+ const transposed = [];
6444
+ for (let c = 0; c < cols; c++) {
6445
+ const newRow = [];
6446
+ for (let r = 0; r < rows; r++) {
6447
+ newRow.push(data[r][c]);
6448
+ }
6449
+ transposed.push(newRow);
6450
+ }
6451
+ return transposed[0][0] ?? null;
4788
6452
  }
4789
6453
  });
4790
- registry.set("ISNA", {
6454
+ registry.set("MMULT", {
6455
+ minArgs: 2,
6456
+ maxArgs: 2,
6457
+ evaluate(args, context, _evaluator) {
6458
+ if (args[0].kind !== "range") {
6459
+ return new FormulaError("#VALUE!", "MMULT: array1 must be a range");
6460
+ }
6461
+ if (args[1].kind !== "range") {
6462
+ return new FormulaError("#VALUE!", "MMULT: array2 must be a range");
6463
+ }
6464
+ const a = context.getRangeValues({ start: args[0].start, end: args[0].end });
6465
+ const b = context.getRangeValues({ start: args[1].start, end: args[1].end });
6466
+ if (a.length === 0 || b.length === 0) {
6467
+ return new FormulaError("#VALUE!", "MMULT: empty array");
6468
+ }
6469
+ const aRows = a.length;
6470
+ const aCols = a[0].length;
6471
+ const bRows = b.length;
6472
+ const bCols = b[0].length;
6473
+ if (aCols !== bRows) {
6474
+ return new FormulaError("#VALUE!", `MMULT: columns of array1 (${aCols}) must equal rows of array2 (${bRows})`);
6475
+ }
6476
+ const result = [];
6477
+ for (let r = 0; r < aRows; r++) {
6478
+ const row = [];
6479
+ for (let c = 0; c < bCols; c++) {
6480
+ let sum = 0;
6481
+ for (let k = 0; k < aCols; k++) {
6482
+ const av = toNumber(a[r][k]);
6483
+ const bv = toNumber(b[k][c]);
6484
+ if (av instanceof FormulaError) return av;
6485
+ if (bv instanceof FormulaError) return bv;
6486
+ sum += av * bv;
6487
+ }
6488
+ row.push(sum);
6489
+ }
6490
+ result.push(row);
6491
+ }
6492
+ return result[0][0];
6493
+ }
6494
+ });
6495
+ registry.set("MDETERM", {
4791
6496
  minArgs: 1,
4792
6497
  maxArgs: 1,
4793
- evaluate(args, context, evaluator) {
4794
- const val = evaluator.evaluate(args[0], context);
4795
- return val instanceof FormulaError && val.type === "#N/A";
6498
+ evaluate(args, context, _evaluator) {
6499
+ if (args[0].kind !== "range") {
6500
+ return new FormulaError("#VALUE!", "MDETERM: argument must be a range");
6501
+ }
6502
+ const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
6503
+ if (data.length === 0) return new FormulaError("#VALUE!", "MDETERM: empty array");
6504
+ const n = data.length;
6505
+ if (data.some((row) => row.length !== n)) {
6506
+ return new FormulaError("#VALUE!", "MDETERM: array must be square");
6507
+ }
6508
+ const matrix = [];
6509
+ for (let r = 0; r < n; r++) {
6510
+ const row = [];
6511
+ for (let c = 0; c < n; c++) {
6512
+ const v = toNumber(data[r][c]);
6513
+ if (v instanceof FormulaError) return v;
6514
+ row.push(v);
6515
+ }
6516
+ matrix.push(row);
6517
+ }
6518
+ return determinant(matrix);
4796
6519
  }
4797
6520
  });
4798
- registry.set("TYPE", {
6521
+ registry.set("MINVERSE", {
4799
6522
  minArgs: 1,
4800
6523
  maxArgs: 1,
4801
- evaluate(args, context, evaluator) {
4802
- const val = evaluator.evaluate(args[0], context);
4803
- if (val instanceof FormulaError) return 16;
4804
- if (typeof val === "number") return 1;
4805
- if (typeof val === "string") return 2;
4806
- if (typeof val === "boolean") return 4;
4807
- if (val === null || val === void 0) return 1;
4808
- return 1;
6524
+ evaluate(args, context, _evaluator) {
6525
+ if (args[0].kind !== "range") {
6526
+ return new FormulaError("#VALUE!", "MINVERSE: argument must be a range");
6527
+ }
6528
+ const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
6529
+ if (data.length === 0) return new FormulaError("#VALUE!", "MINVERSE: empty array");
6530
+ const n = data.length;
6531
+ if (data.some((row) => row.length !== n)) {
6532
+ return new FormulaError("#VALUE!", "MINVERSE: array must be square");
6533
+ }
6534
+ const matrix = [];
6535
+ for (let r = 0; r < n; r++) {
6536
+ const row = [];
6537
+ for (let c = 0; c < n; c++) {
6538
+ const v = toNumber(data[r][c]);
6539
+ if (v instanceof FormulaError) return v;
6540
+ row.push(v);
6541
+ }
6542
+ matrix.push(row);
6543
+ }
6544
+ const inv = matrixInverse(matrix, n);
6545
+ if (inv instanceof FormulaError) return inv;
6546
+ return inv[0][0];
4809
6547
  }
4810
6548
  });
4811
6549
  }
6550
+ function determinant(m) {
6551
+ const n = m.length;
6552
+ if (n === 1) return m[0][0];
6553
+ if (n === 2) return m[0][0] * m[1][1] - m[0][1] * m[1][0];
6554
+ let det = 0;
6555
+ for (let c = 0; c < n; c++) {
6556
+ const minor = [];
6557
+ for (let r = 1; r < n; r++) {
6558
+ const row = [];
6559
+ for (let cc = 0; cc < n; cc++) {
6560
+ if (cc !== c) row.push(m[r][cc]);
6561
+ }
6562
+ minor.push(row);
6563
+ }
6564
+ det += (c % 2 === 0 ? 1 : -1) * m[0][c] * determinant(minor);
6565
+ }
6566
+ return det;
6567
+ }
6568
+ function matrixInverse(m, n) {
6569
+ const aug = [];
6570
+ for (let r = 0; r < n; r++) {
6571
+ const row = [...m[r]];
6572
+ for (let c = 0; c < n; c++) {
6573
+ row.push(c === r ? 1 : 0);
6574
+ }
6575
+ aug.push(row);
6576
+ }
6577
+ for (let col = 0; col < n; col++) {
6578
+ let pivotRow = -1;
6579
+ let pivotVal = 0;
6580
+ for (let r = col; r < n; r++) {
6581
+ if (Math.abs(aug[r][col]) > Math.abs(pivotVal)) {
6582
+ pivotVal = aug[r][col];
6583
+ pivotRow = r;
6584
+ }
6585
+ }
6586
+ if (pivotRow === -1 || Math.abs(pivotVal) < 1e-12) {
6587
+ return new FormulaError("#NUM!", "MINVERSE: matrix is singular");
6588
+ }
6589
+ if (pivotRow !== col) {
6590
+ [aug[col], aug[pivotRow]] = [aug[pivotRow], aug[col]];
6591
+ }
6592
+ const scale = aug[col][col];
6593
+ for (let c = 0; c < 2 * n; c++) {
6594
+ aug[col][c] /= scale;
6595
+ }
6596
+ for (let r = 0; r < n; r++) {
6597
+ if (r !== col) {
6598
+ const factor = aug[r][col];
6599
+ for (let c = 0; c < 2 * n; c++) {
6600
+ aug[r][c] -= factor * aug[col][c];
6601
+ }
6602
+ }
6603
+ }
6604
+ }
6605
+ const result = [];
6606
+ for (let r = 0; r < n; r++) {
6607
+ result.push(aug[r].slice(n));
6608
+ }
6609
+ return result;
6610
+ }
4812
6611
  function createBuiltInFunctions() {
4813
6612
  const registry = /* @__PURE__ */ new Map();
4814
6613
  registerMathFunctions(registry);
@@ -4818,6 +6617,9 @@ function createBuiltInFunctions() {
4818
6617
  registerDateFunctions(registry);
4819
6618
  registerStatsFunctions(registry);
4820
6619
  registerInfoFunctions(registry);
6620
+ registerFinancialFunctions(registry);
6621
+ registerStatisticalExtendedFunctions(registry);
6622
+ registerReferenceFunctions(registry);
4821
6623
  return registry;
4822
6624
  }
4823
6625
  function extractDependencies(node) {
@@ -5170,8 +6972,7 @@ var FormulaEngine = class {
5170
6972
  return {
5171
6973
  getCellValue: (addr) => {
5172
6974
  const key = toCellKey(addr.col, addr.row, addr.sheet);
5173
- const cached = this.values.get(key);
5174
- if (cached !== void 0) return cached;
6975
+ if (this.values.has(key)) return this.values.get(key);
5175
6976
  if (addr.sheet) {
5176
6977
  const sheetAccessor = this.sheetAccessors.get(addr.sheet);
5177
6978
  if (!sheetAccessor) return new FormulaError("#REF!", `Unknown sheet: ${addr.sheet}`);
@@ -5194,18 +6995,21 @@ var FormulaEngine = class {
5194
6995
  const row = [];
5195
6996
  for (let c = minCol; c <= maxCol; c++) {
5196
6997
  const key = toCellKey(c, r, sheet);
5197
- const cached = this.values.get(key);
5198
- if (cached !== void 0) {
5199
- row.push(cached);
6998
+ if (this.values.has(key)) {
6999
+ row.push(this.values.get(key));
5200
7000
  } else {
5201
- row.push(rangeAccessor.getCellValue(c, r));
7001
+ row.push(rangeAccessor?.getCellValue(c, r));
5202
7002
  }
5203
7003
  }
5204
7004
  result.push(row);
5205
7005
  }
5206
7006
  return result;
5207
7007
  },
5208
- now: () => contextNow
7008
+ now: () => contextNow,
7009
+ getCellFormula: (addr) => {
7010
+ const key = toCellKey(addr.col, addr.row, addr.sheet);
7011
+ return this.formulas.get(key);
7012
+ }
5209
7013
  };
5210
7014
  }
5211
7015
  recalcCells(order, accessor, updatedCells) {
@@ -5694,9 +7498,13 @@ var TableRenderer = class {
5694
7498
  this.lastColumnWidths = {};
5695
7499
  this.lastHeaderSignature = "";
5696
7500
  this.lastRenderedItems = null;
7501
+ this.formulaEngine = null;
5697
7502
  this.container = container;
5698
7503
  this.state = state;
5699
7504
  }
7505
+ setFormulaEngine(engine) {
7506
+ this.formulaEngine = engine;
7507
+ }
5700
7508
  setVirtualScrollState(vs) {
5701
7509
  this.virtualScrollState = vs;
5702
7510
  }
@@ -5774,12 +7582,13 @@ var TableRenderer = class {
5774
7582
  const target = e.target;
5775
7583
  if (target.classList.contains("ogrid-resize-handle")) {
5776
7584
  e.stopPropagation();
7585
+ const handleColumnId = target.getAttribute("data-column-id");
5777
7586
  const th = target.closest("th[data-column-id]");
5778
- if (!th) return;
5779
- const columnId = th.getAttribute("data-column-id");
7587
+ const columnId = handleColumnId ?? th?.getAttribute("data-column-id");
5780
7588
  if (columnId) {
5781
- const rect = th.getBoundingClientRect();
5782
- this.interactionState?.onResizeStart?.(columnId, e.clientX, rect.width);
7589
+ const parentTh = target.closest("th");
7590
+ const rect = parentTh?.getBoundingClientRect();
7591
+ this.interactionState?.onResizeStart?.(columnId, e.clientX, rect?.width ?? ROW_NUMBER_COLUMN_WIDTH);
5783
7592
  }
5784
7593
  return;
5785
7594
  }
@@ -5797,9 +7606,9 @@ var TableRenderer = class {
5797
7606
  const target = e.target;
5798
7607
  if (target.classList.contains("ogrid-resize-handle")) {
5799
7608
  e.stopPropagation();
7609
+ const handleColumnId = target.getAttribute("data-column-id");
5800
7610
  const th = target.closest("th[data-column-id]");
5801
- if (!th) return;
5802
- const columnId = th.getAttribute("data-column-id");
7611
+ const columnId = handleColumnId ?? th?.getAttribute("data-column-id");
5803
7612
  if (columnId) {
5804
7613
  this.interactionState?.onResizeDoubleClick?.(columnId);
5805
7614
  }
@@ -5876,6 +7685,9 @@ var TableRenderer = class {
5876
7685
  parts.push(`allSel:${is?.allSelected ?? ""}`);
5877
7686
  parts.push(`someSel:${is?.someSelected ?? ""}`);
5878
7687
  parts.push(`rn:${is?.showRowNumbers ?? ""}`);
7688
+ if (is?.showRowNumbers) {
7689
+ parts.push(`rnw:${is?.columnWidths[ROW_NUMBER_COLUMN_ID] ?? ""}`);
7690
+ }
5879
7691
  parts.push(`cl:${is?.showColumnLetters ?? ""}`);
5880
7692
  for (const [colId, config] of this.filterConfigs) {
5881
7693
  const hasActive = this.headerFilterState?.hasActiveFilter(config);
@@ -6091,7 +7903,8 @@ var TableRenderer = class {
6091
7903
  if (hasRowNumbers) {
6092
7904
  const th = document.createElement("th");
6093
7905
  th.className = "ogrid-column-letter-cell";
6094
- th.style.width = `${ROW_NUMBER_COLUMN_WIDTH}px`;
7906
+ const rnw = this.interactionState?.columnWidths[ROW_NUMBER_COLUMN_ID] ?? ROW_NUMBER_COLUMN_WIDTH;
7907
+ th.style.width = `${rnw}px`;
6095
7908
  letterTr.appendChild(th);
6096
7909
  }
6097
7910
  for (let colIdx = 0; colIdx < visibleCols.length; colIdx++) {
@@ -6104,17 +7917,49 @@ var TableRenderer = class {
6104
7917
  }
6105
7918
  const headerRows = buildHeaderRows(this.state.allColumns, this.state.visibleColumns);
6106
7919
  if (headerRows.length > 1) {
6107
- for (const row of headerRows) {
7920
+ for (let rowIdx = 0; rowIdx < headerRows.length; rowIdx++) {
7921
+ const row = headerRows[rowIdx];
7922
+ const isLastRow = rowIdx === headerRows.length - 1;
6108
7923
  const tr = document.createElement("tr");
6109
7924
  if (hasCheckbox) {
6110
7925
  const th = document.createElement("th");
6111
7926
  th.className = "ogrid-header-cell ogrid-checkbox-header";
6112
7927
  th.style.width = `${CHECKBOX_COLUMN_WIDTH}px`;
6113
- if (row === headerRows[headerRows.length - 1]) {
7928
+ if (isLastRow) {
6114
7929
  this.appendSelectAllCheckbox(th);
6115
7930
  }
6116
7931
  tr.appendChild(th);
6117
7932
  }
7933
+ if (hasRowNumbers) {
7934
+ if (isLastRow) {
7935
+ const rnw = this.interactionState?.columnWidths[ROW_NUMBER_COLUMN_ID] ?? ROW_NUMBER_COLUMN_WIDTH;
7936
+ const th = document.createElement("th");
7937
+ th.className = "ogrid-header-cell ogrid-row-number-header";
7938
+ th.style.width = `${rnw}px`;
7939
+ th.style.minWidth = `${rnw}px`;
7940
+ th.style.maxWidth = `${rnw}px`;
7941
+ th.style.textAlign = "center";
7942
+ th.style.position = th.style.position || "relative";
7943
+ th.textContent = "#";
7944
+ const resizeHandle = document.createElement("div");
7945
+ resizeHandle.className = "ogrid-resize-handle";
7946
+ resizeHandle.style.position = "absolute";
7947
+ resizeHandle.style.right = "0";
7948
+ resizeHandle.style.top = "0";
7949
+ resizeHandle.style.bottom = "0";
7950
+ resizeHandle.style.width = "4px";
7951
+ resizeHandle.style.cursor = "col-resize";
7952
+ resizeHandle.style.userSelect = "none";
7953
+ resizeHandle.setAttribute("data-column-id", ROW_NUMBER_COLUMN_ID);
7954
+ th.appendChild(resizeHandle);
7955
+ tr.appendChild(th);
7956
+ } else if (rowIdx === 0) {
7957
+ const th = document.createElement("th");
7958
+ th.rowSpan = headerRows.length - 1;
7959
+ th.style.padding = "0";
7960
+ tr.appendChild(th);
7961
+ }
7962
+ }
6118
7963
  for (const cell of row) {
6119
7964
  const th = document.createElement("th");
6120
7965
  th.textContent = cell.label;
@@ -6149,11 +7994,26 @@ var TableRenderer = class {
6149
7994
  tr.appendChild(th);
6150
7995
  }
6151
7996
  if (this.hasRowNumbersColumn()) {
7997
+ const rnw = this.interactionState?.columnWidths[ROW_NUMBER_COLUMN_ID] ?? ROW_NUMBER_COLUMN_WIDTH;
6152
7998
  const th = document.createElement("th");
6153
7999
  th.className = "ogrid-header-cell ogrid-row-number-header";
6154
- th.style.width = `${ROW_NUMBER_COLUMN_WIDTH}px`;
8000
+ th.style.width = `${rnw}px`;
8001
+ th.style.minWidth = `${rnw}px`;
8002
+ th.style.maxWidth = `${rnw}px`;
6155
8003
  th.style.textAlign = "center";
8004
+ th.style.position = th.style.position || "relative";
6156
8005
  th.textContent = "#";
8006
+ const resizeHandle = document.createElement("div");
8007
+ resizeHandle.className = "ogrid-resize-handle";
8008
+ resizeHandle.style.position = "absolute";
8009
+ resizeHandle.style.right = "0";
8010
+ resizeHandle.style.top = "0";
8011
+ resizeHandle.style.bottom = "0";
8012
+ resizeHandle.style.width = "4px";
8013
+ resizeHandle.style.cursor = "col-resize";
8014
+ resizeHandle.style.userSelect = "none";
8015
+ resizeHandle.setAttribute("data-column-id", ROW_NUMBER_COLUMN_ID);
8016
+ th.appendChild(resizeHandle);
6157
8017
  tr.appendChild(th);
6158
8018
  }
6159
8019
  for (let colIdx = 0; colIdx < visibleCols.length; colIdx++) {
@@ -6325,9 +8185,12 @@ var TableRenderer = class {
6325
8185
  tr.appendChild(td);
6326
8186
  }
6327
8187
  if (hasRowNumbers) {
8188
+ const rnw = this.interactionState?.columnWidths[ROW_NUMBER_COLUMN_ID] ?? ROW_NUMBER_COLUMN_WIDTH;
6328
8189
  const td = document.createElement("td");
6329
8190
  td.className = "ogrid-cell ogrid-row-number-cell";
6330
- td.style.width = `${ROW_NUMBER_COLUMN_WIDTH}px`;
8191
+ td.style.width = `${rnw}px`;
8192
+ td.style.minWidth = `${rnw}px`;
8193
+ td.style.maxWidth = `${rnw}px`;
6331
8194
  td.style.textAlign = "center";
6332
8195
  td.style.color = "var(--ogrid-fg-muted, #666)";
6333
8196
  td.style.fontSize = "0.9em";
@@ -6377,11 +8240,22 @@ var TableRenderer = class {
6377
8240
  }
6378
8241
  }
6379
8242
  if (col.renderCell) {
6380
- const value = getCellValue(item, col);
8243
+ const rawValue = getCellValue(item, col);
8244
+ const value = this.formulaEngine?.isEnabled() && this.formulaEngine.hasFormula(colIndex, rowIndex) ? this.formulaEngine.getValue(colIndex, rowIndex) ?? rawValue : rawValue;
6381
8245
  col.renderCell(td, item, value);
6382
8246
  } else {
6383
- const value = getCellValue(item, col);
6384
- if (col.valueFormatter) {
8247
+ const rawValue = getCellValue(item, col);
8248
+ const value = this.formulaEngine?.isEnabled() && this.formulaEngine.hasFormula(colIndex, rowIndex) ? this.formulaEngine.getValue(colIndex, rowIndex) ?? rawValue : rawValue;
8249
+ if (col.type === "boolean") {
8250
+ const checkbox = document.createElement("input");
8251
+ checkbox.type = "checkbox";
8252
+ checkbox.checked = Boolean(value);
8253
+ checkbox.disabled = true;
8254
+ checkbox.style.margin = "0";
8255
+ checkbox.style.pointerEvents = "none";
8256
+ checkbox.setAttribute("aria-label", value ? "True" : "False");
8257
+ td.appendChild(checkbox);
8258
+ } else if (col.valueFormatter) {
6385
8259
  td.textContent = col.valueFormatter(value, item);
6386
8260
  } else if (value != null) {
6387
8261
  td.textContent = String(value);
@@ -8522,13 +10396,15 @@ var ColumnResizeState = class {
8522
10396
  updateResize(clientX) {
8523
10397
  if (!this.isResizing || !this.resizeColumnId) return null;
8524
10398
  const delta = clientX - this.resizeStartX;
8525
- const newWidth = Math.max(DEFAULT_MIN_COLUMN_WIDTH, this.resizeStartWidth + delta);
10399
+ const minW = this.resizeColumnId === ROW_NUMBER_COLUMN_ID ? ROW_NUMBER_COLUMN_MIN_WIDTH : DEFAULT_MIN_COLUMN_WIDTH;
10400
+ const newWidth = Math.max(minW, this.resizeStartWidth + delta);
8526
10401
  return newWidth;
8527
10402
  }
8528
10403
  endResize(clientX) {
8529
10404
  if (!this.isResizing || !this.resizeColumnId) return;
8530
10405
  const delta = clientX - this.resizeStartX;
8531
- const newWidth = Math.max(DEFAULT_MIN_COLUMN_WIDTH, this.resizeStartWidth + delta);
10406
+ const minW = this.resizeColumnId === ROW_NUMBER_COLUMN_ID ? ROW_NUMBER_COLUMN_MIN_WIDTH : DEFAULT_MIN_COLUMN_WIDTH;
10407
+ const newWidth = Math.max(minW, this.resizeStartWidth + delta);
8532
10408
  this.columnWidths.set(this.resizeColumnId, newWidth);
8533
10409
  this.emitter.emit("columnWidthChange", { columnId: this.resizeColumnId, widthPx: newWidth });
8534
10410
  this.isResizing = false;
@@ -9121,6 +10997,8 @@ var InlineCellEditor = class {
9121
10997
  e.stopPropagation();
9122
10998
  this.onCancel?.();
9123
10999
  this.closeEditor();
11000
+ } else if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
11001
+ e.stopPropagation();
9124
11002
  } else if ((e.ctrlKey || e.metaKey) && ["c", "x", "v", "a", "z", "y"].includes(e.key)) {
9125
11003
  e.stopPropagation();
9126
11004
  }
@@ -9165,6 +11043,8 @@ var InlineCellEditor = class {
9165
11043
  e.stopPropagation();
9166
11044
  this.onCancel?.();
9167
11045
  this.closeEditor();
11046
+ } else if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
11047
+ e.stopPropagation();
9168
11048
  }
9169
11049
  });
9170
11050
  return input;
@@ -9263,6 +11143,7 @@ var InlineCellEditor = class {
9263
11143
  };
9264
11144
  buildOptions();
9265
11145
  wrapper.addEventListener("keydown", (e) => {
11146
+ e.stopPropagation();
9266
11147
  switch (e.key) {
9267
11148
  case "ArrowDown": {
9268
11149
  e.preventDefault();
@@ -9384,6 +11265,8 @@ var InlineCellEditor = class {
9384
11265
  e.stopPropagation();
9385
11266
  this.onCancel?.();
9386
11267
  this.closeEditor();
11268
+ } else if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
11269
+ e.stopPropagation();
9387
11270
  } else if ((e.ctrlKey || e.metaKey) && ["c", "x", "v", "a", "z", "y"].includes(e.key)) {
9388
11271
  e.stopPropagation();
9389
11272
  }
@@ -9917,90 +11800,296 @@ var OGridRendering = class {
9917
11800
  this.renderSideBar();
9918
11801
  this.renderLoadingOverlay();
9919
11802
  }
9920
- renderHeaderFilterPopover() {
9921
- const { headerFilterState, headerFilterComponent, filterConfigs } = this.ctx;
9922
- const openId = headerFilterState.openColumnId;
9923
- const allBtns = this.ctx.tableContainer.querySelectorAll(".ogrid-filter-icon[aria-haspopup]");
9924
- for (const btn of allBtns) {
9925
- const colId = btn.closest("th[data-column-id]")?.getAttribute("data-column-id");
9926
- btn.setAttribute("aria-expanded", colId === openId ? "true" : "false");
9927
- }
9928
- if (!openId) {
9929
- headerFilterComponent.cleanup();
9930
- return;
9931
- }
9932
- const config = filterConfigs.get(openId);
9933
- if (!config) return;
9934
- headerFilterComponent.render(config);
9935
- const popoverEl = document.querySelector(".ogrid-header-filter-popover");
9936
- headerFilterState.setPopoverEl(popoverEl);
11803
+ renderHeaderFilterPopover() {
11804
+ const { headerFilterState, headerFilterComponent, filterConfigs } = this.ctx;
11805
+ const openId = headerFilterState.openColumnId;
11806
+ const allBtns = this.ctx.tableContainer.querySelectorAll(".ogrid-filter-icon[aria-haspopup]");
11807
+ for (const btn of allBtns) {
11808
+ const colId = btn.closest("th[data-column-id]")?.getAttribute("data-column-id");
11809
+ btn.setAttribute("aria-expanded", colId === openId ? "true" : "false");
11810
+ }
11811
+ if (!openId) {
11812
+ headerFilterComponent.cleanup();
11813
+ return;
11814
+ }
11815
+ const config = filterConfigs.get(openId);
11816
+ if (!config) return;
11817
+ headerFilterComponent.render(config);
11818
+ const popoverEl = document.querySelector(".ogrid-header-filter-popover");
11819
+ headerFilterState.setPopoverEl(popoverEl);
11820
+ }
11821
+ renderSideBar() {
11822
+ const { sideBarComponent, sideBarState, state } = this.ctx;
11823
+ if (!sideBarComponent || !sideBarState) return;
11824
+ const columns = state.columns.map((c) => ({
11825
+ columnId: c.columnId,
11826
+ name: c.name,
11827
+ required: c.required === true
11828
+ }));
11829
+ const filterableColumns = state.columns.filter((c) => c.filterable && typeof c.filterable === "object" && c.filterable.type).map((c) => ({
11830
+ columnId: c.columnId,
11831
+ name: c.name,
11832
+ filterField: c.filterable.filterField ?? c.columnId,
11833
+ filterType: c.filterable.type
11834
+ }));
11835
+ sideBarComponent.setConfig({
11836
+ columns,
11837
+ visibleColumns: state.visibleColumns,
11838
+ onVisibilityChange: (columnKey, visible) => {
11839
+ const next = new Set(state.visibleColumns);
11840
+ if (visible) next.add(columnKey);
11841
+ else next.delete(columnKey);
11842
+ state.setVisibleColumns(next);
11843
+ },
11844
+ onSetVisibleColumns: (cols) => state.setVisibleColumns(cols),
11845
+ filterableColumns,
11846
+ filters: state.filters,
11847
+ onFilterChange: (key, value) => state.setFilter(key, value),
11848
+ filterOptions: state.filterOptions
11849
+ });
11850
+ sideBarComponent.render();
11851
+ }
11852
+ renderLoadingOverlay() {
11853
+ const { state, tableContainer } = this.ctx;
11854
+ if (state.isLoading) {
11855
+ const { items } = state.getProcessedItems();
11856
+ tableContainer.style.minHeight = !items || items.length === 0 ? "200px" : "";
11857
+ let loadingOverlay = this.ctx.loadingOverlay;
11858
+ if (!loadingOverlay) {
11859
+ loadingOverlay = document.createElement("div");
11860
+ loadingOverlay.className = "ogrid-loading-overlay";
11861
+ loadingOverlay.style.position = "absolute";
11862
+ loadingOverlay.style.top = "0";
11863
+ loadingOverlay.style.left = "0";
11864
+ loadingOverlay.style.right = "0";
11865
+ loadingOverlay.style.bottom = "0";
11866
+ loadingOverlay.style.display = "flex";
11867
+ loadingOverlay.style.alignItems = "center";
11868
+ loadingOverlay.style.justifyContent = "center";
11869
+ loadingOverlay.style.background = "var(--ogrid-loading-overlay, rgba(255, 255, 255, 0.7))";
11870
+ loadingOverlay.style.zIndex = "100";
11871
+ const spinner = document.createElement("div");
11872
+ spinner.className = "ogrid-loading-spinner";
11873
+ spinner.textContent = "Loading...";
11874
+ loadingOverlay.appendChild(spinner);
11875
+ this.ctx.setLoadingOverlay(loadingOverlay);
11876
+ }
11877
+ if (!tableContainer.contains(loadingOverlay)) {
11878
+ tableContainer.appendChild(loadingOverlay);
11879
+ }
11880
+ } else {
11881
+ tableContainer.style.minHeight = "";
11882
+ const loadingOverlay = this.ctx.loadingOverlay;
11883
+ if (loadingOverlay && tableContainer.contains(loadingOverlay)) {
11884
+ loadingOverlay.remove();
11885
+ }
11886
+ }
11887
+ }
11888
+ };
11889
+
11890
+ // src/state/FormulaEngineState.ts
11891
+ var FormulaEngineState = class {
11892
+ constructor(options) {
11893
+ this.emitter = new EventEmitter();
11894
+ this.engine = null;
11895
+ this.options = options;
11896
+ if (options.formulas) {
11897
+ this.engine = new FormulaEngine({
11898
+ customFunctions: options.formulaFunctions,
11899
+ namedRanges: options.namedRanges
11900
+ });
11901
+ if (options.sheets) {
11902
+ for (const [name, accessor] of Object.entries(options.sheets)) {
11903
+ this.engine.registerSheet(name, accessor);
11904
+ }
11905
+ }
11906
+ }
11907
+ }
11908
+ /**
11909
+ * Initialize with an accessor — loads `initialFormulas` if provided.
11910
+ * Must be called after the grid data is available so the accessor is valid.
11911
+ */
11912
+ initialize(accessor) {
11913
+ if (!this.engine || !this.options.initialFormulas?.length) return;
11914
+ const result = this.engine.loadFormulas(this.options.initialFormulas, accessor);
11915
+ if (result.updatedCells.length > 0) {
11916
+ this.emitRecalc(result);
11917
+ }
11918
+ }
11919
+ /**
11920
+ * Set or clear a formula for a cell. Triggers recalculation of dependents
11921
+ * and emits `formulaRecalc`.
11922
+ */
11923
+ setFormula(col, row, formula, accessor) {
11924
+ if (!this.engine) return void 0;
11925
+ const result = this.engine.setFormula(col, row, formula, accessor);
11926
+ if (result.updatedCells.length > 0) {
11927
+ this.emitRecalc(result);
11928
+ }
11929
+ return result;
11930
+ }
11931
+ /**
11932
+ * Notify the engine that a non-formula cell's value changed.
11933
+ * Triggers recalculation of any formulas that depend on the changed cell.
11934
+ */
11935
+ onCellChanged(col, row, accessor) {
11936
+ if (!this.engine) return void 0;
11937
+ const result = this.engine.onCellChanged(col, row, accessor);
11938
+ if (result.updatedCells.length > 0) {
11939
+ this.emitRecalc(result);
11940
+ }
11941
+ return result;
11942
+ }
11943
+ /** Get the computed value for a formula cell (or undefined if no formula). */
11944
+ getValue(col, row) {
11945
+ return this.engine?.getValue(col, row);
11946
+ }
11947
+ /** Check if a cell has a formula. */
11948
+ hasFormula(col, row) {
11949
+ return this.engine?.hasFormula(col, row) ?? false;
11950
+ }
11951
+ /** Get the formula string for a cell (or undefined if no formula). */
11952
+ getFormula(col, row) {
11953
+ return this.engine?.getFormula(col, row);
11954
+ }
11955
+ /** Whether the formula engine is active. */
11956
+ isEnabled() {
11957
+ return this.engine !== null;
11958
+ }
11959
+ /** Define a named range. */
11960
+ defineNamedRange(name, ref) {
11961
+ this.engine?.defineNamedRange(name, ref);
11962
+ }
11963
+ /** Remove a named range. */
11964
+ removeNamedRange(name) {
11965
+ this.engine?.removeNamedRange(name);
11966
+ }
11967
+ /** Register a sheet accessor for cross-sheet references. */
11968
+ registerSheet(name, accessor) {
11969
+ this.engine?.registerSheet(name, accessor);
11970
+ }
11971
+ /** Unregister a sheet accessor. */
11972
+ unregisterSheet(name) {
11973
+ this.engine?.unregisterSheet(name);
11974
+ }
11975
+ /** Get all cells that a cell depends on (deep, transitive). */
11976
+ getPrecedents(col, row) {
11977
+ return this.engine?.getPrecedents(col, row) ?? [];
11978
+ }
11979
+ /** Get all cells that depend on a cell (deep, transitive). */
11980
+ getDependents(col, row) {
11981
+ return this.engine?.getDependents(col, row) ?? [];
11982
+ }
11983
+ /** Get full audit trail for a cell. */
11984
+ getAuditTrail(col, row) {
11985
+ return this.engine?.getAuditTrail(col, row) ?? null;
11986
+ }
11987
+ /** Subscribe to the `formulaRecalc` event. Returns an unsubscribe function. */
11988
+ onFormulaRecalc(handler) {
11989
+ this.emitter.on("formulaRecalc", handler);
11990
+ return () => this.emitter.off("formulaRecalc", handler);
9937
11991
  }
9938
- renderSideBar() {
9939
- const { sideBarComponent, sideBarState, state } = this.ctx;
9940
- if (!sideBarComponent || !sideBarState) return;
9941
- const columns = state.columns.map((c) => ({
9942
- columnId: c.columnId,
9943
- name: c.name,
9944
- required: c.required === true
9945
- }));
9946
- const filterableColumns = state.columns.filter((c) => c.filterable && typeof c.filterable === "object" && c.filterable.type).map((c) => ({
9947
- columnId: c.columnId,
9948
- name: c.name,
9949
- filterField: c.filterable.filterField ?? c.columnId,
9950
- filterType: c.filterable.type
9951
- }));
9952
- sideBarComponent.setConfig({
9953
- columns,
9954
- visibleColumns: state.visibleColumns,
9955
- onVisibilityChange: (columnKey, visible) => {
9956
- const next = new Set(state.visibleColumns);
9957
- if (visible) next.add(columnKey);
9958
- else next.delete(columnKey);
9959
- state.setVisibleColumns(next);
9960
- },
9961
- onSetVisibleColumns: (cols) => state.setVisibleColumns(cols),
9962
- filterableColumns,
9963
- filters: state.filters,
9964
- onFilterChange: (key, value) => state.setFilter(key, value),
9965
- filterOptions: state.filterOptions
9966
- });
9967
- sideBarComponent.render();
11992
+ /** Clean up all listeners. */
11993
+ destroy() {
11994
+ this.engine = null;
11995
+ this.emitter.removeAllListeners();
9968
11996
  }
9969
- renderLoadingOverlay() {
9970
- const { state, tableContainer } = this.ctx;
9971
- if (state.isLoading) {
9972
- const { items } = state.getProcessedItems();
9973
- tableContainer.style.minHeight = !items || items.length === 0 ? "200px" : "";
9974
- let loadingOverlay = this.ctx.loadingOverlay;
9975
- if (!loadingOverlay) {
9976
- loadingOverlay = document.createElement("div");
9977
- loadingOverlay.className = "ogrid-loading-overlay";
9978
- loadingOverlay.style.position = "absolute";
9979
- loadingOverlay.style.top = "0";
9980
- loadingOverlay.style.left = "0";
9981
- loadingOverlay.style.right = "0";
9982
- loadingOverlay.style.bottom = "0";
9983
- loadingOverlay.style.display = "flex";
9984
- loadingOverlay.style.alignItems = "center";
9985
- loadingOverlay.style.justifyContent = "center";
9986
- loadingOverlay.style.background = "var(--ogrid-loading-overlay, rgba(255, 255, 255, 0.7))";
9987
- loadingOverlay.style.zIndex = "100";
9988
- const spinner = document.createElement("div");
9989
- spinner.className = "ogrid-loading-spinner";
9990
- spinner.textContent = "Loading...";
9991
- loadingOverlay.appendChild(spinner);
9992
- this.ctx.setLoadingOverlay(loadingOverlay);
9993
- }
9994
- if (!tableContainer.contains(loadingOverlay)) {
9995
- tableContainer.appendChild(loadingOverlay);
11997
+ // --- Private ---
11998
+ emitRecalc(result) {
11999
+ this.options.onFormulaRecalc?.(result);
12000
+ this.emitter.emit("formulaRecalc", result);
12001
+ }
12002
+ };
12003
+
12004
+ // src/components/FormulaBar.ts
12005
+ var FormulaBar = class {
12006
+ constructor(callbacks) {
12007
+ this.el = null;
12008
+ this.nameBoxEl = null;
12009
+ this.inputEl = null;
12010
+ this.isEditing = false;
12011
+ // --- Private event handlers (arrow functions for stable `this`) ---
12012
+ this.handleKeyDown = (e) => {
12013
+ handleFormulaBarKeyDown(e.key, () => e.preventDefault(), this.callbacks.onCommit, this.callbacks.onCancel);
12014
+ };
12015
+ this.handleInput = () => {
12016
+ if (this.inputEl) {
12017
+ this.callbacks.onInputChange(this.inputEl.value);
9996
12018
  }
9997
- } else {
9998
- tableContainer.style.minHeight = "";
9999
- const loadingOverlay = this.ctx.loadingOverlay;
10000
- if (loadingOverlay && tableContainer.contains(loadingOverlay)) {
10001
- loadingOverlay.remove();
12019
+ };
12020
+ this.handleClick = () => {
12021
+ if (!this.isEditing) {
12022
+ this.callbacks.onStartEditing();
10002
12023
  }
12024
+ };
12025
+ this.callbacks = callbacks;
12026
+ }
12027
+ /** Create the formula bar DOM and append it to the given container. */
12028
+ mount(container) {
12029
+ if (this.el) return;
12030
+ this.el = document.createElement("div");
12031
+ this.el.className = "ogrid-formula-bar";
12032
+ this.el.setAttribute("role", "toolbar");
12033
+ this.el.setAttribute("aria-label", "Formula bar");
12034
+ this.el.style.cssText = FORMULA_BAR_CSS.bar;
12035
+ this.nameBoxEl = document.createElement("div");
12036
+ this.nameBoxEl.className = "ogrid-formula-bar-name";
12037
+ this.nameBoxEl.setAttribute("aria-label", "Active cell reference");
12038
+ this.nameBoxEl.style.cssText = FORMULA_BAR_CSS.nameBox;
12039
+ this.nameBoxEl.textContent = "\u2014";
12040
+ this.el.appendChild(this.nameBoxEl);
12041
+ const fxLabel = document.createElement("div");
12042
+ fxLabel.className = "ogrid-formula-bar-fx";
12043
+ fxLabel.setAttribute("aria-hidden", "true");
12044
+ fxLabel.style.cssText = FORMULA_BAR_CSS.fxLabel;
12045
+ fxLabel.textContent = "fx";
12046
+ this.el.appendChild(fxLabel);
12047
+ this.inputEl = document.createElement("input");
12048
+ this.inputEl.type = "text";
12049
+ this.inputEl.className = "ogrid-formula-bar-input";
12050
+ this.inputEl.setAttribute("aria-label", "Formula input");
12051
+ this.inputEl.spellcheck = false;
12052
+ this.inputEl.autocomplete = "off";
12053
+ this.inputEl.readOnly = true;
12054
+ this.inputEl.style.cssText = FORMULA_BAR_CSS.input;
12055
+ this.inputEl.addEventListener("keydown", this.handleKeyDown);
12056
+ this.inputEl.addEventListener("input", this.handleInput);
12057
+ this.inputEl.addEventListener("click", this.handleClick);
12058
+ this.inputEl.addEventListener("dblclick", this.handleClick);
12059
+ this.el.appendChild(this.inputEl);
12060
+ container.appendChild(this.el);
12061
+ }
12062
+ /** Update the formula bar display with the current active cell ref and formula text. */
12063
+ update(cellRef, formulaText) {
12064
+ if (this.nameBoxEl) {
12065
+ this.nameBoxEl.textContent = cellRef ?? "\u2014";
12066
+ }
12067
+ if (this.inputEl) {
12068
+ this.inputEl.value = formulaText;
12069
+ }
12070
+ }
12071
+ /** Set editing state. When true, the input becomes editable and receives focus. */
12072
+ setEditing(editing) {
12073
+ this.isEditing = editing;
12074
+ if (this.inputEl) {
12075
+ this.inputEl.readOnly = !editing;
12076
+ if (editing) {
12077
+ this.inputEl.focus();
12078
+ }
12079
+ }
12080
+ }
12081
+ /** Remove the formula bar from the DOM and clean up event listeners. */
12082
+ destroy() {
12083
+ if (this.inputEl) {
12084
+ this.inputEl.removeEventListener("keydown", this.handleKeyDown);
12085
+ this.inputEl.removeEventListener("input", this.handleInput);
12086
+ this.inputEl.removeEventListener("click", this.handleClick);
12087
+ this.inputEl.removeEventListener("dblclick", this.handleClick);
10003
12088
  }
12089
+ this.el?.remove();
12090
+ this.el = null;
12091
+ this.nameBoxEl = null;
12092
+ this.inputEl = null;
10004
12093
  }
10005
12094
  };
10006
12095
 
@@ -10134,6 +12223,13 @@ var OGrid = class {
10134
12223
  this.marchingAnts = null;
10135
12224
  this.cellEditor = null;
10136
12225
  this.contextMenu = null;
12226
+ this.formulaEngine = null;
12227
+ this.formulaBar = null;
12228
+ this.formulaBarContainer = null;
12229
+ /** Tracks the text currently displayed/edited in the formula bar. */
12230
+ this.formulaBarText = "";
12231
+ /** Whether the formula bar input is currently in editing mode. */
12232
+ this.formulaBarEditing = false;
10137
12233
  this.events = new EventEmitter();
10138
12234
  this.unsubscribes = [];
10139
12235
  this.isFullScreen = false;
@@ -10143,6 +12239,17 @@ var OGrid = class {
10143
12239
  this.state = new GridState(options);
10144
12240
  this.api = this.state.getApi();
10145
12241
  this.eventWiringHelper = new OGridEventWiring();
12242
+ if (options.formulas) {
12243
+ this.formulaEngine = new FormulaEngineState({
12244
+ formulas: true,
12245
+ initialFormulas: options.initialFormulas,
12246
+ onFormulaRecalc: options.onFormulaRecalc,
12247
+ formulaFunctions: options.formulaFunctions,
12248
+ namedRanges: options.namedRanges,
12249
+ sheets: options.sheets
12250
+ });
12251
+ this.state.setFormulaEngine(this.formulaEngine);
12252
+ }
10146
12253
  injectGlobalStyles("ogrid-theme-vars", OGRID_THEME_CSS);
10147
12254
  this.containerEl = document.createElement("div");
10148
12255
  this.containerEl.className = "ogrid-container";
@@ -10178,6 +12285,20 @@ var OGrid = class {
10178
12285
  this.unsubscribes.push(() => document.removeEventListener("keydown", handleEscKey));
10179
12286
  }
10180
12287
  this.containerEl.appendChild(this.toolbarEl);
12288
+ if (options.formulas) {
12289
+ this.formulaBarContainer = document.createElement("div");
12290
+ this.formulaBarContainer.className = "ogrid-formula-bar-container";
12291
+ this.formulaBar = new FormulaBar({
12292
+ onCommit: () => this.handleFormulaBarCommit(),
12293
+ onCancel: () => this.handleFormulaBarCancel(),
12294
+ onInputChange: (text) => {
12295
+ this.formulaBarText = text;
12296
+ },
12297
+ onStartEditing: () => this.handleFormulaBarStartEditing()
12298
+ });
12299
+ this.formulaBar.mount(this.formulaBarContainer);
12300
+ this.containerEl.appendChild(this.formulaBarContainer);
12301
+ }
10181
12302
  this.bodyArea = document.createElement("div");
10182
12303
  this.bodyArea.className = "ogrid-body-area";
10183
12304
  this.bodyArea.style.display = "flex";
@@ -10200,6 +12321,9 @@ var OGrid = class {
10200
12321
  this.layoutState = new TableLayoutState();
10201
12322
  this.layoutState.observeContainer(this.tableContainer);
10202
12323
  this.renderer = new TableRenderer(this.tableContainer, this.state);
12324
+ if (this.formulaEngine) {
12325
+ this.renderer.setFormulaEngine(this.formulaEngine);
12326
+ }
10203
12327
  this.pagination = new PaginationControls(this.paginationContainer, this.state);
10204
12328
  this.statusBar = new StatusBar(this.statusBarContainer);
10205
12329
  this.columnChooser = new ColumnChooser(this.toolbarEl, this.state);
@@ -10311,6 +12435,41 @@ var OGrid = class {
10311
12435
  })
10312
12436
  );
10313
12437
  }
12438
+ if (this.formulaBar && this.selectionState && this.formulaEngine) {
12439
+ const fBar = this.formulaBar;
12440
+ const sel = this.selectionState;
12441
+ const fEngine = this.formulaEngine;
12442
+ let colOffset = 0;
12443
+ if (this.rowSelectionState) colOffset++;
12444
+ if (options.showRowNumbers || options.cellReferences || options.formulas) colOffset++;
12445
+ this.unsubscribes.push(
12446
+ sel.onSelectionChange(({ activeCell }) => {
12447
+ this.formulaBarEditing = false;
12448
+ fBar.setEditing(false);
12449
+ if (activeCell) {
12450
+ const dataCol = activeCell.columnIndex - colOffset;
12451
+ const dataRow = (this.state.page - 1) * this.state.pageSize + activeCell.rowIndex;
12452
+ const cellRef = formatCellReference(dataCol, dataRow + 1);
12453
+ const formula = fEngine.getFormula(dataCol, dataRow);
12454
+ if (formula) {
12455
+ this.formulaBarText = formula;
12456
+ fBar.update(cellRef, formula);
12457
+ } else {
12458
+ const { items } = this.state.getProcessedItems();
12459
+ const visibleCols = this.state.visibleColumnDefs;
12460
+ const item = items[activeCell.rowIndex];
12461
+ const col = visibleCols[dataCol];
12462
+ const value = item && col ? String(item[col.columnId] ?? "") : "";
12463
+ this.formulaBarText = value;
12464
+ fBar.update(cellRef, value);
12465
+ }
12466
+ } else {
12467
+ this.formulaBarText = "";
12468
+ fBar.update(null, "");
12469
+ }
12470
+ })
12471
+ );
12472
+ }
10314
12473
  }
10315
12474
  this.unsubscribes.push(
10316
12475
  this.state.onStateChange(() => {
@@ -10535,6 +12694,83 @@ var OGrid = class {
10535
12694
  this.headerFilterState.setFilterOptions(this.state.filterOptions);
10536
12695
  this.headerFilterState.open(columnId, config, headerEl, tempPopover);
10537
12696
  }
12697
+ // --- Formula bar handlers ---
12698
+ /** Build a grid data accessor for the formula engine from current state. */
12699
+ buildFormulaAccessor() {
12700
+ const { items } = this.state.getProcessedItems();
12701
+ const visibleCols = this.state.visibleColumnDefs;
12702
+ return {
12703
+ getCellValue: (col, row) => {
12704
+ const item = items[row];
12705
+ const colDef = visibleCols[col];
12706
+ if (!item || !colDef) return void 0;
12707
+ return item[colDef.columnId];
12708
+ },
12709
+ getRowCount: () => items.length,
12710
+ getColumnCount: () => visibleCols.length
12711
+ };
12712
+ }
12713
+ handleFormulaBarCommit() {
12714
+ if (!this.formulaEngine || !this.selectionState) return;
12715
+ const ac = this.selectionState.activeCell;
12716
+ if (!ac) return;
12717
+ let colOffset = 0;
12718
+ if (this.rowSelectionState) colOffset++;
12719
+ if (this.options.showRowNumbers || this.options.cellReferences || this.options.formulas) colOffset++;
12720
+ const dataCol = ac.columnIndex - colOffset;
12721
+ const dataRow = (this.state.page - 1) * this.state.pageSize + ac.rowIndex;
12722
+ const text = this.formulaBarText;
12723
+ const accessor = this.buildFormulaAccessor();
12724
+ if (text.startsWith("=")) {
12725
+ this.formulaEngine.setFormula(dataCol, dataRow, text, accessor);
12726
+ } else {
12727
+ if (this.formulaEngine.hasFormula(dataCol, dataRow)) {
12728
+ this.formulaEngine.setFormula(dataCol, dataRow, null, accessor);
12729
+ }
12730
+ const { items } = this.state.getProcessedItems();
12731
+ const visibleCols = this.state.visibleColumnDefs;
12732
+ const item = items[ac.rowIndex];
12733
+ const col = visibleCols[dataCol];
12734
+ if (item && col) {
12735
+ item[col.columnId] = text;
12736
+ }
12737
+ }
12738
+ this.formulaBarEditing = false;
12739
+ this.formulaBar?.setEditing(false);
12740
+ this.renderingHelper.updateRendererInteractionState();
12741
+ this.renderer.getWrapperElement()?.focus();
12742
+ }
12743
+ handleFormulaBarCancel() {
12744
+ if (this.selectionState?.activeCell && this.formulaEngine) {
12745
+ const ac = this.selectionState.activeCell;
12746
+ let colOffset = 0;
12747
+ if (this.rowSelectionState) colOffset++;
12748
+ if (this.options.showRowNumbers || this.options.cellReferences || this.options.formulas) colOffset++;
12749
+ const dataCol = ac.columnIndex - colOffset;
12750
+ const dataRow = (this.state.page - 1) * this.state.pageSize + ac.rowIndex;
12751
+ const formula = this.formulaEngine.getFormula(dataCol, dataRow);
12752
+ if (formula) {
12753
+ this.formulaBarText = formula;
12754
+ this.formulaBar?.update(formatCellReference(dataCol, dataRow + 1), formula);
12755
+ } else {
12756
+ const { items } = this.state.getProcessedItems();
12757
+ const visibleCols = this.state.visibleColumnDefs;
12758
+ const item = items[ac.rowIndex];
12759
+ const col = visibleCols[dataCol];
12760
+ const value = item && col ? String(item[col.columnId] ?? "") : "";
12761
+ this.formulaBarText = value;
12762
+ this.formulaBar?.update(formatCellReference(dataCol, dataRow + 1), value);
12763
+ }
12764
+ }
12765
+ this.formulaBarEditing = false;
12766
+ this.formulaBar?.setEditing(false);
12767
+ this.renderer.getWrapperElement()?.focus();
12768
+ }
12769
+ handleFormulaBarStartEditing() {
12770
+ if (!this.selectionState?.activeCell) return;
12771
+ this.formulaBarEditing = true;
12772
+ this.formulaBar?.setEditing(true);
12773
+ }
10538
12774
  // Rendering methods delegated to OGridRendering helper:
10539
12775
  // - updateRendererInteractionState() -> this.renderingHelper.updateRendererInteractionState()
10540
12776
  // - updateDragAttributes() -> this.renderingHelper.updateDragAttributes()
@@ -10589,123 +12825,11 @@ var OGrid = class {
10589
12825
  this.layoutState.destroy();
10590
12826
  this.cellEditor?.closeEditor();
10591
12827
  this.contextMenu?.close();
12828
+ this.formulaBar?.destroy();
12829
+ this.formulaEngine?.destroy();
10592
12830
  this.events.removeAllListeners();
10593
12831
  this.containerEl.remove();
10594
12832
  }
10595
12833
  };
10596
12834
 
10597
- // src/state/FormulaEngineState.ts
10598
- var FormulaEngineState = class {
10599
- constructor(options) {
10600
- this.emitter = new EventEmitter();
10601
- this.engine = null;
10602
- this.options = options;
10603
- if (options.formulas) {
10604
- this.engine = new FormulaEngine({
10605
- customFunctions: options.formulaFunctions,
10606
- namedRanges: options.namedRanges
10607
- });
10608
- if (options.sheets) {
10609
- for (const [name, accessor] of Object.entries(options.sheets)) {
10610
- this.engine.registerSheet(name, accessor);
10611
- }
10612
- }
10613
- }
10614
- }
10615
- /**
10616
- * Initialize with an accessor — loads `initialFormulas` if provided.
10617
- * Must be called after the grid data is available so the accessor is valid.
10618
- */
10619
- initialize(accessor) {
10620
- if (!this.engine || !this.options.initialFormulas?.length) return;
10621
- const result = this.engine.loadFormulas(this.options.initialFormulas, accessor);
10622
- if (result.updatedCells.length > 0) {
10623
- this.emitRecalc(result);
10624
- }
10625
- }
10626
- /**
10627
- * Set or clear a formula for a cell. Triggers recalculation of dependents
10628
- * and emits `formulaRecalc`.
10629
- */
10630
- setFormula(col, row, formula, accessor) {
10631
- if (!this.engine) return void 0;
10632
- const result = this.engine.setFormula(col, row, formula, accessor);
10633
- if (result.updatedCells.length > 0) {
10634
- this.emitRecalc(result);
10635
- }
10636
- return result;
10637
- }
10638
- /**
10639
- * Notify the engine that a non-formula cell's value changed.
10640
- * Triggers recalculation of any formulas that depend on the changed cell.
10641
- */
10642
- onCellChanged(col, row, accessor) {
10643
- if (!this.engine) return void 0;
10644
- const result = this.engine.onCellChanged(col, row, accessor);
10645
- if (result.updatedCells.length > 0) {
10646
- this.emitRecalc(result);
10647
- }
10648
- return result;
10649
- }
10650
- /** Get the computed value for a formula cell (or undefined if no formula). */
10651
- getValue(col, row) {
10652
- return this.engine?.getValue(col, row);
10653
- }
10654
- /** Check if a cell has a formula. */
10655
- hasFormula(col, row) {
10656
- return this.engine?.hasFormula(col, row) ?? false;
10657
- }
10658
- /** Get the formula string for a cell (or undefined if no formula). */
10659
- getFormula(col, row) {
10660
- return this.engine?.getFormula(col, row);
10661
- }
10662
- /** Whether the formula engine is active. */
10663
- isEnabled() {
10664
- return this.engine !== null;
10665
- }
10666
- /** Define a named range. */
10667
- defineNamedRange(name, ref) {
10668
- this.engine?.defineNamedRange(name, ref);
10669
- }
10670
- /** Remove a named range. */
10671
- removeNamedRange(name) {
10672
- this.engine?.removeNamedRange(name);
10673
- }
10674
- /** Register a sheet accessor for cross-sheet references. */
10675
- registerSheet(name, accessor) {
10676
- this.engine?.registerSheet(name, accessor);
10677
- }
10678
- /** Unregister a sheet accessor. */
10679
- unregisterSheet(name) {
10680
- this.engine?.unregisterSheet(name);
10681
- }
10682
- /** Get all cells that a cell depends on (deep, transitive). */
10683
- getPrecedents(col, row) {
10684
- return this.engine?.getPrecedents(col, row) ?? [];
10685
- }
10686
- /** Get all cells that depend on a cell (deep, transitive). */
10687
- getDependents(col, row) {
10688
- return this.engine?.getDependents(col, row) ?? [];
10689
- }
10690
- /** Get full audit trail for a cell. */
10691
- getAuditTrail(col, row) {
10692
- return this.engine?.getAuditTrail(col, row) ?? null;
10693
- }
10694
- /** Subscribe to the `formulaRecalc` event. Returns an unsubscribe function. */
10695
- onFormulaRecalc(handler) {
10696
- this.emitter.on("formulaRecalc", handler);
10697
- return () => this.emitter.off("formulaRecalc", handler);
10698
- }
10699
- /** Clean up all listeners. */
10700
- destroy() {
10701
- this.engine = null;
10702
- this.emitter.removeAllListeners();
10703
- }
10704
- // --- Private ---
10705
- emitRecalc(result) {
10706
- this.options.onFormulaRecalc?.(result);
10707
- this.emitter.emit("formulaRecalc", result);
10708
- }
10709
- };
10710
-
10711
- export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, CIRC_ERROR, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, DIV_ZERO_ERROR, DependencyGraph, EventEmitter, FillHandleState, FormulaEngine, FormulaEngineState, FormulaError, FormulaEvaluator, GENERAL_ERROR, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NAME_ERROR, NA_ERROR, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, REF_ERROR, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VALUE_ERROR, VirtualScrollState, Z_INDEX, adjustFormulaReferences, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, columnLetterToIndex, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleColumnRange, computeVisibleRange, createBuiltInFunctions, createSortFilterWorker, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, emailParser, escapeCsvValue, exportToCsv, extractValueMatrix, findCtrlArrowTarget, flattenArgs, flattenColumns, formatAddress, formatCellReference, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, toString as formulaToString, fromCellKey, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, indexToColumnLetter, injectGlobalStyles, isColumnEditable, isFilterConfig, isFormulaError, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parse, parseCellRef, parseRange, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, processClientSideDataAsync, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, terminateSortFilterWorker, toBoolean, toCellKey, toNumber, toUserLike, tokenize, triggerCsvDownload, validateColumns, validateRowIds, validateVirtualScrollConfig };
12835
+ export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, CIRC_ERROR, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, DIV_ZERO_ERROR, DependencyGraph, EventEmitter, FORMULA_BAR_CSS, FORMULA_BAR_STYLES, FORMULA_REF_COLORS, FillHandleState, FormulaBar, FormulaEngine, FormulaEngineState, FormulaError, FormulaEvaluator, GENERAL_ERROR, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NAME_ERROR, NA_ERROR, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, REF_ERROR, ROW_NUMBER_COLUMN_ID, ROW_NUMBER_COLUMN_MIN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VALUE_ERROR, VirtualScrollState, Z_INDEX, adjustFormulaReferences, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, columnLetterToIndex, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleColumnRange, computeVisibleRange, createBuiltInFunctions, createGridDataAccessor, createSortFilterWorker, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, deriveFormulaBarText, emailParser, escapeCsvValue, exportToCsv, extractFormulaReferences, extractValueMatrix, findCtrlArrowTarget, flattenArgs, flattenColumns, formatAddress, formatCellReference, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, toString as formulaToString, fromCellKey, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, handleFormulaBarKeyDown, indexToColumnLetter, injectGlobalStyles, isColumnEditable, isFilterConfig, isFormulaError, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parse, parseCellRef, parseRange, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, processClientSideDataAsync, processFormulaBarCommit, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, terminateSortFilterWorker, toBoolean, toCellKey, toNumber, toUserLike, tokenize, triggerCsvDownload, validateColumns, validateRowIds, validateVirtualScrollConfig };