@alaarab/ogrid-js 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -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,7 @@ 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
1213
1225
  };
1214
1226
  }
1215
1227
  function resolveCellDisplayContent(col, item, displayValue) {
@@ -1224,7 +1236,7 @@ function resolveCellDisplayContent(col, item, displayValue) {
1224
1236
  if (displayValue == null) return null;
1225
1237
  if (col.type === "date") {
1226
1238
  const d = new Date(String(displayValue));
1227
- if (!Number.isNaN(d.getTime())) return d.toLocaleDateString();
1239
+ if (!Number.isNaN(d.getTime())) return d.toLocaleDateString(void 0, { timeZone: "UTC" });
1228
1240
  }
1229
1241
  if (col.type === "boolean") {
1230
1242
  return displayValue ? "True" : "False";
@@ -1994,59 +2006,6 @@ function validateRowIds(items, getRowId) {
1994
2006
  ids.add(id);
1995
2007
  }
1996
2008
  }
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
2009
  var CELL_REF_PATTERN = /^\$?[A-Za-z]+\$?\d+$/;
2051
2010
  var SINGLE_CHAR_OPERATORS = {
2052
2011
  "+": "PLUS",
@@ -2168,6 +2127,12 @@ function tokenize(input) {
2168
2127
  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
2128
  pos++;
2170
2129
  }
2130
+ 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")) {
2131
+ pos++;
2132
+ while (pos < input.length && (input[pos] >= "A" && input[pos] <= "Z" || input[pos] >= "a" && input[pos] <= "z" || input[pos] >= "0" && input[pos] <= "9" || input[pos] === "_")) {
2133
+ pos++;
2134
+ }
2135
+ }
2171
2136
  const word = input.slice(start, pos);
2172
2137
  if (pos < input.length && input[pos] === "!") {
2173
2138
  pos++;
@@ -2195,6 +2160,190 @@ function tokenize(input) {
2195
2160
  tokens.push({ type: "EOF", value: "", position: pos });
2196
2161
  return tokens;
2197
2162
  }
2163
+ var CELL_REF_RE2 = /^\$?([A-Za-z]+)\$?(\d+)$/;
2164
+ function parseCellRefCoords(ref) {
2165
+ const m = ref.match(CELL_REF_RE2);
2166
+ if (!m) return null;
2167
+ return { col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 };
2168
+ }
2169
+ function handleFormulaBarKeyDown(key, preventDefault, onCommit, onCancel) {
2170
+ if (key === "Enter") {
2171
+ preventDefault();
2172
+ onCommit();
2173
+ } else if (key === "Escape") {
2174
+ preventDefault();
2175
+ onCancel();
2176
+ }
2177
+ }
2178
+ function processFormulaBarCommit(text, col, row, setFormula, onCellValueChanged) {
2179
+ const trimmed = text.trim();
2180
+ if (trimmed.startsWith("=")) {
2181
+ setFormula(col, row, trimmed);
2182
+ } else {
2183
+ setFormula(col, row, null);
2184
+ onCellValueChanged?.(col, row, trimmed);
2185
+ }
2186
+ }
2187
+ function deriveFormulaBarText(col, row, getFormula, getRawValue) {
2188
+ if (col == null || row == null) return "";
2189
+ const formula = getFormula?.(col, row);
2190
+ if (formula) return formula;
2191
+ const raw = getRawValue?.(col, row);
2192
+ return raw != null ? String(raw) : "";
2193
+ }
2194
+ function extractFormulaReferences(formula) {
2195
+ if (!formula || formula[0] !== "=") return [];
2196
+ const refs = [];
2197
+ let colorIdx = 0;
2198
+ try {
2199
+ const tokens = tokenize(formula.substring(1));
2200
+ for (let i = 0; i < tokens.length; i++) {
2201
+ const tok = tokens[i];
2202
+ if (tok.type === "CELL_REF") {
2203
+ if (i + 2 < tokens.length && tokens[i + 1].type === "COLON" && tokens[i + 2].type === "CELL_REF") {
2204
+ const start = parseCellRefCoords(tok.value);
2205
+ const end = parseCellRefCoords(tokens[i + 2].value);
2206
+ if (start && end) {
2207
+ refs.push({
2208
+ type: "range",
2209
+ col: start.col,
2210
+ row: start.row,
2211
+ endCol: end.col,
2212
+ endRow: end.row,
2213
+ colorIndex: colorIdx++ % 6
2214
+ });
2215
+ i += 2;
2216
+ continue;
2217
+ }
2218
+ }
2219
+ const coords = parseCellRefCoords(tok.value);
2220
+ if (coords) {
2221
+ refs.push({
2222
+ type: "cell",
2223
+ col: coords.col,
2224
+ row: coords.row,
2225
+ colorIndex: colorIdx++ % 6
2226
+ });
2227
+ }
2228
+ }
2229
+ }
2230
+ } catch {
2231
+ }
2232
+ return refs;
2233
+ }
2234
+ var DEFAULT_DEBOUNCE_MS = 300;
2235
+ var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
2236
+ var SIDEBAR_TRANSITION_MS = 300;
2237
+ var Z_INDEX = {
2238
+ /** Column resize drag handle */
2239
+ RESIZE_HANDLE: 1,
2240
+ /** Active/editing cell outline */
2241
+ ACTIVE_CELL: 2,
2242
+ /** Fill handle dot */
2243
+ FILL_HANDLE: 3,
2244
+ /** Selection range overlay (marching ants) */
2245
+ SELECTION_OVERLAY: 4,
2246
+ /** Row number column */
2247
+ ROW_NUMBER: 5,
2248
+ /** Clipboard overlay (copy/cut animation) */
2249
+ CLIPBOARD_OVERLAY: 5,
2250
+ /** Sticky pinned body cells */
2251
+ PINNED: 6,
2252
+ /** Selection checkbox column in body */
2253
+ SELECTION_CELL: 7,
2254
+ /** Sticky thead row */
2255
+ THEAD: 8,
2256
+ /** Pinned header cells (sticky both axes) */
2257
+ PINNED_HEADER: 10,
2258
+ /** Focused header cell */
2259
+ HEADER_FOCUS: 11,
2260
+ /** Checkbox column in sticky header (sticky both axes) */
2261
+ SELECTION_HEADER_PINNED: 12,
2262
+ /** Loading overlay within table */
2263
+ LOADING: 2,
2264
+ /** Column reorder drop indicator */
2265
+ DROP_INDICATOR: 100,
2266
+ /** Dropdown menus (column chooser, pagination size select) */
2267
+ DROPDOWN: 1e3,
2268
+ /** Filter popovers */
2269
+ FILTER_POPOVER: 1e3,
2270
+ /** Modal dialogs */
2271
+ MODAL: 2e3,
2272
+ /** Fullscreen grid container */
2273
+ FULLSCREEN: 9999,
2274
+ /** Context menus (right-click grid menu) */
2275
+ CONTEXT_MENU: 1e4
2276
+ };
2277
+ var FORMULA_REF_COLORS = [
2278
+ "var(--ogrid-formula-ref-0, #4285f4)",
2279
+ "var(--ogrid-formula-ref-1, #ea4335)",
2280
+ "var(--ogrid-formula-ref-2, #34a853)",
2281
+ "var(--ogrid-formula-ref-3, #9334e6)",
2282
+ "var(--ogrid-formula-ref-4, #ff6d01)",
2283
+ "var(--ogrid-formula-ref-5, #46bdc6)"
2284
+ ];
2285
+ var FORMULA_BAR_CSS = {
2286
+ 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;",
2287
+ 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;",
2288
+ 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;",
2289
+ 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;"
2290
+ };
2291
+ var FORMULA_BAR_STYLES = {
2292
+ bar: {
2293
+ display: "flex",
2294
+ alignItems: "center",
2295
+ borderBottom: "1px solid var(--ogrid-border, #e0e0e0)",
2296
+ background: "var(--ogrid-bg, #fff)",
2297
+ minHeight: "28px",
2298
+ fontSize: "13px"
2299
+ },
2300
+ nameBox: {
2301
+ fontFamily: "monospace",
2302
+ fontSize: "12px",
2303
+ fontWeight: 500,
2304
+ padding: "2px 8px",
2305
+ borderRight: "1px solid var(--ogrid-border, #e0e0e0)",
2306
+ background: "var(--ogrid-bg, #fff)",
2307
+ color: "var(--ogrid-fg, #242424)",
2308
+ minWidth: "52px",
2309
+ textAlign: "center",
2310
+ lineHeight: "24px",
2311
+ userSelect: "none",
2312
+ whiteSpace: "nowrap"
2313
+ },
2314
+ fxLabel: {
2315
+ padding: "2px 8px",
2316
+ fontStyle: "italic",
2317
+ fontWeight: 600,
2318
+ color: "var(--ogrid-muted-fg, #888)",
2319
+ userSelect: "none",
2320
+ borderRight: "1px solid var(--ogrid-border, #e0e0e0)",
2321
+ lineHeight: "24px",
2322
+ fontSize: "12px"
2323
+ },
2324
+ input: {
2325
+ flex: 1,
2326
+ border: "none",
2327
+ outline: "none",
2328
+ padding: "2px 8px",
2329
+ fontFamily: "monospace",
2330
+ fontSize: "12px",
2331
+ lineHeight: "24px",
2332
+ background: "transparent",
2333
+ color: "var(--ogrid-fg, #242424)",
2334
+ minWidth: 0
2335
+ }
2336
+ };
2337
+ var REF_ERROR = new FormulaError("#REF!", "Invalid cell reference");
2338
+ var DIV_ZERO_ERROR = new FormulaError("#DIV/0!", "Division by zero");
2339
+ var VALUE_ERROR = new FormulaError("#VALUE!", "Wrong value type");
2340
+ var NAME_ERROR = new FormulaError("#NAME?", "Unknown function or name");
2341
+ var CIRC_ERROR = new FormulaError("#CIRC!", "Circular reference");
2342
+ var GENERAL_ERROR = new FormulaError("#ERROR!", "Formula error");
2343
+ var NA_ERROR = new FormulaError("#N/A", "No match found");
2344
+ function isFormulaError(value) {
2345
+ return value instanceof FormulaError;
2346
+ }
2198
2347
  function parse(tokens, namedRanges) {
2199
2348
  let pos = 0;
2200
2349
  function peek() {
@@ -2839,7 +2988,7 @@ var DependencyGraph = class {
2839
2988
  if (cellDependents) {
2840
2989
  for (const dependent of cellDependents) {
2841
2990
  if (affected.has(dependent)) {
2842
- const newDegree = inDegree.get(dependent) - 1;
2991
+ const newDegree = (inDegree.get(dependent) ?? 0) - 1;
2843
2992
  inDegree.set(dependent, newDegree);
2844
2993
  if (newDegree === 0) {
2845
2994
  queue.push(dependent);
@@ -3149,7 +3298,7 @@ function registerMathFunctions(registry) {
3149
3298
  registry.set("SUMPRODUCT", {
3150
3299
  minArgs: 1,
3151
3300
  maxArgs: -1,
3152
- evaluate(args, context, evaluator) {
3301
+ evaluate(args, context, _evaluator) {
3153
3302
  const arrays = [];
3154
3303
  for (const arg of args) {
3155
3304
  if (arg.kind !== "range") {
@@ -3368,6 +3517,162 @@ function registerMathFunctions(registry) {
3368
3517
  return Math.floor(Math.random() * (hi - lo + 1)) + lo;
3369
3518
  }
3370
3519
  });
3520
+ registry.set("MROUND", {
3521
+ minArgs: 2,
3522
+ maxArgs: 2,
3523
+ evaluate(args, context, evaluator) {
3524
+ const rawNum = evaluator.evaluate(args[0], context);
3525
+ if (rawNum instanceof FormulaError) return rawNum;
3526
+ const num = toNumber(rawNum);
3527
+ if (num instanceof FormulaError) return num;
3528
+ const rawMul = evaluator.evaluate(args[1], context);
3529
+ if (rawMul instanceof FormulaError) return rawMul;
3530
+ const multiple = toNumber(rawMul);
3531
+ if (multiple instanceof FormulaError) return multiple;
3532
+ if (multiple === 0) return 0;
3533
+ if (num > 0 && multiple < 0 || num < 0 && multiple > 0) {
3534
+ return new FormulaError("#NUM!", "MROUND: number and multiple must have the same sign");
3535
+ }
3536
+ return Math.round(num / multiple) * multiple;
3537
+ }
3538
+ });
3539
+ registry.set("QUOTIENT", {
3540
+ minArgs: 2,
3541
+ maxArgs: 2,
3542
+ evaluate(args, context, evaluator) {
3543
+ const rawNum = evaluator.evaluate(args[0], context);
3544
+ if (rawNum instanceof FormulaError) return rawNum;
3545
+ const num = toNumber(rawNum);
3546
+ if (num instanceof FormulaError) return num;
3547
+ const rawDen = evaluator.evaluate(args[1], context);
3548
+ if (rawDen instanceof FormulaError) return rawDen;
3549
+ const den = toNumber(rawDen);
3550
+ if (den instanceof FormulaError) return den;
3551
+ if (den === 0) return new FormulaError("#DIV/0!", "QUOTIENT: division by zero");
3552
+ return Math.trunc(num / den);
3553
+ }
3554
+ });
3555
+ registry.set("COMBIN", {
3556
+ minArgs: 2,
3557
+ maxArgs: 2,
3558
+ evaluate(args, context, evaluator) {
3559
+ const rawN = evaluator.evaluate(args[0], context);
3560
+ if (rawN instanceof FormulaError) return rawN;
3561
+ const n = toNumber(rawN);
3562
+ if (n instanceof FormulaError) return n;
3563
+ const rawK = evaluator.evaluate(args[1], context);
3564
+ if (rawK instanceof FormulaError) return rawK;
3565
+ const k = toNumber(rawK);
3566
+ if (k instanceof FormulaError) return k;
3567
+ const ni = Math.trunc(n);
3568
+ const ki = Math.trunc(k);
3569
+ if (ni < 0 || ki < 0) return new FormulaError("#NUM!", "COMBIN: n and k must be non-negative");
3570
+ if (ki > ni) return new FormulaError("#NUM!", "COMBIN: k must be <= n");
3571
+ if (ki === 0 || ki === ni) return 1;
3572
+ const kk = Math.min(ki, ni - ki);
3573
+ let result = 1;
3574
+ for (let i = 0; i < kk; i++) {
3575
+ result = result * (ni - i) / (i + 1);
3576
+ }
3577
+ return Math.round(result);
3578
+ }
3579
+ });
3580
+ registry.set("PERMUT", {
3581
+ minArgs: 2,
3582
+ maxArgs: 2,
3583
+ evaluate(args, context, evaluator) {
3584
+ const rawN = evaluator.evaluate(args[0], context);
3585
+ if (rawN instanceof FormulaError) return rawN;
3586
+ const n = toNumber(rawN);
3587
+ if (n instanceof FormulaError) return n;
3588
+ const rawK = evaluator.evaluate(args[1], context);
3589
+ if (rawK instanceof FormulaError) return rawK;
3590
+ const k = toNumber(rawK);
3591
+ if (k instanceof FormulaError) return k;
3592
+ const ni = Math.trunc(n);
3593
+ const ki = Math.trunc(k);
3594
+ if (ni < 0 || ki < 0) return new FormulaError("#NUM!", "PERMUT: n and k must be non-negative");
3595
+ if (ki > ni) return new FormulaError("#NUM!", "PERMUT: k must be <= n");
3596
+ let result = 1;
3597
+ for (let i = 0; i < ki; i++) {
3598
+ result *= ni - i;
3599
+ }
3600
+ return result;
3601
+ }
3602
+ });
3603
+ registry.set("FACT", {
3604
+ minArgs: 1,
3605
+ maxArgs: 1,
3606
+ evaluate(args, context, evaluator) {
3607
+ const rawVal = evaluator.evaluate(args[0], context);
3608
+ if (rawVal instanceof FormulaError) return rawVal;
3609
+ const num = toNumber(rawVal);
3610
+ if (num instanceof FormulaError) return num;
3611
+ const n = Math.trunc(num);
3612
+ if (n < 0) return new FormulaError("#NUM!", "FACT: argument must be non-negative");
3613
+ if (n > 170) return new FormulaError("#NUM!", "FACT: argument too large (>170)");
3614
+ let result = 1;
3615
+ for (let i = 2; i <= n; i++) {
3616
+ result *= i;
3617
+ }
3618
+ return result;
3619
+ }
3620
+ });
3621
+ registry.set("GCD", {
3622
+ minArgs: 1,
3623
+ maxArgs: -1,
3624
+ evaluate(args, context, evaluator) {
3625
+ const nums = [];
3626
+ for (const arg of args) {
3627
+ const rawVal = evaluator.evaluate(arg, context);
3628
+ if (rawVal instanceof FormulaError) return rawVal;
3629
+ const v = toNumber(rawVal);
3630
+ if (v instanceof FormulaError) return v;
3631
+ const n = Math.trunc(Math.abs(v));
3632
+ nums.push(n);
3633
+ }
3634
+ if (nums.length === 0) return new FormulaError("#NUM!", "GCD: no arguments");
3635
+ let result = nums[0];
3636
+ for (let i = 1; i < nums.length; i++) {
3637
+ result = gcdTwo(result, nums[i]);
3638
+ }
3639
+ return result;
3640
+ }
3641
+ });
3642
+ registry.set("LCM", {
3643
+ minArgs: 1,
3644
+ maxArgs: -1,
3645
+ evaluate(args, context, evaluator) {
3646
+ const nums = [];
3647
+ for (const arg of args) {
3648
+ const rawVal = evaluator.evaluate(arg, context);
3649
+ if (rawVal instanceof FormulaError) return rawVal;
3650
+ const v = toNumber(rawVal);
3651
+ if (v instanceof FormulaError) return v;
3652
+ const n = Math.trunc(Math.abs(v));
3653
+ nums.push(n);
3654
+ }
3655
+ if (nums.length === 0) return new FormulaError("#NUM!", "LCM: no arguments");
3656
+ let result = nums[0];
3657
+ for (let i = 1; i < nums.length; i++) {
3658
+ const g = gcdTwo(result, nums[i]);
3659
+ if (g === 0) {
3660
+ result = 0;
3661
+ break;
3662
+ }
3663
+ result = result / g * nums[i];
3664
+ }
3665
+ return result;
3666
+ }
3667
+ });
3668
+ }
3669
+ function gcdTwo(a, b) {
3670
+ while (b !== 0) {
3671
+ const t = b;
3672
+ b = a % b;
3673
+ a = t;
3674
+ }
3675
+ return a;
3371
3676
  }
3372
3677
  function flattenArgs2(args, context, evaluator) {
3373
3678
  const result = [];
@@ -4202,48 +4507,194 @@ function registerTextFunctions(registry) {
4202
4507
  return parts.join(delimiter);
4203
4508
  }
4204
4509
  });
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", {
4510
+ registry.set("DOLLAR", {
4241
4511
  minArgs: 1,
4242
- maxArgs: 1,
4512
+ maxArgs: 2,
4243
4513
  evaluate(args, context, evaluator) {
4244
- const val = evaluator.evaluate(args[0], context);
4245
- if (val instanceof FormulaError) return val;
4246
- const date = toDate(val);
4514
+ const rawNum = evaluator.evaluate(args[0], context);
4515
+ if (rawNum instanceof FormulaError) return rawNum;
4516
+ const num = toNumber(rawNum);
4517
+ if (num instanceof FormulaError) return num;
4518
+ let decimals = 2;
4519
+ if (args.length >= 2) {
4520
+ const rawDec = evaluator.evaluate(args[1], context);
4521
+ if (rawDec instanceof FormulaError) return rawDec;
4522
+ const d = toNumber(rawDec);
4523
+ if (d instanceof FormulaError) return d;
4524
+ decimals = Math.trunc(d);
4525
+ }
4526
+ const absNum = Math.abs(num);
4527
+ const rounded = decimals >= 0 ? absNum.toFixed(decimals) : (Math.round(absNum / Math.pow(10, -decimals)) * Math.pow(10, -decimals)).toFixed(0);
4528
+ const [intPart, decPart] = rounded.split(".");
4529
+ const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
4530
+ const formatted = decPart !== void 0 ? `${withCommas}.${decPart}` : withCommas;
4531
+ return num < 0 ? `($${formatted})` : `$${formatted}`;
4532
+ }
4533
+ });
4534
+ registry.set("FIXED", {
4535
+ minArgs: 1,
4536
+ maxArgs: 3,
4537
+ evaluate(args, context, evaluator) {
4538
+ const rawNum = evaluator.evaluate(args[0], context);
4539
+ if (rawNum instanceof FormulaError) return rawNum;
4540
+ const num = toNumber(rawNum);
4541
+ if (num instanceof FormulaError) return num;
4542
+ let decimals = 2;
4543
+ if (args.length >= 2) {
4544
+ const rawDec = evaluator.evaluate(args[1], context);
4545
+ if (rawDec instanceof FormulaError) return rawDec;
4546
+ const d = toNumber(rawDec);
4547
+ if (d instanceof FormulaError) return d;
4548
+ decimals = Math.trunc(d);
4549
+ }
4550
+ let noCommas = false;
4551
+ if (args.length >= 3) {
4552
+ const rawNoCommas = evaluator.evaluate(args[2], context);
4553
+ if (rawNoCommas instanceof FormulaError) return rawNoCommas;
4554
+ noCommas = !!rawNoCommas;
4555
+ }
4556
+ const absNum = Math.abs(num);
4557
+ const rounded = decimals >= 0 ? absNum.toFixed(decimals) : (Math.round(absNum / Math.pow(10, -decimals)) * Math.pow(10, -decimals)).toFixed(0);
4558
+ if (noCommas) {
4559
+ return num < 0 ? `-${rounded}` : rounded;
4560
+ }
4561
+ const [intPart, decPart] = rounded.split(".");
4562
+ const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
4563
+ const formatted = decPart !== void 0 ? `${withCommas}.${decPart}` : withCommas;
4564
+ return num < 0 ? `-${formatted}` : formatted;
4565
+ }
4566
+ });
4567
+ registry.set("T", {
4568
+ minArgs: 1,
4569
+ maxArgs: 1,
4570
+ evaluate(args, context, evaluator) {
4571
+ const val = evaluator.evaluate(args[0], context);
4572
+ if (val instanceof FormulaError) return val;
4573
+ return typeof val === "string" ? val : "";
4574
+ }
4575
+ });
4576
+ registry.set("N", {
4577
+ minArgs: 1,
4578
+ maxArgs: 1,
4579
+ evaluate(args, context, evaluator) {
4580
+ const val = evaluator.evaluate(args[0], context);
4581
+ if (val instanceof FormulaError) return val;
4582
+ if (typeof val === "number") return val;
4583
+ if (typeof val === "boolean") return val ? 1 : 0;
4584
+ if (val instanceof Date) return val.getTime();
4585
+ return 0;
4586
+ }
4587
+ });
4588
+ registry.set("FORMULATEXT", {
4589
+ minArgs: 1,
4590
+ maxArgs: 1,
4591
+ evaluate(args, context, _evaluator) {
4592
+ const arg = args[0];
4593
+ if (arg.kind !== "cellRef") {
4594
+ return new FormulaError("#N/A", "FORMULATEXT requires a cell reference");
4595
+ }
4596
+ if (!context.getCellFormula) {
4597
+ return new FormulaError("#N/A", "FORMULATEXT not supported in this context");
4598
+ }
4599
+ const formula = context.getCellFormula(arg.address);
4600
+ if (formula === void 0) {
4601
+ return new FormulaError("#N/A", "Cell does not contain a formula");
4602
+ }
4603
+ return formula;
4604
+ }
4605
+ });
4606
+ registry.set("NUMBERVALUE", {
4607
+ minArgs: 1,
4608
+ maxArgs: 3,
4609
+ evaluate(args, context, evaluator) {
4610
+ const rawText = evaluator.evaluate(args[0], context);
4611
+ if (rawText instanceof FormulaError) return rawText;
4612
+ if (typeof rawText === "number") return rawText;
4613
+ let text = toString(rawText).trim();
4614
+ let decimalSep = ".";
4615
+ let groupSep = ",";
4616
+ const hasDecimalArg = args.length >= 2;
4617
+ const hasGroupArg = args.length >= 3;
4618
+ if (hasDecimalArg) {
4619
+ const rawDec = evaluator.evaluate(args[1], context);
4620
+ if (rawDec instanceof FormulaError) return rawDec;
4621
+ decimalSep = toString(rawDec);
4622
+ if (decimalSep.length !== 1) return new FormulaError("#VALUE!", "NUMBERVALUE decimal separator must be 1 character");
4623
+ if (!hasGroupArg) {
4624
+ groupSep = decimalSep === "," ? "." : ",";
4625
+ }
4626
+ }
4627
+ if (hasGroupArg) {
4628
+ const rawGrp = evaluator.evaluate(args[2], context);
4629
+ if (rawGrp instanceof FormulaError) return rawGrp;
4630
+ groupSep = toString(rawGrp);
4631
+ if (groupSep.length !== 1) return new FormulaError("#VALUE!", "NUMBERVALUE group separator must be 1 character");
4632
+ }
4633
+ if (decimalSep === groupSep) return new FormulaError("#VALUE!", "NUMBERVALUE separators must be different");
4634
+ const isPercent = text.endsWith("%");
4635
+ if (isPercent) text = text.slice(0, -1).trim();
4636
+ const escapedGroup = groupSep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4637
+ text = text.replace(new RegExp(escapedGroup, "g"), "");
4638
+ if (decimalSep !== ".") {
4639
+ const escapedDec = decimalSep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4640
+ text = text.replace(new RegExp(escapedDec), ".");
4641
+ }
4642
+ const n = Number(text);
4643
+ if (isNaN(n)) return new FormulaError("#VALUE!", `NUMBERVALUE cannot parse "${toString(rawText)}"`);
4644
+ return isPercent ? n / 100 : n;
4645
+ }
4646
+ });
4647
+ registry.set("PHONETIC", {
4648
+ minArgs: 1,
4649
+ maxArgs: 1,
4650
+ evaluate(args, context, evaluator) {
4651
+ const val = evaluator.evaluate(args[0], context);
4652
+ if (val instanceof FormulaError) return val;
4653
+ return toString(val);
4654
+ }
4655
+ });
4656
+ }
4657
+ function toDate(val) {
4658
+ if (val instanceof FormulaError) return val;
4659
+ if (val instanceof Date) {
4660
+ if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
4661
+ return val;
4662
+ }
4663
+ if (typeof val === "string") {
4664
+ const d = new Date(val);
4665
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
4666
+ return d;
4667
+ }
4668
+ if (typeof val === "number") {
4669
+ const d = new Date(val);
4670
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
4671
+ return d;
4672
+ }
4673
+ return new FormulaError("#VALUE!", "Cannot convert value to date");
4674
+ }
4675
+ function registerDateFunctions(registry) {
4676
+ registry.set("TODAY", {
4677
+ minArgs: 0,
4678
+ maxArgs: 0,
4679
+ evaluate(_args, context) {
4680
+ const now = context.now();
4681
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
4682
+ }
4683
+ });
4684
+ registry.set("NOW", {
4685
+ minArgs: 0,
4686
+ maxArgs: 0,
4687
+ evaluate(_args, context) {
4688
+ return context.now();
4689
+ }
4690
+ });
4691
+ registry.set("YEAR", {
4692
+ minArgs: 1,
4693
+ maxArgs: 1,
4694
+ evaluate(args, context, evaluator) {
4695
+ const val = evaluator.evaluate(args[0], context);
4696
+ if (val instanceof FormulaError) return val;
4697
+ const date = toDate(val);
4247
4698
  if (date instanceof FormulaError) return date;
4248
4699
  return date.getFullYear();
4249
4700
  }
@@ -4449,6 +4900,311 @@ function registerDateFunctions(registry) {
4449
4900
  return count * sign;
4450
4901
  }
4451
4902
  });
4903
+ registry.set("DAYS", {
4904
+ minArgs: 2,
4905
+ maxArgs: 2,
4906
+ evaluate(args, context, evaluator) {
4907
+ const rawEnd = evaluator.evaluate(args[0], context);
4908
+ if (rawEnd instanceof FormulaError) return rawEnd;
4909
+ const endDate = toDate(rawEnd);
4910
+ if (endDate instanceof FormulaError) return endDate;
4911
+ const rawStart = evaluator.evaluate(args[1], context);
4912
+ if (rawStart instanceof FormulaError) return rawStart;
4913
+ const startDate = toDate(rawStart);
4914
+ if (startDate instanceof FormulaError) return startDate;
4915
+ const endMs = Date.UTC(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
4916
+ const startMs = Date.UTC(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
4917
+ return Math.round((endMs - startMs) / 864e5);
4918
+ }
4919
+ });
4920
+ registry.set("DAYS360", {
4921
+ minArgs: 2,
4922
+ maxArgs: 3,
4923
+ evaluate(args, context, evaluator) {
4924
+ const rawStart = evaluator.evaluate(args[0], context);
4925
+ if (rawStart instanceof FormulaError) return rawStart;
4926
+ const startDate = toDate(rawStart);
4927
+ if (startDate instanceof FormulaError) return startDate;
4928
+ const rawEnd = evaluator.evaluate(args[1], context);
4929
+ if (rawEnd instanceof FormulaError) return rawEnd;
4930
+ const endDate = toDate(rawEnd);
4931
+ if (endDate instanceof FormulaError) return endDate;
4932
+ let method = false;
4933
+ if (args.length >= 3) {
4934
+ const rawMethod = evaluator.evaluate(args[2], context);
4935
+ if (rawMethod instanceof FormulaError) return rawMethod;
4936
+ method = !!rawMethod;
4937
+ }
4938
+ const sm = startDate.getMonth() + 1;
4939
+ const em = endDate.getMonth() + 1;
4940
+ let sd = startDate.getDate();
4941
+ let ed = endDate.getDate();
4942
+ const sy = startDate.getFullYear();
4943
+ const ey = endDate.getFullYear();
4944
+ if (!method) {
4945
+ if (sd === 31) sd = 30;
4946
+ if (ed === 31 && sd === 30) ed = 30;
4947
+ } else {
4948
+ if (sd === 31) sd = 30;
4949
+ if (ed === 31) ed = 30;
4950
+ }
4951
+ return (ey - sy) * 360 + (em - sm) * 30 + (ed - sd);
4952
+ }
4953
+ });
4954
+ registry.set("ISOWEEKNUM", {
4955
+ minArgs: 1,
4956
+ maxArgs: 1,
4957
+ evaluate(args, context, evaluator) {
4958
+ const rawDate = evaluator.evaluate(args[0], context);
4959
+ if (rawDate instanceof FormulaError) return rawDate;
4960
+ const date = toDate(rawDate);
4961
+ if (date instanceof FormulaError) return date;
4962
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
4963
+ const day = d.getUTCDay() || 7;
4964
+ d.setUTCDate(d.getUTCDate() + 4 - day);
4965
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
4966
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
4967
+ }
4968
+ });
4969
+ registry.set("YEARFRAC", {
4970
+ minArgs: 2,
4971
+ maxArgs: 3,
4972
+ evaluate(args, context, evaluator) {
4973
+ const rawStart = evaluator.evaluate(args[0], context);
4974
+ if (rawStart instanceof FormulaError) return rawStart;
4975
+ const startDate = toDate(rawStart);
4976
+ if (startDate instanceof FormulaError) return startDate;
4977
+ const rawEnd = evaluator.evaluate(args[1], context);
4978
+ if (rawEnd instanceof FormulaError) return rawEnd;
4979
+ const endDate = toDate(rawEnd);
4980
+ if (endDate instanceof FormulaError) return endDate;
4981
+ let basis = 0;
4982
+ if (args.length >= 3) {
4983
+ const rawBasis = evaluator.evaluate(args[2], context);
4984
+ if (rawBasis instanceof FormulaError) return rawBasis;
4985
+ const b = toNumber(rawBasis);
4986
+ if (b instanceof FormulaError) return b;
4987
+ basis = Math.trunc(b);
4988
+ }
4989
+ const sy = startDate.getFullYear();
4990
+ const sm = startDate.getMonth() + 1;
4991
+ const sd = startDate.getDate();
4992
+ const ey = endDate.getFullYear();
4993
+ const em = endDate.getMonth() + 1;
4994
+ const ed = endDate.getDate();
4995
+ switch (basis) {
4996
+ case 0: {
4997
+ const startDay = sd === 31 ? 30 : sd;
4998
+ const endDay = ed === 31 && startDay === 30 ? 30 : ed;
4999
+ const days360 = (ey - sy) * 360 + (em - sm) * 30 + (endDay - startDay);
5000
+ return days360 / 360;
5001
+ }
5002
+ case 1: {
5003
+ const diffMs = Date.UTC(ey, em - 1, ed) - Date.UTC(sy, sm - 1, sd);
5004
+ const diffDays = diffMs / 864e5;
5005
+ const avgYear = ey === sy ? isLeapYear(sy) ? 366 : 365 : (Date.UTC(ey + 1, 0, 1) - Date.UTC(sy, 0, 1)) / 864e5 / (ey - sy + 1);
5006
+ return diffDays / avgYear;
5007
+ }
5008
+ case 3: {
5009
+ const diffMs = Date.UTC(ey, em - 1, ed) - Date.UTC(sy, sm - 1, sd);
5010
+ return diffMs / 864e5 / 365;
5011
+ }
5012
+ default:
5013
+ return new FormulaError("#VALUE!", "YEARFRAC basis must be 0, 1, or 3");
5014
+ }
5015
+ }
5016
+ });
5017
+ registry.set("DATEVALUE", {
5018
+ minArgs: 1,
5019
+ maxArgs: 1,
5020
+ evaluate(args, context, evaluator) {
5021
+ const rawVal = evaluator.evaluate(args[0], context);
5022
+ if (rawVal instanceof FormulaError) return rawVal;
5023
+ const str = typeof rawVal === "string" ? rawVal : String(rawVal);
5024
+ const d = new Date(str);
5025
+ if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `DATEVALUE cannot parse "${str}"`);
5026
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
5027
+ }
5028
+ });
5029
+ registry.set("TIMEVALUE", {
5030
+ minArgs: 1,
5031
+ maxArgs: 1,
5032
+ evaluate(args, context, evaluator) {
5033
+ const rawVal = evaluator.evaluate(args[0], context);
5034
+ if (rawVal instanceof FormulaError) return rawVal;
5035
+ const str = typeof rawVal === "string" ? rawVal : String(rawVal);
5036
+ const match = str.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*(AM|PM))?$/i);
5037
+ if (!match) return new FormulaError("#VALUE!", `TIMEVALUE cannot parse "${str}"`);
5038
+ let hours = parseInt(match[1], 10);
5039
+ const minutes = parseInt(match[2], 10);
5040
+ const seconds = match[3] ? parseInt(match[3], 10) : 0;
5041
+ const ampm = match[4] ? match[4].toUpperCase() : null;
5042
+ if (ampm === "PM" && hours < 12) hours += 12;
5043
+ if (ampm === "AM" && hours === 12) hours = 0;
5044
+ if (hours > 23 || minutes > 59 || seconds > 59) {
5045
+ return new FormulaError("#VALUE!", "TIMEVALUE: invalid time component");
5046
+ }
5047
+ return (hours * 3600 + minutes * 60 + seconds) / 86400;
5048
+ }
5049
+ });
5050
+ registry.set("TIME", {
5051
+ minArgs: 3,
5052
+ maxArgs: 3,
5053
+ evaluate(args, context, evaluator) {
5054
+ const rawH = evaluator.evaluate(args[0], context);
5055
+ if (rawH instanceof FormulaError) return rawH;
5056
+ const h = toNumber(rawH);
5057
+ if (h instanceof FormulaError) return h;
5058
+ const rawM = evaluator.evaluate(args[1], context);
5059
+ if (rawM instanceof FormulaError) return rawM;
5060
+ const m = toNumber(rawM);
5061
+ if (m instanceof FormulaError) return m;
5062
+ const rawS = evaluator.evaluate(args[2], context);
5063
+ if (rawS instanceof FormulaError) return rawS;
5064
+ const s = toNumber(rawS);
5065
+ if (s instanceof FormulaError) return s;
5066
+ const totalSeconds = Math.trunc(h) * 3600 + Math.trunc(m) * 60 + Math.trunc(s);
5067
+ return totalSeconds % 86400 / 86400;
5068
+ }
5069
+ });
5070
+ registry.set("WORKDAY", {
5071
+ minArgs: 2,
5072
+ maxArgs: 3,
5073
+ evaluate(args, context, evaluator) {
5074
+ const rawStart = evaluator.evaluate(args[0], context);
5075
+ if (rawStart instanceof FormulaError) return rawStart;
5076
+ const startDate = toDate(rawStart);
5077
+ if (startDate instanceof FormulaError) return startDate;
5078
+ const rawDays = evaluator.evaluate(args[1], context);
5079
+ if (rawDays instanceof FormulaError) return rawDays;
5080
+ const daysNum = toNumber(rawDays);
5081
+ if (daysNum instanceof FormulaError) return daysNum;
5082
+ const days = Math.trunc(daysNum);
5083
+ const holidaySet = /* @__PURE__ */ new Set();
5084
+ if (args.length >= 3) {
5085
+ const rawHol = args[2];
5086
+ let holVals;
5087
+ if (rawHol.kind === "range") {
5088
+ holVals = context.getRangeValues({ start: rawHol.start, end: rawHol.end }).flat();
5089
+ } else {
5090
+ holVals = [evaluator.evaluate(rawHol, context)];
5091
+ }
5092
+ for (const hv of holVals) {
5093
+ if (hv === null || hv === void 0) continue;
5094
+ const hd = toDate(hv);
5095
+ if (!(hd instanceof FormulaError)) {
5096
+ holidaySet.add(`${hd.getFullYear()}-${hd.getMonth()}-${hd.getDate()}`);
5097
+ }
5098
+ }
5099
+ }
5100
+ const current = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
5101
+ const step = days >= 0 ? 1 : -1;
5102
+ let remaining = Math.abs(days);
5103
+ while (remaining > 0) {
5104
+ current.setDate(current.getDate() + step);
5105
+ const dow = current.getDay();
5106
+ if (dow === 0 || dow === 6) continue;
5107
+ const key = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}`;
5108
+ if (holidaySet.has(key)) continue;
5109
+ remaining--;
5110
+ }
5111
+ return current;
5112
+ }
5113
+ });
5114
+ registry.set("WORKDAY.INTL", {
5115
+ minArgs: 2,
5116
+ maxArgs: 4,
5117
+ evaluate(args, context, evaluator) {
5118
+ const rawStart = evaluator.evaluate(args[0], context);
5119
+ if (rawStart instanceof FormulaError) return rawStart;
5120
+ const startDate = toDate(rawStart);
5121
+ if (startDate instanceof FormulaError) return startDate;
5122
+ const rawDays = evaluator.evaluate(args[1], context);
5123
+ if (rawDays instanceof FormulaError) return rawDays;
5124
+ const daysNum = toNumber(rawDays);
5125
+ if (daysNum instanceof FormulaError) return daysNum;
5126
+ const days = Math.trunc(daysNum);
5127
+ let weekendMask = [false, false, false, false, false, true, true];
5128
+ if (args.length >= 3) {
5129
+ const rawWeekend = evaluator.evaluate(args[2], context);
5130
+ if (rawWeekend instanceof FormulaError) return rawWeekend;
5131
+ if (typeof rawWeekend === "string" && /^[01]{7}$/.test(rawWeekend)) {
5132
+ weekendMask = rawWeekend.split("").map((c) => c === "1");
5133
+ } else {
5134
+ const wn = toNumber(rawWeekend);
5135
+ if (wn instanceof FormulaError) return wn;
5136
+ const parsed = parseWeekendNumber(Math.trunc(wn));
5137
+ if (!parsed) return new FormulaError("#VALUE!", "WORKDAY.INTL invalid weekend number");
5138
+ weekendMask = parsed;
5139
+ }
5140
+ }
5141
+ const holidaySet = /* @__PURE__ */ new Set();
5142
+ if (args.length >= 4) {
5143
+ const rawHol = args[3];
5144
+ let holVals;
5145
+ if (rawHol.kind === "range") {
5146
+ holVals = context.getRangeValues({ start: rawHol.start, end: rawHol.end }).flat();
5147
+ } else {
5148
+ holVals = [evaluator.evaluate(rawHol, context)];
5149
+ }
5150
+ for (const hv of holVals) {
5151
+ if (hv === null || hv === void 0) continue;
5152
+ const hd = toDate(hv);
5153
+ if (!(hd instanceof FormulaError)) {
5154
+ holidaySet.add(`${hd.getFullYear()}-${hd.getMonth()}-${hd.getDate()}`);
5155
+ }
5156
+ }
5157
+ }
5158
+ const current = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
5159
+ const step = days >= 0 ? 1 : -1;
5160
+ let remaining = Math.abs(days);
5161
+ while (remaining > 0) {
5162
+ current.setDate(current.getDate() + step);
5163
+ const dow = current.getDay();
5164
+ const maskIndex = dow === 0 ? 6 : dow - 1;
5165
+ if (weekendMask[maskIndex]) continue;
5166
+ const key = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}`;
5167
+ if (holidaySet.has(key)) continue;
5168
+ remaining--;
5169
+ }
5170
+ return current;
5171
+ }
5172
+ });
5173
+ }
5174
+ function isLeapYear(year) {
5175
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
5176
+ }
5177
+ function parseWeekendNumber(n) {
5178
+ const twoDay = [
5179
+ [5, 6],
5180
+ // 1: Sat+Sun
5181
+ [6, 0],
5182
+ // 2: Sun+Mon (Sun=index6, Mon=index0)
5183
+ [0, 1],
5184
+ // 3: Mon+Tue
5185
+ [1, 2],
5186
+ // 4: Tue+Wed
5187
+ [2, 3],
5188
+ // 5: Wed+Thu
5189
+ [3, 4],
5190
+ // 6: Thu+Fri
5191
+ [4, 5]
5192
+ // 7: Fri+Sat
5193
+ ];
5194
+ if (n >= 1 && n <= 7) {
5195
+ const mask = [false, false, false, false, false, false, false];
5196
+ const [a, b] = twoDay[n - 1];
5197
+ mask[a] = true;
5198
+ mask[b] = true;
5199
+ return mask;
5200
+ }
5201
+ if (n >= 11 && n <= 17) {
5202
+ const mask = [false, false, false, false, false, false, false];
5203
+ const singleDay = [6, 0, 1, 2, 3, 4, 5];
5204
+ mask[singleDay[n - 11]] = true;
5205
+ return mask;
5206
+ }
5207
+ return null;
4452
5208
  }
4453
5209
  function makeCriteria(op, value) {
4454
5210
  return { op, value, valueLower: typeof value === "string" ? value.toLowerCase() : null };
@@ -4763,52 +5519,1092 @@ function registerInfoFunctions(registry) {
4763
5519
  return val === null || val === void 0 || val === "";
4764
5520
  }
4765
5521
  });
4766
- registry.set("ISNUMBER", {
5522
+ registry.set("ISNUMBER", {
5523
+ minArgs: 1,
5524
+ maxArgs: 1,
5525
+ evaluate(args, context, evaluator) {
5526
+ const val = evaluator.evaluate(args[0], context);
5527
+ return typeof val === "number" && !isNaN(val);
5528
+ }
5529
+ });
5530
+ registry.set("ISTEXT", {
5531
+ minArgs: 1,
5532
+ maxArgs: 1,
5533
+ evaluate(args, context, evaluator) {
5534
+ const val = evaluator.evaluate(args[0], context);
5535
+ return typeof val === "string";
5536
+ }
5537
+ });
5538
+ registry.set("ISERROR", {
5539
+ minArgs: 1,
5540
+ maxArgs: 1,
5541
+ evaluate(args, context, evaluator) {
5542
+ const val = evaluator.evaluate(args[0], context);
5543
+ return val instanceof FormulaError;
5544
+ }
5545
+ });
5546
+ registry.set("ISNA", {
5547
+ minArgs: 1,
5548
+ maxArgs: 1,
5549
+ evaluate(args, context, evaluator) {
5550
+ const val = evaluator.evaluate(args[0], context);
5551
+ return val instanceof FormulaError && val.type === "#N/A";
5552
+ }
5553
+ });
5554
+ registry.set("TYPE", {
5555
+ minArgs: 1,
5556
+ maxArgs: 1,
5557
+ evaluate(args, context, evaluator) {
5558
+ const val = evaluator.evaluate(args[0], context);
5559
+ if (val instanceof FormulaError) return 16;
5560
+ if (typeof val === "number") return 1;
5561
+ if (typeof val === "string") return 2;
5562
+ if (typeof val === "boolean") return 4;
5563
+ if (val === null || val === void 0) return 1;
5564
+ return 1;
5565
+ }
5566
+ });
5567
+ registry.set("ISODD", {
5568
+ minArgs: 1,
5569
+ maxArgs: 1,
5570
+ evaluate(args, context, evaluator) {
5571
+ const val = evaluator.evaluate(args[0], context);
5572
+ if (val instanceof FormulaError) return val;
5573
+ if (typeof val === "boolean") return new FormulaError("#VALUE!", "ISODD requires a number");
5574
+ const n = toNumber(val);
5575
+ if (n instanceof FormulaError) return n;
5576
+ return Math.trunc(n) % 2 !== 0;
5577
+ }
5578
+ });
5579
+ registry.set("ISEVEN", {
5580
+ minArgs: 1,
5581
+ maxArgs: 1,
5582
+ evaluate(args, context, evaluator) {
5583
+ const val = evaluator.evaluate(args[0], context);
5584
+ if (val instanceof FormulaError) return val;
5585
+ if (typeof val === "boolean") return new FormulaError("#VALUE!", "ISEVEN requires a number");
5586
+ const n = toNumber(val);
5587
+ if (n instanceof FormulaError) return n;
5588
+ return Math.trunc(n) % 2 === 0;
5589
+ }
5590
+ });
5591
+ registry.set("ISFORMULA", {
5592
+ minArgs: 1,
5593
+ maxArgs: 1,
5594
+ evaluate(args, context, _evaluator) {
5595
+ const arg = args[0];
5596
+ if (arg.kind !== "cellRef") return false;
5597
+ if (!context.getCellFormula) return false;
5598
+ const formula = context.getCellFormula(arg.address);
5599
+ return formula !== void 0;
5600
+ }
5601
+ });
5602
+ registry.set("ISLOGICAL", {
5603
+ minArgs: 1,
5604
+ maxArgs: 1,
5605
+ evaluate(args, context, evaluator) {
5606
+ const val = evaluator.evaluate(args[0], context);
5607
+ if (val instanceof FormulaError) return false;
5608
+ return typeof val === "boolean";
5609
+ }
5610
+ });
5611
+ registry.set("ISNONTEXT", {
5612
+ minArgs: 1,
5613
+ maxArgs: 1,
5614
+ evaluate(args, context, evaluator) {
5615
+ const val = evaluator.evaluate(args[0], context);
5616
+ if (val instanceof FormulaError) return true;
5617
+ return typeof val !== "string";
5618
+ }
5619
+ });
5620
+ registry.set("ISREF", {
5621
+ minArgs: 1,
5622
+ maxArgs: 1,
5623
+ evaluate(args, _context, _evaluator) {
5624
+ const arg = args[0];
5625
+ return arg.kind === "cellRef" || arg.kind === "range";
5626
+ }
5627
+ });
5628
+ }
5629
+ function registerFinancialFunctions(registry) {
5630
+ registry.set("PMT", {
5631
+ minArgs: 3,
5632
+ maxArgs: 5,
5633
+ evaluate(args, context, evaluator) {
5634
+ const rawRate = evaluator.evaluate(args[0], context);
5635
+ if (rawRate instanceof FormulaError) return rawRate;
5636
+ const rate = toNumber(rawRate);
5637
+ if (rate instanceof FormulaError) return rate;
5638
+ const rawNper = evaluator.evaluate(args[1], context);
5639
+ if (rawNper instanceof FormulaError) return rawNper;
5640
+ const nper = toNumber(rawNper);
5641
+ if (nper instanceof FormulaError) return nper;
5642
+ const rawPv = evaluator.evaluate(args[2], context);
5643
+ if (rawPv instanceof FormulaError) return rawPv;
5644
+ const pv = toNumber(rawPv);
5645
+ if (pv instanceof FormulaError) return pv;
5646
+ let fv = 0;
5647
+ if (args.length >= 4) {
5648
+ const rawFv = evaluator.evaluate(args[3], context);
5649
+ if (rawFv instanceof FormulaError) return rawFv;
5650
+ const fvNum = toNumber(rawFv);
5651
+ if (fvNum instanceof FormulaError) return fvNum;
5652
+ fv = fvNum;
5653
+ }
5654
+ let type = 0;
5655
+ if (args.length >= 5) {
5656
+ const rawType = evaluator.evaluate(args[4], context);
5657
+ if (rawType instanceof FormulaError) return rawType;
5658
+ const typeNum = toNumber(rawType);
5659
+ if (typeNum instanceof FormulaError) return typeNum;
5660
+ type = typeNum;
5661
+ }
5662
+ if (nper === 0) return new FormulaError("#NUM!", "PMT: nper cannot be 0");
5663
+ if (rate === 0) {
5664
+ return -(pv + fv) / nper;
5665
+ }
5666
+ const factor = Math.pow(1 + rate, nper);
5667
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5668
+ return -(pv * factor + fv) / ((factor - 1) / rate * typeAdj);
5669
+ }
5670
+ });
5671
+ registry.set("FV", {
5672
+ minArgs: 3,
5673
+ maxArgs: 5,
5674
+ evaluate(args, context, evaluator) {
5675
+ const rawRate = evaluator.evaluate(args[0], context);
5676
+ if (rawRate instanceof FormulaError) return rawRate;
5677
+ const rate = toNumber(rawRate);
5678
+ if (rate instanceof FormulaError) return rate;
5679
+ const rawNper = evaluator.evaluate(args[1], context);
5680
+ if (rawNper instanceof FormulaError) return rawNper;
5681
+ const nper = toNumber(rawNper);
5682
+ if (nper instanceof FormulaError) return nper;
5683
+ const rawPmt = evaluator.evaluate(args[2], context);
5684
+ if (rawPmt instanceof FormulaError) return rawPmt;
5685
+ const pmt = toNumber(rawPmt);
5686
+ if (pmt instanceof FormulaError) return pmt;
5687
+ let pv = 0;
5688
+ if (args.length >= 4) {
5689
+ const rawPv = evaluator.evaluate(args[3], context);
5690
+ if (rawPv instanceof FormulaError) return rawPv;
5691
+ const pvNum = toNumber(rawPv);
5692
+ if (pvNum instanceof FormulaError) return pvNum;
5693
+ pv = pvNum;
5694
+ }
5695
+ let type = 0;
5696
+ if (args.length >= 5) {
5697
+ const rawType = evaluator.evaluate(args[4], context);
5698
+ if (rawType instanceof FormulaError) return rawType;
5699
+ const typeNum = toNumber(rawType);
5700
+ if (typeNum instanceof FormulaError) return typeNum;
5701
+ type = typeNum;
5702
+ }
5703
+ if (rate === 0) {
5704
+ return -(pv + pmt * nper);
5705
+ }
5706
+ const factor = Math.pow(1 + rate, nper);
5707
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5708
+ return -(pv * factor + pmt * typeAdj * (factor - 1) / rate);
5709
+ }
5710
+ });
5711
+ registry.set("PV", {
5712
+ minArgs: 3,
5713
+ maxArgs: 5,
5714
+ evaluate(args, context, evaluator) {
5715
+ const rawRate = evaluator.evaluate(args[0], context);
5716
+ if (rawRate instanceof FormulaError) return rawRate;
5717
+ const rate = toNumber(rawRate);
5718
+ if (rate instanceof FormulaError) return rate;
5719
+ const rawNper = evaluator.evaluate(args[1], context);
5720
+ if (rawNper instanceof FormulaError) return rawNper;
5721
+ const nper = toNumber(rawNper);
5722
+ if (nper instanceof FormulaError) return nper;
5723
+ const rawPmt = evaluator.evaluate(args[2], context);
5724
+ if (rawPmt instanceof FormulaError) return rawPmt;
5725
+ const pmt = toNumber(rawPmt);
5726
+ if (pmt instanceof FormulaError) return pmt;
5727
+ let fv = 0;
5728
+ if (args.length >= 4) {
5729
+ const rawFv = evaluator.evaluate(args[3], context);
5730
+ if (rawFv instanceof FormulaError) return rawFv;
5731
+ const fvNum = toNumber(rawFv);
5732
+ if (fvNum instanceof FormulaError) return fvNum;
5733
+ fv = fvNum;
5734
+ }
5735
+ let type = 0;
5736
+ if (args.length >= 5) {
5737
+ const rawType = evaluator.evaluate(args[4], context);
5738
+ if (rawType instanceof FormulaError) return rawType;
5739
+ const typeNum = toNumber(rawType);
5740
+ if (typeNum instanceof FormulaError) return typeNum;
5741
+ type = typeNum;
5742
+ }
5743
+ if (rate === 0) {
5744
+ return -pmt * nper - fv;
5745
+ }
5746
+ const factor = Math.pow(1 + rate, nper);
5747
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5748
+ return -(fv + pmt * typeAdj * (factor - 1) / rate) / factor;
5749
+ }
5750
+ });
5751
+ registry.set("NPER", {
5752
+ minArgs: 3,
5753
+ maxArgs: 5,
5754
+ evaluate(args, context, evaluator) {
5755
+ const rawRate = evaluator.evaluate(args[0], context);
5756
+ if (rawRate instanceof FormulaError) return rawRate;
5757
+ const rate = toNumber(rawRate);
5758
+ if (rate instanceof FormulaError) return rate;
5759
+ const rawPmt = evaluator.evaluate(args[1], context);
5760
+ if (rawPmt instanceof FormulaError) return rawPmt;
5761
+ const pmt = toNumber(rawPmt);
5762
+ if (pmt instanceof FormulaError) return pmt;
5763
+ const rawPv = evaluator.evaluate(args[2], context);
5764
+ if (rawPv instanceof FormulaError) return rawPv;
5765
+ const pv = toNumber(rawPv);
5766
+ if (pv instanceof FormulaError) return pv;
5767
+ let fv = 0;
5768
+ if (args.length >= 4) {
5769
+ const rawFv = evaluator.evaluate(args[3], context);
5770
+ if (rawFv instanceof FormulaError) return rawFv;
5771
+ const fvNum = toNumber(rawFv);
5772
+ if (fvNum instanceof FormulaError) return fvNum;
5773
+ fv = fvNum;
5774
+ }
5775
+ let type = 0;
5776
+ if (args.length >= 5) {
5777
+ const rawType = evaluator.evaluate(args[4], context);
5778
+ if (rawType instanceof FormulaError) return rawType;
5779
+ const typeNum = toNumber(rawType);
5780
+ if (typeNum instanceof FormulaError) return typeNum;
5781
+ type = typeNum;
5782
+ }
5783
+ if (rate === 0) {
5784
+ if (pmt === 0) return new FormulaError("#NUM!", "NPER: pmt cannot be 0 when rate is 0");
5785
+ return -(pv + fv) / pmt;
5786
+ }
5787
+ const typeAdj = type !== 0 ? 1 + rate : 1;
5788
+ const pmtAdj = pmt * typeAdj;
5789
+ const num = pmtAdj - fv * rate;
5790
+ const den = pmtAdj + pv * rate;
5791
+ if (den === 0) return new FormulaError("#DIV/0!", "NPER: division by zero");
5792
+ if (num / den <= 0) return new FormulaError("#NUM!", "NPER: logarithm of non-positive number");
5793
+ return Math.log(num / den) / Math.log(1 + rate);
5794
+ }
5795
+ });
5796
+ registry.set("RATE", {
5797
+ minArgs: 3,
5798
+ maxArgs: 6,
5799
+ evaluate(args, context, evaluator) {
5800
+ const rawNper = evaluator.evaluate(args[0], context);
5801
+ if (rawNper instanceof FormulaError) return rawNper;
5802
+ const nper = toNumber(rawNper);
5803
+ if (nper instanceof FormulaError) return nper;
5804
+ const rawPmt = evaluator.evaluate(args[1], context);
5805
+ if (rawPmt instanceof FormulaError) return rawPmt;
5806
+ const pmt = toNumber(rawPmt);
5807
+ if (pmt instanceof FormulaError) return pmt;
5808
+ const rawPv = evaluator.evaluate(args[2], context);
5809
+ if (rawPv instanceof FormulaError) return rawPv;
5810
+ const pv = toNumber(rawPv);
5811
+ if (pv instanceof FormulaError) return pv;
5812
+ let fv = 0;
5813
+ if (args.length >= 4) {
5814
+ const rawFv = evaluator.evaluate(args[3], context);
5815
+ if (rawFv instanceof FormulaError) return rawFv;
5816
+ const fvNum = toNumber(rawFv);
5817
+ if (fvNum instanceof FormulaError) return fvNum;
5818
+ fv = fvNum;
5819
+ }
5820
+ let type = 0;
5821
+ if (args.length >= 5) {
5822
+ const rawType = evaluator.evaluate(args[4], context);
5823
+ if (rawType instanceof FormulaError) return rawType;
5824
+ const typeNum = toNumber(rawType);
5825
+ if (typeNum instanceof FormulaError) return typeNum;
5826
+ type = typeNum;
5827
+ }
5828
+ let guess = 0.1;
5829
+ if (args.length >= 6) {
5830
+ const rawGuess = evaluator.evaluate(args[5], context);
5831
+ if (rawGuess instanceof FormulaError) return rawGuess;
5832
+ const guessNum = toNumber(rawGuess);
5833
+ if (guessNum instanceof FormulaError) return guessNum;
5834
+ guess = guessNum;
5835
+ }
5836
+ const MAX_ITER = 20;
5837
+ const TOLERANCE = 1e-7;
5838
+ let r = guess;
5839
+ for (let i = 0; i < MAX_ITER; i++) {
5840
+ if (r <= -1) return new FormulaError("#NUM!", "RATE: rate converged to invalid value");
5841
+ const factor = Math.pow(1 + r, nper);
5842
+ const typeAdj = type !== 0 ? 1 + r : 1;
5843
+ let f;
5844
+ let df;
5845
+ if (Math.abs(r) < 1e-10) {
5846
+ f = pv + pmt * nper + fv;
5847
+ df = pv * nper + pmt * nper * (nper - 1) / 2;
5848
+ } else {
5849
+ f = pv * factor + pmt * typeAdj * (factor - 1) / r + fv;
5850
+ const dfactor = nper * Math.pow(1 + r, nper - 1);
5851
+ const dTypeAdj = type !== 0 ? 1 : 0;
5852
+ df = pv * dfactor + pmt * (dTypeAdj * (factor - 1) / r + typeAdj * (dfactor * r - (factor - 1)) / (r * r));
5853
+ }
5854
+ if (Math.abs(df) < 1e-15) return new FormulaError("#NUM!", "RATE: derivative too small, no convergence");
5855
+ const delta = f / df;
5856
+ r = r - delta;
5857
+ if (Math.abs(delta) < TOLERANCE) return r;
5858
+ }
5859
+ return new FormulaError("#NUM!", "RATE: did not converge");
5860
+ }
5861
+ });
5862
+ registry.set("NPV", {
5863
+ minArgs: 2,
5864
+ maxArgs: -1,
5865
+ evaluate(args, context, evaluator) {
5866
+ const rawRate = evaluator.evaluate(args[0], context);
5867
+ if (rawRate instanceof FormulaError) return rawRate;
5868
+ const rate = toNumber(rawRate);
5869
+ if (rate instanceof FormulaError) return rate;
5870
+ const values = flattenArgs(args.slice(1), context, evaluator);
5871
+ let npv = 0;
5872
+ let period = 1;
5873
+ for (const val of values) {
5874
+ if (val instanceof FormulaError) return val;
5875
+ if (typeof val === "number" || typeof val === "boolean") {
5876
+ const n = typeof val === "boolean" ? val ? 1 : 0 : val;
5877
+ npv += n / Math.pow(1 + rate, period);
5878
+ period++;
5879
+ }
5880
+ }
5881
+ return npv;
5882
+ }
5883
+ });
5884
+ registry.set("IRR", {
5885
+ minArgs: 1,
5886
+ maxArgs: 2,
5887
+ evaluate(args, context, evaluator) {
5888
+ const cashFlows = flattenArgs([args[0]], context, evaluator);
5889
+ const nums = [];
5890
+ for (const val of cashFlows) {
5891
+ if (val instanceof FormulaError) return val;
5892
+ if (typeof val === "number") nums.push(val);
5893
+ else if (typeof val === "boolean") nums.push(val ? 1 : 0);
5894
+ }
5895
+ if (nums.length === 0) return new FormulaError("#NUM!", "IRR: no values");
5896
+ let guess = 0.1;
5897
+ if (args.length >= 2) {
5898
+ const rawGuess = evaluator.evaluate(args[1], context);
5899
+ if (rawGuess instanceof FormulaError) return rawGuess;
5900
+ const guessNum = toNumber(rawGuess);
5901
+ if (guessNum instanceof FormulaError) return guessNum;
5902
+ guess = guessNum;
5903
+ }
5904
+ const MAX_ITER = 20;
5905
+ const TOLERANCE = 1e-7;
5906
+ let r = guess;
5907
+ for (let i = 0; i < MAX_ITER; i++) {
5908
+ if (r <= -1) return new FormulaError("#NUM!", "IRR: rate converged below -1");
5909
+ let f = 0;
5910
+ let df = 0;
5911
+ for (let j = 0; j < nums.length; j++) {
5912
+ const factor = Math.pow(1 + r, j);
5913
+ f += nums[j] / factor;
5914
+ if (j > 0) {
5915
+ df -= j * nums[j] / Math.pow(1 + r, j + 1);
5916
+ }
5917
+ }
5918
+ if (Math.abs(df) < 1e-15) return new FormulaError("#NUM!", "IRR: derivative too small");
5919
+ const delta = f / df;
5920
+ r = r - delta;
5921
+ if (Math.abs(delta) < TOLERANCE) return r;
5922
+ }
5923
+ return new FormulaError("#NUM!", "IRR: did not converge");
5924
+ }
5925
+ });
5926
+ registry.set("SLN", {
5927
+ minArgs: 3,
5928
+ maxArgs: 3,
5929
+ evaluate(args, context, evaluator) {
5930
+ const rawCost = evaluator.evaluate(args[0], context);
5931
+ if (rawCost instanceof FormulaError) return rawCost;
5932
+ const cost = toNumber(rawCost);
5933
+ if (cost instanceof FormulaError) return cost;
5934
+ const rawSalvage = evaluator.evaluate(args[1], context);
5935
+ if (rawSalvage instanceof FormulaError) return rawSalvage;
5936
+ const salvage = toNumber(rawSalvage);
5937
+ if (salvage instanceof FormulaError) return salvage;
5938
+ const rawLife = evaluator.evaluate(args[2], context);
5939
+ if (rawLife instanceof FormulaError) return rawLife;
5940
+ const life = toNumber(rawLife);
5941
+ if (life instanceof FormulaError) return life;
5942
+ if (life === 0) return new FormulaError("#DIV/0!", "SLN: life cannot be 0");
5943
+ return (cost - salvage) / life;
5944
+ }
5945
+ });
5946
+ }
5947
+ function extractNums(args, context, evaluator) {
5948
+ const values = flattenArgs(args, context, evaluator);
5949
+ const nums = [];
5950
+ for (const val of values) {
5951
+ if (val instanceof FormulaError) return val;
5952
+ if (typeof val === "number") nums.push(val);
5953
+ else if (typeof val === "boolean") nums.push(val ? 1 : 0);
5954
+ }
5955
+ return nums;
5956
+ }
5957
+ function mean(nums) {
5958
+ let sum = 0;
5959
+ for (const n of nums) sum += n;
5960
+ return sum / nums.length;
5961
+ }
5962
+ function registerStatisticalExtendedFunctions(registry) {
5963
+ const stdevImpl = {
5964
+ minArgs: 1,
5965
+ maxArgs: -1,
5966
+ evaluate(args, context, evaluator) {
5967
+ const nums = extractNums(args, context, evaluator);
5968
+ if (nums instanceof FormulaError) return nums;
5969
+ if (nums.length < 2) return new FormulaError("#DIV/0!", "STDEV requires at least 2 values");
5970
+ const m = mean(nums);
5971
+ let sum = 0;
5972
+ for (const n of nums) sum += (n - m) * (n - m);
5973
+ return Math.sqrt(sum / (nums.length - 1));
5974
+ }
5975
+ };
5976
+ registry.set("STDEV", stdevImpl);
5977
+ registry.set("STDEV.S", stdevImpl);
5978
+ const stdevpImpl = {
5979
+ minArgs: 1,
5980
+ maxArgs: -1,
5981
+ evaluate(args, context, evaluator) {
5982
+ const nums = extractNums(args, context, evaluator);
5983
+ if (nums instanceof FormulaError) return nums;
5984
+ if (nums.length === 0) return new FormulaError("#DIV/0!", "STDEVP requires at least 1 value");
5985
+ const m = mean(nums);
5986
+ let sum = 0;
5987
+ for (const n of nums) sum += (n - m) * (n - m);
5988
+ return Math.sqrt(sum / nums.length);
5989
+ }
5990
+ };
5991
+ registry.set("STDEVP", stdevpImpl);
5992
+ registry.set("STDEV.P", stdevpImpl);
5993
+ const varImpl = {
5994
+ minArgs: 1,
5995
+ maxArgs: -1,
5996
+ evaluate(args, context, evaluator) {
5997
+ const nums = extractNums(args, context, evaluator);
5998
+ if (nums instanceof FormulaError) return nums;
5999
+ if (nums.length < 2) return new FormulaError("#DIV/0!", "VAR requires at least 2 values");
6000
+ const m = mean(nums);
6001
+ let sum = 0;
6002
+ for (const n of nums) sum += (n - m) * (n - m);
6003
+ return sum / (nums.length - 1);
6004
+ }
6005
+ };
6006
+ registry.set("VAR", varImpl);
6007
+ registry.set("VAR.S", varImpl);
6008
+ const varpImpl = {
6009
+ minArgs: 1,
6010
+ maxArgs: -1,
6011
+ evaluate(args, context, evaluator) {
6012
+ const nums = extractNums(args, context, evaluator);
6013
+ if (nums instanceof FormulaError) return nums;
6014
+ if (nums.length === 0) return new FormulaError("#DIV/0!", "VARP requires at least 1 value");
6015
+ const m = mean(nums);
6016
+ let sum = 0;
6017
+ for (const n of nums) sum += (n - m) * (n - m);
6018
+ return sum / nums.length;
6019
+ }
6020
+ };
6021
+ registry.set("VARP", varpImpl);
6022
+ registry.set("VAR.P", varpImpl);
6023
+ registry.set("CORREL", {
6024
+ minArgs: 2,
6025
+ maxArgs: 2,
6026
+ evaluate(args, context, evaluator) {
6027
+ const nums1 = extractNums([args[0]], context, evaluator);
6028
+ if (nums1 instanceof FormulaError) return nums1;
6029
+ const nums2 = extractNums([args[1]], context, evaluator);
6030
+ if (nums2 instanceof FormulaError) return nums2;
6031
+ if (nums1.length !== nums2.length) {
6032
+ return new FormulaError("#N/A", "CORREL: arrays must have same number of values");
6033
+ }
6034
+ if (nums1.length < 2) {
6035
+ return new FormulaError("#DIV/0!", "CORREL requires at least 2 paired values");
6036
+ }
6037
+ const m1 = mean(nums1);
6038
+ const m2 = mean(nums2);
6039
+ let cov = 0;
6040
+ let var1 = 0;
6041
+ let var2 = 0;
6042
+ for (let i = 0; i < nums1.length; i++) {
6043
+ const d1 = nums1[i] - m1;
6044
+ const d2 = nums2[i] - m2;
6045
+ cov += d1 * d2;
6046
+ var1 += d1 * d1;
6047
+ var2 += d2 * d2;
6048
+ }
6049
+ const denom = Math.sqrt(var1 * var2);
6050
+ if (denom === 0) return new FormulaError("#DIV/0!", "CORREL: zero variance");
6051
+ return cov / denom;
6052
+ }
6053
+ });
6054
+ function percentileCalc(nums, k) {
6055
+ if (k < 0 || k > 1) return new FormulaError("#NUM!", "PERCENTILE: k must be between 0 and 1");
6056
+ if (nums.length === 0) return new FormulaError("#NUM!", "PERCENTILE: empty array");
6057
+ const sorted = [...nums].sort((a, b) => a - b);
6058
+ const idx = k * (sorted.length - 1);
6059
+ const low = Math.floor(idx);
6060
+ const high = Math.ceil(idx);
6061
+ if (low === high) return sorted[low];
6062
+ const frac = idx - low;
6063
+ return sorted[low] + frac * (sorted[high] - sorted[low]);
6064
+ }
6065
+ const percentileImpl = {
6066
+ minArgs: 2,
6067
+ maxArgs: 2,
6068
+ evaluate(args, context, evaluator) {
6069
+ const nums = extractNums([args[0]], context, evaluator);
6070
+ if (nums instanceof FormulaError) return nums;
6071
+ const rawK = evaluator.evaluate(args[1], context);
6072
+ if (rawK instanceof FormulaError) return rawK;
6073
+ const k = toNumber(rawK);
6074
+ if (k instanceof FormulaError) return k;
6075
+ return percentileCalc(nums, k);
6076
+ }
6077
+ };
6078
+ registry.set("PERCENTILE", percentileImpl);
6079
+ registry.set("PERCENTILE.INC", percentileImpl);
6080
+ const quartileImpl = {
6081
+ minArgs: 2,
6082
+ maxArgs: 2,
6083
+ evaluate(args, context, evaluator) {
6084
+ const nums = extractNums([args[0]], context, evaluator);
6085
+ if (nums instanceof FormulaError) return nums;
6086
+ const rawQuart = evaluator.evaluate(args[1], context);
6087
+ if (rawQuart instanceof FormulaError) return rawQuart;
6088
+ const quart = toNumber(rawQuart);
6089
+ if (quart instanceof FormulaError) return quart;
6090
+ const quartInt = Math.trunc(quart);
6091
+ if (quartInt < 0 || quartInt > 4) {
6092
+ return new FormulaError("#NUM!", "QUARTILE: quart must be 0, 1, 2, 3, or 4");
6093
+ }
6094
+ return percentileCalc(nums, quartInt * 0.25);
6095
+ }
6096
+ };
6097
+ registry.set("QUARTILE", quartileImpl);
6098
+ registry.set("QUARTILE.INC", quartileImpl);
6099
+ const modeImpl = {
6100
+ minArgs: 1,
6101
+ maxArgs: -1,
6102
+ evaluate(args, context, evaluator) {
6103
+ const nums = extractNums(args, context, evaluator);
6104
+ if (nums instanceof FormulaError) return nums;
6105
+ if (nums.length === 0) return new FormulaError("#N/A", "MODE: no numeric values");
6106
+ const freq = /* @__PURE__ */ new Map();
6107
+ for (const n of nums) {
6108
+ freq.set(n, (freq.get(n) ?? 0) + 1);
6109
+ }
6110
+ let maxFreq = 0;
6111
+ let modeVal = null;
6112
+ for (const n of nums) {
6113
+ const f = freq.get(n) ?? 0;
6114
+ if (f > maxFreq) {
6115
+ maxFreq = f;
6116
+ modeVal = n;
6117
+ }
6118
+ }
6119
+ if (maxFreq < 1 || modeVal === null) return new FormulaError("#N/A", "MODE: no values");
6120
+ return modeVal;
6121
+ }
6122
+ };
6123
+ registry.set("MODE", modeImpl);
6124
+ registry.set("MODE.SNGL", modeImpl);
6125
+ registry.set("GEOMEAN", {
6126
+ minArgs: 1,
6127
+ maxArgs: -1,
6128
+ evaluate(args, context, evaluator) {
6129
+ const nums = extractNums(args, context, evaluator);
6130
+ if (nums instanceof FormulaError) return nums;
6131
+ if (nums.length === 0) return new FormulaError("#NUM!", "GEOMEAN: no numeric values");
6132
+ let sumLn = 0;
6133
+ for (const n of nums) {
6134
+ if (n <= 0) return new FormulaError("#NUM!", "GEOMEAN: all values must be positive");
6135
+ sumLn += Math.log(n);
6136
+ }
6137
+ return Math.exp(sumLn / nums.length);
6138
+ }
6139
+ });
6140
+ registry.set("HARMEAN", {
6141
+ minArgs: 1,
6142
+ maxArgs: -1,
6143
+ evaluate(args, context, evaluator) {
6144
+ const nums = extractNums(args, context, evaluator);
6145
+ if (nums instanceof FormulaError) return nums;
6146
+ if (nums.length === 0) return new FormulaError("#NUM!", "HARMEAN: no numeric values");
6147
+ let sumRecip = 0;
6148
+ for (const n of nums) {
6149
+ if (n <= 0) return new FormulaError("#NUM!", "HARMEAN: all values must be positive");
6150
+ sumRecip += 1 / n;
6151
+ }
6152
+ if (sumRecip === 0) return new FormulaError("#DIV/0!", "HARMEAN: sum of reciprocals is zero");
6153
+ return nums.length / sumRecip;
6154
+ }
6155
+ });
6156
+ }
6157
+ function registerReferenceFunctions(registry) {
6158
+ registry.set("INDIRECT", {
6159
+ minArgs: 1,
6160
+ maxArgs: 2,
6161
+ evaluate(args, context, evaluator) {
6162
+ const rawRef = evaluator.evaluate(args[0], context);
6163
+ if (rawRef instanceof FormulaError) return rawRef;
6164
+ const refText = String(rawRef ?? "");
6165
+ const range = parseRange(refText);
6166
+ if (range) {
6167
+ const start = range.start;
6168
+ const end = range.end;
6169
+ if (start.row === end.row && start.col === end.col) {
6170
+ return context.getCellValue(start);
6171
+ }
6172
+ return context.getCellValue(start);
6173
+ }
6174
+ const addr = parseCellRef(refText);
6175
+ if (!addr) {
6176
+ return new FormulaError("#REF!", `INDIRECT: invalid reference "${refText}"`);
6177
+ }
6178
+ return context.getCellValue(addr);
6179
+ }
6180
+ });
6181
+ registry.set("OFFSET", {
6182
+ minArgs: 3,
6183
+ maxArgs: 5,
6184
+ evaluate(args, context, evaluator) {
6185
+ let baseCol;
6186
+ let baseRow;
6187
+ if (args[0].kind === "cellRef") {
6188
+ baseCol = args[0].address.col;
6189
+ baseRow = args[0].address.row;
6190
+ } else if (args[0].kind === "range") {
6191
+ baseCol = args[0].start.col;
6192
+ baseRow = args[0].start.row;
6193
+ } else {
6194
+ return new FormulaError("#VALUE!", "OFFSET: first argument must be a cell reference");
6195
+ }
6196
+ const rawRows = evaluator.evaluate(args[1], context);
6197
+ if (rawRows instanceof FormulaError) return rawRows;
6198
+ const rowOffset = toNumber(rawRows);
6199
+ if (rowOffset instanceof FormulaError) return rowOffset;
6200
+ const rawCols = evaluator.evaluate(args[2], context);
6201
+ if (rawCols instanceof FormulaError) return rawCols;
6202
+ const colOffset = toNumber(rawCols);
6203
+ if (colOffset instanceof FormulaError) return colOffset;
6204
+ const targetRow = baseRow + Math.trunc(rowOffset);
6205
+ const targetCol = baseCol + Math.trunc(colOffset);
6206
+ if (targetRow < 0 || targetCol < 0) {
6207
+ return new FormulaError("#REF!", "OFFSET: reference out of bounds");
6208
+ }
6209
+ let height = 1;
6210
+ if (args.length >= 4) {
6211
+ const rawH = evaluator.evaluate(args[3], context);
6212
+ if (rawH instanceof FormulaError) return rawH;
6213
+ const h = toNumber(rawH);
6214
+ if (h instanceof FormulaError) return h;
6215
+ height = Math.trunc(h);
6216
+ }
6217
+ let width = 1;
6218
+ if (args.length >= 5) {
6219
+ const rawW = evaluator.evaluate(args[4], context);
6220
+ if (rawW instanceof FormulaError) return rawW;
6221
+ const w = toNumber(rawW);
6222
+ if (w instanceof FormulaError) return w;
6223
+ width = Math.trunc(w);
6224
+ }
6225
+ if (height <= 0 || width <= 0) {
6226
+ return new FormulaError("#VALUE!", "OFFSET: height and width must be >= 1");
6227
+ }
6228
+ if (height === 1 && width === 1) {
6229
+ return context.getCellValue({ col: targetCol, row: targetRow, absCol: false, absRow: false });
6230
+ }
6231
+ return context.getCellValue({ col: targetCol, row: targetRow, absCol: false, absRow: false });
6232
+ }
6233
+ });
6234
+ registry.set("ADDRESS", {
6235
+ minArgs: 2,
6236
+ maxArgs: 5,
6237
+ evaluate(args, context, evaluator) {
6238
+ const rawRow = evaluator.evaluate(args[0], context);
6239
+ if (rawRow instanceof FormulaError) return rawRow;
6240
+ const rowNum = toNumber(rawRow);
6241
+ if (rowNum instanceof FormulaError) return rowNum;
6242
+ const rawCol = evaluator.evaluate(args[1], context);
6243
+ if (rawCol instanceof FormulaError) return rawCol;
6244
+ const colNum = toNumber(rawCol);
6245
+ if (colNum instanceof FormulaError) return colNum;
6246
+ const row = Math.trunc(rowNum);
6247
+ const col = Math.trunc(colNum);
6248
+ if (row < 1 || col < 1) {
6249
+ return new FormulaError("#VALUE!", "ADDRESS: row and column must be >= 1");
6250
+ }
6251
+ let absNum = 1;
6252
+ if (args.length >= 3) {
6253
+ const rawAbs = evaluator.evaluate(args[2], context);
6254
+ if (rawAbs instanceof FormulaError) return rawAbs;
6255
+ const a = toNumber(rawAbs);
6256
+ if (a instanceof FormulaError) return a;
6257
+ absNum = Math.trunc(a);
6258
+ }
6259
+ let sheetText = "";
6260
+ if (args.length >= 5) {
6261
+ const rawSheet = evaluator.evaluate(args[4], context);
6262
+ if (rawSheet instanceof FormulaError) return rawSheet;
6263
+ if (rawSheet !== null && rawSheet !== void 0 && rawSheet !== false) {
6264
+ sheetText = String(rawSheet);
6265
+ }
6266
+ }
6267
+ const colLetter = indexToColumnLetter(col - 1);
6268
+ let address;
6269
+ switch (absNum) {
6270
+ case 1:
6271
+ address = `$${colLetter}$${row}`;
6272
+ break;
6273
+ // $A$1
6274
+ case 2:
6275
+ address = `${colLetter}$${row}`;
6276
+ break;
6277
+ // A$1
6278
+ case 3:
6279
+ address = `$${colLetter}${row}`;
6280
+ break;
6281
+ // $A1
6282
+ case 4:
6283
+ address = `${colLetter}${row}`;
6284
+ break;
6285
+ // A1
6286
+ default:
6287
+ address = `$${colLetter}$${row}`;
6288
+ }
6289
+ if (sheetText) {
6290
+ const quoted = sheetText.includes(" ") ? `'${sheetText}'` : sheetText;
6291
+ return `${quoted}!${address}`;
6292
+ }
6293
+ return address;
6294
+ }
6295
+ });
6296
+ registry.set("ROW", {
6297
+ minArgs: 0,
6298
+ maxArgs: 1,
6299
+ evaluate(args, context, evaluator) {
6300
+ if (args.length === 0) {
6301
+ return 1;
6302
+ }
6303
+ const arg = args[0];
6304
+ if (arg.kind === "cellRef") {
6305
+ return arg.address.row + 1;
6306
+ }
6307
+ if (arg.kind === "range") {
6308
+ return arg.start.row + 1;
6309
+ }
6310
+ const rawRef = evaluator.evaluate(arg, context);
6311
+ if (rawRef instanceof FormulaError) return rawRef;
6312
+ const refText = String(rawRef ?? "");
6313
+ const addr = parseCellRef(refText);
6314
+ if (!addr) {
6315
+ const rng = parseRange(refText);
6316
+ if (rng) return rng.start.row + 1;
6317
+ return new FormulaError("#VALUE!", "ROW: invalid reference");
6318
+ }
6319
+ return addr.row + 1;
6320
+ }
6321
+ });
6322
+ registry.set("COLUMN", {
6323
+ minArgs: 0,
6324
+ maxArgs: 1,
6325
+ evaluate(args, context, evaluator) {
6326
+ if (args.length === 0) {
6327
+ return 1;
6328
+ }
6329
+ const arg = args[0];
6330
+ if (arg.kind === "cellRef") {
6331
+ return arg.address.col + 1;
6332
+ }
6333
+ if (arg.kind === "range") {
6334
+ return arg.start.col + 1;
6335
+ }
6336
+ const rawRef = evaluator.evaluate(arg, context);
6337
+ if (rawRef instanceof FormulaError) return rawRef;
6338
+ const refText = String(rawRef ?? "");
6339
+ const addr = parseCellRef(refText);
6340
+ if (!addr) {
6341
+ const rng = parseRange(refText);
6342
+ if (rng) return rng.start.col + 1;
6343
+ return new FormulaError("#VALUE!", "COLUMN: invalid reference");
6344
+ }
6345
+ return addr.col + 1;
6346
+ }
6347
+ });
6348
+ registry.set("ROWS", {
6349
+ minArgs: 1,
6350
+ maxArgs: 1,
6351
+ evaluate(args, _context, _evaluator) {
6352
+ const arg = args[0];
6353
+ if (arg.kind === "range") {
6354
+ return Math.abs(arg.end.row - arg.start.row) + 1;
6355
+ }
6356
+ if (arg.kind === "cellRef") {
6357
+ return 1;
6358
+ }
6359
+ return new FormulaError("#VALUE!", "ROWS: argument must be a range reference");
6360
+ }
6361
+ });
6362
+ registry.set("COLUMNS", {
4767
6363
  minArgs: 1,
4768
6364
  maxArgs: 1,
4769
- evaluate(args, context, evaluator) {
4770
- const val = evaluator.evaluate(args[0], context);
4771
- return typeof val === "number" && !isNaN(val);
6365
+ evaluate(args, _context, _evaluator) {
6366
+ const arg = args[0];
6367
+ if (arg.kind === "range") {
6368
+ return Math.abs(arg.end.col - arg.start.col) + 1;
6369
+ }
6370
+ if (arg.kind === "cellRef") {
6371
+ return 1;
6372
+ }
6373
+ return new FormulaError("#VALUE!", "COLUMNS: argument must be a range reference");
4772
6374
  }
4773
6375
  });
4774
- registry.set("ISTEXT", {
6376
+ registry.set("SEQUENCE", {
4775
6377
  minArgs: 1,
4776
- maxArgs: 1,
6378
+ maxArgs: 4,
4777
6379
  evaluate(args, context, evaluator) {
4778
- const val = evaluator.evaluate(args[0], context);
4779
- return typeof val === "string";
6380
+ const rawRows = evaluator.evaluate(args[0], context);
6381
+ if (rawRows instanceof FormulaError) return rawRows;
6382
+ const rows = toNumber(rawRows);
6383
+ if (rows instanceof FormulaError) return rows;
6384
+ let cols = 1;
6385
+ if (args.length >= 2) {
6386
+ const rawCols = evaluator.evaluate(args[1], context);
6387
+ if (rawCols instanceof FormulaError) return rawCols;
6388
+ const c = toNumber(rawCols);
6389
+ if (c instanceof FormulaError) return c;
6390
+ cols = Math.trunc(c);
6391
+ }
6392
+ let start = 1;
6393
+ if (args.length >= 3) {
6394
+ const rawStart = evaluator.evaluate(args[2], context);
6395
+ if (rawStart instanceof FormulaError) return rawStart;
6396
+ const s = toNumber(rawStart);
6397
+ if (s instanceof FormulaError) return s;
6398
+ start = s;
6399
+ }
6400
+ let step = 1;
6401
+ if (args.length >= 4) {
6402
+ const rawStep = evaluator.evaluate(args[3], context);
6403
+ if (rawStep instanceof FormulaError) return rawStep;
6404
+ const st = toNumber(rawStep);
6405
+ if (st instanceof FormulaError) return st;
6406
+ step = st;
6407
+ }
6408
+ const rowCount = Math.trunc(rows);
6409
+ const colCount = Math.max(1, cols);
6410
+ if (rowCount < 1) {
6411
+ return new FormulaError("#VALUE!", "SEQUENCE: rows must be >= 1");
6412
+ }
6413
+ const result = [];
6414
+ let current = start;
6415
+ for (let r = 0; r < rowCount; r++) {
6416
+ const row = [];
6417
+ for (let c = 0; c < colCount; c++) {
6418
+ row.push(current);
6419
+ current += step;
6420
+ }
6421
+ result.push(row);
6422
+ }
6423
+ if (rowCount === 1 && colCount === 1) {
6424
+ return result[0][0];
6425
+ }
6426
+ return result[0][0];
4780
6427
  }
4781
6428
  });
4782
- registry.set("ISERROR", {
6429
+ registry.set("TRANSPOSE", {
4783
6430
  minArgs: 1,
4784
6431
  maxArgs: 1,
4785
- evaluate(args, context, evaluator) {
4786
- const val = evaluator.evaluate(args[0], context);
4787
- return val instanceof FormulaError;
6432
+ evaluate(args, context, _evaluator) {
6433
+ if (args[0].kind !== "range") {
6434
+ return new FormulaError("#VALUE!", "TRANSPOSE: argument must be a range");
6435
+ }
6436
+ const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
6437
+ if (data.length === 0) return null;
6438
+ const rows = data.length;
6439
+ const cols = data[0].length;
6440
+ const transposed = [];
6441
+ for (let c = 0; c < cols; c++) {
6442
+ const newRow = [];
6443
+ for (let r = 0; r < rows; r++) {
6444
+ newRow.push(data[r][c]);
6445
+ }
6446
+ transposed.push(newRow);
6447
+ }
6448
+ return transposed[0][0] ?? null;
4788
6449
  }
4789
6450
  });
4790
- registry.set("ISNA", {
6451
+ registry.set("MMULT", {
6452
+ minArgs: 2,
6453
+ maxArgs: 2,
6454
+ evaluate(args, context, _evaluator) {
6455
+ if (args[0].kind !== "range") {
6456
+ return new FormulaError("#VALUE!", "MMULT: array1 must be a range");
6457
+ }
6458
+ if (args[1].kind !== "range") {
6459
+ return new FormulaError("#VALUE!", "MMULT: array2 must be a range");
6460
+ }
6461
+ const a = context.getRangeValues({ start: args[0].start, end: args[0].end });
6462
+ const b = context.getRangeValues({ start: args[1].start, end: args[1].end });
6463
+ if (a.length === 0 || b.length === 0) {
6464
+ return new FormulaError("#VALUE!", "MMULT: empty array");
6465
+ }
6466
+ const aRows = a.length;
6467
+ const aCols = a[0].length;
6468
+ const bRows = b.length;
6469
+ const bCols = b[0].length;
6470
+ if (aCols !== bRows) {
6471
+ return new FormulaError("#VALUE!", `MMULT: columns of array1 (${aCols}) must equal rows of array2 (${bRows})`);
6472
+ }
6473
+ const result = [];
6474
+ for (let r = 0; r < aRows; r++) {
6475
+ const row = [];
6476
+ for (let c = 0; c < bCols; c++) {
6477
+ let sum = 0;
6478
+ for (let k = 0; k < aCols; k++) {
6479
+ const av = toNumber(a[r][k]);
6480
+ const bv = toNumber(b[k][c]);
6481
+ if (av instanceof FormulaError) return av;
6482
+ if (bv instanceof FormulaError) return bv;
6483
+ sum += av * bv;
6484
+ }
6485
+ row.push(sum);
6486
+ }
6487
+ result.push(row);
6488
+ }
6489
+ return result[0][0];
6490
+ }
6491
+ });
6492
+ registry.set("MDETERM", {
4791
6493
  minArgs: 1,
4792
6494
  maxArgs: 1,
4793
- evaluate(args, context, evaluator) {
4794
- const val = evaluator.evaluate(args[0], context);
4795
- return val instanceof FormulaError && val.type === "#N/A";
6495
+ evaluate(args, context, _evaluator) {
6496
+ if (args[0].kind !== "range") {
6497
+ return new FormulaError("#VALUE!", "MDETERM: argument must be a range");
6498
+ }
6499
+ const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
6500
+ if (data.length === 0) return new FormulaError("#VALUE!", "MDETERM: empty array");
6501
+ const n = data.length;
6502
+ if (data.some((row) => row.length !== n)) {
6503
+ return new FormulaError("#VALUE!", "MDETERM: array must be square");
6504
+ }
6505
+ const matrix = [];
6506
+ for (let r = 0; r < n; r++) {
6507
+ const row = [];
6508
+ for (let c = 0; c < n; c++) {
6509
+ const v = toNumber(data[r][c]);
6510
+ if (v instanceof FormulaError) return v;
6511
+ row.push(v);
6512
+ }
6513
+ matrix.push(row);
6514
+ }
6515
+ return determinant(matrix);
4796
6516
  }
4797
6517
  });
4798
- registry.set("TYPE", {
6518
+ registry.set("MINVERSE", {
4799
6519
  minArgs: 1,
4800
6520
  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;
6521
+ evaluate(args, context, _evaluator) {
6522
+ if (args[0].kind !== "range") {
6523
+ return new FormulaError("#VALUE!", "MINVERSE: argument must be a range");
6524
+ }
6525
+ const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
6526
+ if (data.length === 0) return new FormulaError("#VALUE!", "MINVERSE: empty array");
6527
+ const n = data.length;
6528
+ if (data.some((row) => row.length !== n)) {
6529
+ return new FormulaError("#VALUE!", "MINVERSE: array must be square");
6530
+ }
6531
+ const matrix = [];
6532
+ for (let r = 0; r < n; r++) {
6533
+ const row = [];
6534
+ for (let c = 0; c < n; c++) {
6535
+ const v = toNumber(data[r][c]);
6536
+ if (v instanceof FormulaError) return v;
6537
+ row.push(v);
6538
+ }
6539
+ matrix.push(row);
6540
+ }
6541
+ const inv = matrixInverse(matrix, n);
6542
+ if (inv instanceof FormulaError) return inv;
6543
+ return inv[0][0];
4809
6544
  }
4810
6545
  });
4811
6546
  }
6547
+ function determinant(m) {
6548
+ const n = m.length;
6549
+ if (n === 1) return m[0][0];
6550
+ if (n === 2) return m[0][0] * m[1][1] - m[0][1] * m[1][0];
6551
+ let det = 0;
6552
+ for (let c = 0; c < n; c++) {
6553
+ const minor = [];
6554
+ for (let r = 1; r < n; r++) {
6555
+ const row = [];
6556
+ for (let cc = 0; cc < n; cc++) {
6557
+ if (cc !== c) row.push(m[r][cc]);
6558
+ }
6559
+ minor.push(row);
6560
+ }
6561
+ det += (c % 2 === 0 ? 1 : -1) * m[0][c] * determinant(minor);
6562
+ }
6563
+ return det;
6564
+ }
6565
+ function matrixInverse(m, n) {
6566
+ const aug = [];
6567
+ for (let r = 0; r < n; r++) {
6568
+ const row = [...m[r]];
6569
+ for (let c = 0; c < n; c++) {
6570
+ row.push(c === r ? 1 : 0);
6571
+ }
6572
+ aug.push(row);
6573
+ }
6574
+ for (let col = 0; col < n; col++) {
6575
+ let pivotRow = -1;
6576
+ let pivotVal = 0;
6577
+ for (let r = col; r < n; r++) {
6578
+ if (Math.abs(aug[r][col]) > Math.abs(pivotVal)) {
6579
+ pivotVal = aug[r][col];
6580
+ pivotRow = r;
6581
+ }
6582
+ }
6583
+ if (pivotRow === -1 || Math.abs(pivotVal) < 1e-12) {
6584
+ return new FormulaError("#NUM!", "MINVERSE: matrix is singular");
6585
+ }
6586
+ if (pivotRow !== col) {
6587
+ [aug[col], aug[pivotRow]] = [aug[pivotRow], aug[col]];
6588
+ }
6589
+ const scale = aug[col][col];
6590
+ for (let c = 0; c < 2 * n; c++) {
6591
+ aug[col][c] /= scale;
6592
+ }
6593
+ for (let r = 0; r < n; r++) {
6594
+ if (r !== col) {
6595
+ const factor = aug[r][col];
6596
+ for (let c = 0; c < 2 * n; c++) {
6597
+ aug[r][c] -= factor * aug[col][c];
6598
+ }
6599
+ }
6600
+ }
6601
+ }
6602
+ const result = [];
6603
+ for (let r = 0; r < n; r++) {
6604
+ result.push(aug[r].slice(n));
6605
+ }
6606
+ return result;
6607
+ }
4812
6608
  function createBuiltInFunctions() {
4813
6609
  const registry = /* @__PURE__ */ new Map();
4814
6610
  registerMathFunctions(registry);
@@ -4818,6 +6614,9 @@ function createBuiltInFunctions() {
4818
6614
  registerDateFunctions(registry);
4819
6615
  registerStatsFunctions(registry);
4820
6616
  registerInfoFunctions(registry);
6617
+ registerFinancialFunctions(registry);
6618
+ registerStatisticalExtendedFunctions(registry);
6619
+ registerReferenceFunctions(registry);
4821
6620
  return registry;
4822
6621
  }
4823
6622
  function extractDependencies(node) {
@@ -5170,8 +6969,7 @@ var FormulaEngine = class {
5170
6969
  return {
5171
6970
  getCellValue: (addr) => {
5172
6971
  const key = toCellKey(addr.col, addr.row, addr.sheet);
5173
- const cached = this.values.get(key);
5174
- if (cached !== void 0) return cached;
6972
+ if (this.values.has(key)) return this.values.get(key);
5175
6973
  if (addr.sheet) {
5176
6974
  const sheetAccessor = this.sheetAccessors.get(addr.sheet);
5177
6975
  if (!sheetAccessor) return new FormulaError("#REF!", `Unknown sheet: ${addr.sheet}`);
@@ -5194,18 +6992,21 @@ var FormulaEngine = class {
5194
6992
  const row = [];
5195
6993
  for (let c = minCol; c <= maxCol; c++) {
5196
6994
  const key = toCellKey(c, r, sheet);
5197
- const cached = this.values.get(key);
5198
- if (cached !== void 0) {
5199
- row.push(cached);
6995
+ if (this.values.has(key)) {
6996
+ row.push(this.values.get(key));
5200
6997
  } else {
5201
- row.push(rangeAccessor.getCellValue(c, r));
6998
+ row.push(rangeAccessor?.getCellValue(c, r));
5202
6999
  }
5203
7000
  }
5204
7001
  result.push(row);
5205
7002
  }
5206
7003
  return result;
5207
7004
  },
5208
- now: () => contextNow
7005
+ now: () => contextNow,
7006
+ getCellFormula: (addr) => {
7007
+ const key = toCellKey(addr.col, addr.row, addr.sheet);
7008
+ return this.formulas.get(key);
7009
+ }
5209
7010
  };
5210
7011
  }
5211
7012
  recalcCells(order, accessor, updatedCells) {
@@ -5694,9 +7495,13 @@ var TableRenderer = class {
5694
7495
  this.lastColumnWidths = {};
5695
7496
  this.lastHeaderSignature = "";
5696
7497
  this.lastRenderedItems = null;
7498
+ this.formulaEngine = null;
5697
7499
  this.container = container;
5698
7500
  this.state = state;
5699
7501
  }
7502
+ setFormulaEngine(engine) {
7503
+ this.formulaEngine = engine;
7504
+ }
5700
7505
  setVirtualScrollState(vs) {
5701
7506
  this.virtualScrollState = vs;
5702
7507
  }
@@ -6377,10 +8182,12 @@ var TableRenderer = class {
6377
8182
  }
6378
8183
  }
6379
8184
  if (col.renderCell) {
6380
- const value = getCellValue(item, col);
8185
+ const rawValue = getCellValue(item, col);
8186
+ const value = this.formulaEngine?.isEnabled() && this.formulaEngine.hasFormula(colIndex, rowIndex) ? this.formulaEngine.getValue(colIndex, rowIndex) ?? rawValue : rawValue;
6381
8187
  col.renderCell(td, item, value);
6382
8188
  } else {
6383
- const value = getCellValue(item, col);
8189
+ const rawValue = getCellValue(item, col);
8190
+ const value = this.formulaEngine?.isEnabled() && this.formulaEngine.hasFormula(colIndex, rowIndex) ? this.formulaEngine.getValue(colIndex, rowIndex) ?? rawValue : rawValue;
6384
8191
  if (col.valueFormatter) {
6385
8192
  td.textContent = col.valueFormatter(value, item);
6386
8193
  } else if (value != null) {
@@ -9917,90 +11724,296 @@ var OGridRendering = class {
9917
11724
  this.renderSideBar();
9918
11725
  this.renderLoadingOverlay();
9919
11726
  }
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);
11727
+ renderHeaderFilterPopover() {
11728
+ const { headerFilterState, headerFilterComponent, filterConfigs } = this.ctx;
11729
+ const openId = headerFilterState.openColumnId;
11730
+ const allBtns = this.ctx.tableContainer.querySelectorAll(".ogrid-filter-icon[aria-haspopup]");
11731
+ for (const btn of allBtns) {
11732
+ const colId = btn.closest("th[data-column-id]")?.getAttribute("data-column-id");
11733
+ btn.setAttribute("aria-expanded", colId === openId ? "true" : "false");
11734
+ }
11735
+ if (!openId) {
11736
+ headerFilterComponent.cleanup();
11737
+ return;
11738
+ }
11739
+ const config = filterConfigs.get(openId);
11740
+ if (!config) return;
11741
+ headerFilterComponent.render(config);
11742
+ const popoverEl = document.querySelector(".ogrid-header-filter-popover");
11743
+ headerFilterState.setPopoverEl(popoverEl);
11744
+ }
11745
+ renderSideBar() {
11746
+ const { sideBarComponent, sideBarState, state } = this.ctx;
11747
+ if (!sideBarComponent || !sideBarState) return;
11748
+ const columns = state.columns.map((c) => ({
11749
+ columnId: c.columnId,
11750
+ name: c.name,
11751
+ required: c.required === true
11752
+ }));
11753
+ const filterableColumns = state.columns.filter((c) => c.filterable && typeof c.filterable === "object" && c.filterable.type).map((c) => ({
11754
+ columnId: c.columnId,
11755
+ name: c.name,
11756
+ filterField: c.filterable.filterField ?? c.columnId,
11757
+ filterType: c.filterable.type
11758
+ }));
11759
+ sideBarComponent.setConfig({
11760
+ columns,
11761
+ visibleColumns: state.visibleColumns,
11762
+ onVisibilityChange: (columnKey, visible) => {
11763
+ const next = new Set(state.visibleColumns);
11764
+ if (visible) next.add(columnKey);
11765
+ else next.delete(columnKey);
11766
+ state.setVisibleColumns(next);
11767
+ },
11768
+ onSetVisibleColumns: (cols) => state.setVisibleColumns(cols),
11769
+ filterableColumns,
11770
+ filters: state.filters,
11771
+ onFilterChange: (key, value) => state.setFilter(key, value),
11772
+ filterOptions: state.filterOptions
11773
+ });
11774
+ sideBarComponent.render();
11775
+ }
11776
+ renderLoadingOverlay() {
11777
+ const { state, tableContainer } = this.ctx;
11778
+ if (state.isLoading) {
11779
+ const { items } = state.getProcessedItems();
11780
+ tableContainer.style.minHeight = !items || items.length === 0 ? "200px" : "";
11781
+ let loadingOverlay = this.ctx.loadingOverlay;
11782
+ if (!loadingOverlay) {
11783
+ loadingOverlay = document.createElement("div");
11784
+ loadingOverlay.className = "ogrid-loading-overlay";
11785
+ loadingOverlay.style.position = "absolute";
11786
+ loadingOverlay.style.top = "0";
11787
+ loadingOverlay.style.left = "0";
11788
+ loadingOverlay.style.right = "0";
11789
+ loadingOverlay.style.bottom = "0";
11790
+ loadingOverlay.style.display = "flex";
11791
+ loadingOverlay.style.alignItems = "center";
11792
+ loadingOverlay.style.justifyContent = "center";
11793
+ loadingOverlay.style.background = "var(--ogrid-loading-overlay, rgba(255, 255, 255, 0.7))";
11794
+ loadingOverlay.style.zIndex = "100";
11795
+ const spinner = document.createElement("div");
11796
+ spinner.className = "ogrid-loading-spinner";
11797
+ spinner.textContent = "Loading...";
11798
+ loadingOverlay.appendChild(spinner);
11799
+ this.ctx.setLoadingOverlay(loadingOverlay);
11800
+ }
11801
+ if (!tableContainer.contains(loadingOverlay)) {
11802
+ tableContainer.appendChild(loadingOverlay);
11803
+ }
11804
+ } else {
11805
+ tableContainer.style.minHeight = "";
11806
+ const loadingOverlay = this.ctx.loadingOverlay;
11807
+ if (loadingOverlay && tableContainer.contains(loadingOverlay)) {
11808
+ loadingOverlay.remove();
11809
+ }
11810
+ }
11811
+ }
11812
+ };
11813
+
11814
+ // src/state/FormulaEngineState.ts
11815
+ var FormulaEngineState = class {
11816
+ constructor(options) {
11817
+ this.emitter = new EventEmitter();
11818
+ this.engine = null;
11819
+ this.options = options;
11820
+ if (options.formulas) {
11821
+ this.engine = new FormulaEngine({
11822
+ customFunctions: options.formulaFunctions,
11823
+ namedRanges: options.namedRanges
11824
+ });
11825
+ if (options.sheets) {
11826
+ for (const [name, accessor] of Object.entries(options.sheets)) {
11827
+ this.engine.registerSheet(name, accessor);
11828
+ }
11829
+ }
11830
+ }
11831
+ }
11832
+ /**
11833
+ * Initialize with an accessor — loads `initialFormulas` if provided.
11834
+ * Must be called after the grid data is available so the accessor is valid.
11835
+ */
11836
+ initialize(accessor) {
11837
+ if (!this.engine || !this.options.initialFormulas?.length) return;
11838
+ const result = this.engine.loadFormulas(this.options.initialFormulas, accessor);
11839
+ if (result.updatedCells.length > 0) {
11840
+ this.emitRecalc(result);
11841
+ }
11842
+ }
11843
+ /**
11844
+ * Set or clear a formula for a cell. Triggers recalculation of dependents
11845
+ * and emits `formulaRecalc`.
11846
+ */
11847
+ setFormula(col, row, formula, accessor) {
11848
+ if (!this.engine) return void 0;
11849
+ const result = this.engine.setFormula(col, row, formula, accessor);
11850
+ if (result.updatedCells.length > 0) {
11851
+ this.emitRecalc(result);
11852
+ }
11853
+ return result;
11854
+ }
11855
+ /**
11856
+ * Notify the engine that a non-formula cell's value changed.
11857
+ * Triggers recalculation of any formulas that depend on the changed cell.
11858
+ */
11859
+ onCellChanged(col, row, accessor) {
11860
+ if (!this.engine) return void 0;
11861
+ const result = this.engine.onCellChanged(col, row, accessor);
11862
+ if (result.updatedCells.length > 0) {
11863
+ this.emitRecalc(result);
11864
+ }
11865
+ return result;
11866
+ }
11867
+ /** Get the computed value for a formula cell (or undefined if no formula). */
11868
+ getValue(col, row) {
11869
+ return this.engine?.getValue(col, row);
11870
+ }
11871
+ /** Check if a cell has a formula. */
11872
+ hasFormula(col, row) {
11873
+ return this.engine?.hasFormula(col, row) ?? false;
11874
+ }
11875
+ /** Get the formula string for a cell (or undefined if no formula). */
11876
+ getFormula(col, row) {
11877
+ return this.engine?.getFormula(col, row);
11878
+ }
11879
+ /** Whether the formula engine is active. */
11880
+ isEnabled() {
11881
+ return this.engine !== null;
11882
+ }
11883
+ /** Define a named range. */
11884
+ defineNamedRange(name, ref) {
11885
+ this.engine?.defineNamedRange(name, ref);
11886
+ }
11887
+ /** Remove a named range. */
11888
+ removeNamedRange(name) {
11889
+ this.engine?.removeNamedRange(name);
11890
+ }
11891
+ /** Register a sheet accessor for cross-sheet references. */
11892
+ registerSheet(name, accessor) {
11893
+ this.engine?.registerSheet(name, accessor);
11894
+ }
11895
+ /** Unregister a sheet accessor. */
11896
+ unregisterSheet(name) {
11897
+ this.engine?.unregisterSheet(name);
11898
+ }
11899
+ /** Get all cells that a cell depends on (deep, transitive). */
11900
+ getPrecedents(col, row) {
11901
+ return this.engine?.getPrecedents(col, row) ?? [];
11902
+ }
11903
+ /** Get all cells that depend on a cell (deep, transitive). */
11904
+ getDependents(col, row) {
11905
+ return this.engine?.getDependents(col, row) ?? [];
11906
+ }
11907
+ /** Get full audit trail for a cell. */
11908
+ getAuditTrail(col, row) {
11909
+ return this.engine?.getAuditTrail(col, row) ?? null;
11910
+ }
11911
+ /** Subscribe to the `formulaRecalc` event. Returns an unsubscribe function. */
11912
+ onFormulaRecalc(handler) {
11913
+ this.emitter.on("formulaRecalc", handler);
11914
+ return () => this.emitter.off("formulaRecalc", handler);
9937
11915
  }
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();
11916
+ /** Clean up all listeners. */
11917
+ destroy() {
11918
+ this.engine = null;
11919
+ this.emitter.removeAllListeners();
9968
11920
  }
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);
11921
+ // --- Private ---
11922
+ emitRecalc(result) {
11923
+ this.options.onFormulaRecalc?.(result);
11924
+ this.emitter.emit("formulaRecalc", result);
11925
+ }
11926
+ };
11927
+
11928
+ // src/components/FormulaBar.ts
11929
+ var FormulaBar = class {
11930
+ constructor(callbacks) {
11931
+ this.el = null;
11932
+ this.nameBoxEl = null;
11933
+ this.inputEl = null;
11934
+ this.isEditing = false;
11935
+ // --- Private event handlers (arrow functions for stable `this`) ---
11936
+ this.handleKeyDown = (e) => {
11937
+ handleFormulaBarKeyDown(e.key, () => e.preventDefault(), this.callbacks.onCommit, this.callbacks.onCancel);
11938
+ };
11939
+ this.handleInput = () => {
11940
+ if (this.inputEl) {
11941
+ this.callbacks.onInputChange(this.inputEl.value);
9996
11942
  }
9997
- } else {
9998
- tableContainer.style.minHeight = "";
9999
- const loadingOverlay = this.ctx.loadingOverlay;
10000
- if (loadingOverlay && tableContainer.contains(loadingOverlay)) {
10001
- loadingOverlay.remove();
11943
+ };
11944
+ this.handleClick = () => {
11945
+ if (!this.isEditing) {
11946
+ this.callbacks.onStartEditing();
10002
11947
  }
11948
+ };
11949
+ this.callbacks = callbacks;
11950
+ }
11951
+ /** Create the formula bar DOM and append it to the given container. */
11952
+ mount(container) {
11953
+ if (this.el) return;
11954
+ this.el = document.createElement("div");
11955
+ this.el.className = "ogrid-formula-bar";
11956
+ this.el.setAttribute("role", "toolbar");
11957
+ this.el.setAttribute("aria-label", "Formula bar");
11958
+ this.el.style.cssText = FORMULA_BAR_CSS.bar;
11959
+ this.nameBoxEl = document.createElement("div");
11960
+ this.nameBoxEl.className = "ogrid-formula-bar-name";
11961
+ this.nameBoxEl.setAttribute("aria-label", "Active cell reference");
11962
+ this.nameBoxEl.style.cssText = FORMULA_BAR_CSS.nameBox;
11963
+ this.nameBoxEl.textContent = "\u2014";
11964
+ this.el.appendChild(this.nameBoxEl);
11965
+ const fxLabel = document.createElement("div");
11966
+ fxLabel.className = "ogrid-formula-bar-fx";
11967
+ fxLabel.setAttribute("aria-hidden", "true");
11968
+ fxLabel.style.cssText = FORMULA_BAR_CSS.fxLabel;
11969
+ fxLabel.textContent = "fx";
11970
+ this.el.appendChild(fxLabel);
11971
+ this.inputEl = document.createElement("input");
11972
+ this.inputEl.type = "text";
11973
+ this.inputEl.className = "ogrid-formula-bar-input";
11974
+ this.inputEl.setAttribute("aria-label", "Formula input");
11975
+ this.inputEl.spellcheck = false;
11976
+ this.inputEl.autocomplete = "off";
11977
+ this.inputEl.readOnly = true;
11978
+ this.inputEl.style.cssText = FORMULA_BAR_CSS.input;
11979
+ this.inputEl.addEventListener("keydown", this.handleKeyDown);
11980
+ this.inputEl.addEventListener("input", this.handleInput);
11981
+ this.inputEl.addEventListener("click", this.handleClick);
11982
+ this.inputEl.addEventListener("dblclick", this.handleClick);
11983
+ this.el.appendChild(this.inputEl);
11984
+ container.appendChild(this.el);
11985
+ }
11986
+ /** Update the formula bar display with the current active cell ref and formula text. */
11987
+ update(cellRef, formulaText) {
11988
+ if (this.nameBoxEl) {
11989
+ this.nameBoxEl.textContent = cellRef ?? "\u2014";
11990
+ }
11991
+ if (this.inputEl) {
11992
+ this.inputEl.value = formulaText;
11993
+ }
11994
+ }
11995
+ /** Set editing state. When true, the input becomes editable and receives focus. */
11996
+ setEditing(editing) {
11997
+ this.isEditing = editing;
11998
+ if (this.inputEl) {
11999
+ this.inputEl.readOnly = !editing;
12000
+ if (editing) {
12001
+ this.inputEl.focus();
12002
+ }
12003
+ }
12004
+ }
12005
+ /** Remove the formula bar from the DOM and clean up event listeners. */
12006
+ destroy() {
12007
+ if (this.inputEl) {
12008
+ this.inputEl.removeEventListener("keydown", this.handleKeyDown);
12009
+ this.inputEl.removeEventListener("input", this.handleInput);
12010
+ this.inputEl.removeEventListener("click", this.handleClick);
12011
+ this.inputEl.removeEventListener("dblclick", this.handleClick);
10003
12012
  }
12013
+ this.el?.remove();
12014
+ this.el = null;
12015
+ this.nameBoxEl = null;
12016
+ this.inputEl = null;
10004
12017
  }
10005
12018
  };
10006
12019
 
@@ -10134,6 +12147,13 @@ var OGrid = class {
10134
12147
  this.marchingAnts = null;
10135
12148
  this.cellEditor = null;
10136
12149
  this.contextMenu = null;
12150
+ this.formulaEngine = null;
12151
+ this.formulaBar = null;
12152
+ this.formulaBarContainer = null;
12153
+ /** Tracks the text currently displayed/edited in the formula bar. */
12154
+ this.formulaBarText = "";
12155
+ /** Whether the formula bar input is currently in editing mode. */
12156
+ this.formulaBarEditing = false;
10137
12157
  this.events = new EventEmitter();
10138
12158
  this.unsubscribes = [];
10139
12159
  this.isFullScreen = false;
@@ -10143,6 +12163,17 @@ var OGrid = class {
10143
12163
  this.state = new GridState(options);
10144
12164
  this.api = this.state.getApi();
10145
12165
  this.eventWiringHelper = new OGridEventWiring();
12166
+ if (options.formulas) {
12167
+ this.formulaEngine = new FormulaEngineState({
12168
+ formulas: true,
12169
+ initialFormulas: options.initialFormulas,
12170
+ onFormulaRecalc: options.onFormulaRecalc,
12171
+ formulaFunctions: options.formulaFunctions,
12172
+ namedRanges: options.namedRanges,
12173
+ sheets: options.sheets
12174
+ });
12175
+ this.state.setFormulaEngine(this.formulaEngine);
12176
+ }
10146
12177
  injectGlobalStyles("ogrid-theme-vars", OGRID_THEME_CSS);
10147
12178
  this.containerEl = document.createElement("div");
10148
12179
  this.containerEl.className = "ogrid-container";
@@ -10178,6 +12209,20 @@ var OGrid = class {
10178
12209
  this.unsubscribes.push(() => document.removeEventListener("keydown", handleEscKey));
10179
12210
  }
10180
12211
  this.containerEl.appendChild(this.toolbarEl);
12212
+ if (options.formulas) {
12213
+ this.formulaBarContainer = document.createElement("div");
12214
+ this.formulaBarContainer.className = "ogrid-formula-bar-container";
12215
+ this.formulaBar = new FormulaBar({
12216
+ onCommit: () => this.handleFormulaBarCommit(),
12217
+ onCancel: () => this.handleFormulaBarCancel(),
12218
+ onInputChange: (text) => {
12219
+ this.formulaBarText = text;
12220
+ },
12221
+ onStartEditing: () => this.handleFormulaBarStartEditing()
12222
+ });
12223
+ this.formulaBar.mount(this.formulaBarContainer);
12224
+ this.containerEl.appendChild(this.formulaBarContainer);
12225
+ }
10181
12226
  this.bodyArea = document.createElement("div");
10182
12227
  this.bodyArea.className = "ogrid-body-area";
10183
12228
  this.bodyArea.style.display = "flex";
@@ -10200,6 +12245,9 @@ var OGrid = class {
10200
12245
  this.layoutState = new TableLayoutState();
10201
12246
  this.layoutState.observeContainer(this.tableContainer);
10202
12247
  this.renderer = new TableRenderer(this.tableContainer, this.state);
12248
+ if (this.formulaEngine) {
12249
+ this.renderer.setFormulaEngine(this.formulaEngine);
12250
+ }
10203
12251
  this.pagination = new PaginationControls(this.paginationContainer, this.state);
10204
12252
  this.statusBar = new StatusBar(this.statusBarContainer);
10205
12253
  this.columnChooser = new ColumnChooser(this.toolbarEl, this.state);
@@ -10311,6 +12359,41 @@ var OGrid = class {
10311
12359
  })
10312
12360
  );
10313
12361
  }
12362
+ if (this.formulaBar && this.selectionState && this.formulaEngine) {
12363
+ const fBar = this.formulaBar;
12364
+ const sel = this.selectionState;
12365
+ const fEngine = this.formulaEngine;
12366
+ let colOffset = 0;
12367
+ if (this.rowSelectionState) colOffset++;
12368
+ if (options.showRowNumbers || options.cellReferences || options.formulas) colOffset++;
12369
+ this.unsubscribes.push(
12370
+ sel.onSelectionChange(({ activeCell }) => {
12371
+ this.formulaBarEditing = false;
12372
+ fBar.setEditing(false);
12373
+ if (activeCell) {
12374
+ const dataCol = activeCell.columnIndex - colOffset;
12375
+ const dataRow = (this.state.page - 1) * this.state.pageSize + activeCell.rowIndex;
12376
+ const cellRef = formatCellReference(dataCol, dataRow + 1);
12377
+ const formula = fEngine.getFormula(dataCol, dataRow);
12378
+ if (formula) {
12379
+ this.formulaBarText = formula;
12380
+ fBar.update(cellRef, formula);
12381
+ } else {
12382
+ const { items } = this.state.getProcessedItems();
12383
+ const visibleCols = this.state.visibleColumnDefs;
12384
+ const item = items[activeCell.rowIndex];
12385
+ const col = visibleCols[dataCol];
12386
+ const value = item && col ? String(item[col.columnId] ?? "") : "";
12387
+ this.formulaBarText = value;
12388
+ fBar.update(cellRef, value);
12389
+ }
12390
+ } else {
12391
+ this.formulaBarText = "";
12392
+ fBar.update(null, "");
12393
+ }
12394
+ })
12395
+ );
12396
+ }
10314
12397
  }
10315
12398
  this.unsubscribes.push(
10316
12399
  this.state.onStateChange(() => {
@@ -10535,6 +12618,83 @@ var OGrid = class {
10535
12618
  this.headerFilterState.setFilterOptions(this.state.filterOptions);
10536
12619
  this.headerFilterState.open(columnId, config, headerEl, tempPopover);
10537
12620
  }
12621
+ // --- Formula bar handlers ---
12622
+ /** Build a grid data accessor for the formula engine from current state. */
12623
+ buildFormulaAccessor() {
12624
+ const { items } = this.state.getProcessedItems();
12625
+ const visibleCols = this.state.visibleColumnDefs;
12626
+ return {
12627
+ getCellValue: (col, row) => {
12628
+ const item = items[row];
12629
+ const colDef = visibleCols[col];
12630
+ if (!item || !colDef) return void 0;
12631
+ return item[colDef.columnId];
12632
+ },
12633
+ getRowCount: () => items.length,
12634
+ getColumnCount: () => visibleCols.length
12635
+ };
12636
+ }
12637
+ handleFormulaBarCommit() {
12638
+ if (!this.formulaEngine || !this.selectionState) return;
12639
+ const ac = this.selectionState.activeCell;
12640
+ if (!ac) return;
12641
+ let colOffset = 0;
12642
+ if (this.rowSelectionState) colOffset++;
12643
+ if (this.options.showRowNumbers || this.options.cellReferences || this.options.formulas) colOffset++;
12644
+ const dataCol = ac.columnIndex - colOffset;
12645
+ const dataRow = (this.state.page - 1) * this.state.pageSize + ac.rowIndex;
12646
+ const text = this.formulaBarText;
12647
+ const accessor = this.buildFormulaAccessor();
12648
+ if (text.startsWith("=")) {
12649
+ this.formulaEngine.setFormula(dataCol, dataRow, text, accessor);
12650
+ } else {
12651
+ if (this.formulaEngine.hasFormula(dataCol, dataRow)) {
12652
+ this.formulaEngine.setFormula(dataCol, dataRow, null, accessor);
12653
+ }
12654
+ const { items } = this.state.getProcessedItems();
12655
+ const visibleCols = this.state.visibleColumnDefs;
12656
+ const item = items[ac.rowIndex];
12657
+ const col = visibleCols[dataCol];
12658
+ if (item && col) {
12659
+ item[col.columnId] = text;
12660
+ }
12661
+ }
12662
+ this.formulaBarEditing = false;
12663
+ this.formulaBar?.setEditing(false);
12664
+ this.renderingHelper.updateRendererInteractionState();
12665
+ this.renderer.getWrapperElement()?.focus();
12666
+ }
12667
+ handleFormulaBarCancel() {
12668
+ if (this.selectionState?.activeCell && this.formulaEngine) {
12669
+ const ac = this.selectionState.activeCell;
12670
+ let colOffset = 0;
12671
+ if (this.rowSelectionState) colOffset++;
12672
+ if (this.options.showRowNumbers || this.options.cellReferences || this.options.formulas) colOffset++;
12673
+ const dataCol = ac.columnIndex - colOffset;
12674
+ const dataRow = (this.state.page - 1) * this.state.pageSize + ac.rowIndex;
12675
+ const formula = this.formulaEngine.getFormula(dataCol, dataRow);
12676
+ if (formula) {
12677
+ this.formulaBarText = formula;
12678
+ this.formulaBar?.update(formatCellReference(dataCol, dataRow + 1), formula);
12679
+ } else {
12680
+ const { items } = this.state.getProcessedItems();
12681
+ const visibleCols = this.state.visibleColumnDefs;
12682
+ const item = items[ac.rowIndex];
12683
+ const col = visibleCols[dataCol];
12684
+ const value = item && col ? String(item[col.columnId] ?? "") : "";
12685
+ this.formulaBarText = value;
12686
+ this.formulaBar?.update(formatCellReference(dataCol, dataRow + 1), value);
12687
+ }
12688
+ }
12689
+ this.formulaBarEditing = false;
12690
+ this.formulaBar?.setEditing(false);
12691
+ this.renderer.getWrapperElement()?.focus();
12692
+ }
12693
+ handleFormulaBarStartEditing() {
12694
+ if (!this.selectionState?.activeCell) return;
12695
+ this.formulaBarEditing = true;
12696
+ this.formulaBar?.setEditing(true);
12697
+ }
10538
12698
  // Rendering methods delegated to OGridRendering helper:
10539
12699
  // - updateRendererInteractionState() -> this.renderingHelper.updateRendererInteractionState()
10540
12700
  // - updateDragAttributes() -> this.renderingHelper.updateDragAttributes()
@@ -10589,123 +12749,11 @@ var OGrid = class {
10589
12749
  this.layoutState.destroy();
10590
12750
  this.cellEditor?.closeEditor();
10591
12751
  this.contextMenu?.close();
12752
+ this.formulaBar?.destroy();
12753
+ this.formulaEngine?.destroy();
10592
12754
  this.events.removeAllListeners();
10593
12755
  this.containerEl.remove();
10594
12756
  }
10595
12757
  };
10596
12758
 
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 };
12759
+ 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_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 };