@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,
|
|
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:
|
|
4512
|
+
maxArgs: 2,
|
|
4243
4513
|
evaluate(args, context, evaluator) {
|
|
4244
|
-
const
|
|
4245
|
-
if (
|
|
4246
|
-
const
|
|
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,
|
|
4770
|
-
const
|
|
4771
|
-
|
|
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("
|
|
6376
|
+
registry.set("SEQUENCE", {
|
|
4775
6377
|
minArgs: 1,
|
|
4776
|
-
maxArgs:
|
|
6378
|
+
maxArgs: 4,
|
|
4777
6379
|
evaluate(args, context, evaluator) {
|
|
4778
|
-
const
|
|
4779
|
-
|
|
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("
|
|
6429
|
+
registry.set("TRANSPOSE", {
|
|
4783
6430
|
minArgs: 1,
|
|
4784
6431
|
maxArgs: 1,
|
|
4785
|
-
evaluate(args, context,
|
|
4786
|
-
|
|
4787
|
-
|
|
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("
|
|
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,
|
|
4794
|
-
|
|
4795
|
-
|
|
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("
|
|
6518
|
+
registry.set("MINVERSE", {
|
|
4799
6519
|
minArgs: 1,
|
|
4800
6520
|
maxArgs: 1,
|
|
4801
|
-
evaluate(args, context,
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
if (
|
|
4807
|
-
|
|
4808
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5198
|
-
|
|
5199
|
-
row.push(cached);
|
|
6995
|
+
if (this.values.has(key)) {
|
|
6996
|
+
row.push(this.values.get(key));
|
|
5200
6997
|
} else {
|
|
5201
|
-
row.push(rangeAccessor
|
|
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
|
|
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
|
|
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
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
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
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
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
|
-
}
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
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
|
-
|
|
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 };
|