@alaarab/ogrid-core 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +1922 -108
- package/dist/types/constants/formulaBar.d.ts +60 -0
- package/dist/types/constants/index.d.ts +2 -1
- package/dist/types/constants/layout.d.ts +4 -0
- package/dist/types/formula/functions/financial.d.ts +2 -0
- package/dist/types/formula/functions/reference.d.ts +2 -0
- package/dist/types/formula/functions/statistical-extended.d.ts +2 -0
- package/dist/types/formula/types.d.ts +2 -0
- package/dist/types/index.d.ts +6 -3
- package/dist/types/types/dataGridTypes.d.ts +7 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/cellValue.d.ts +6 -0
- package/dist/types/utils/dataGridViewModel.d.ts +8 -0
- package/dist/types/utils/formulaBarHelpers.d.ts +40 -0
- package/dist/types/utils/index.d.ts +3 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -85,6 +85,17 @@ function getCellValue(item, col) {
|
|
|
85
85
|
function isColumnEditable(col, item) {
|
|
86
86
|
return col.editable === true || typeof col.editable === "function" && col.editable(item);
|
|
87
87
|
}
|
|
88
|
+
function createGridDataAccessor(items, flatColumns) {
|
|
89
|
+
return {
|
|
90
|
+
getCellValue: (col, row) => {
|
|
91
|
+
if (row < 0 || row >= items.length) return null;
|
|
92
|
+
if (col < 0 || col >= flatColumns.length) return null;
|
|
93
|
+
return getCellValue(items[row], flatColumns[col]);
|
|
94
|
+
},
|
|
95
|
+
getRowCount: () => items.length,
|
|
96
|
+
getColumnCount: () => flatColumns.length
|
|
97
|
+
};
|
|
98
|
+
}
|
|
88
99
|
|
|
89
100
|
// src/utils/columnUtils.ts
|
|
90
101
|
function isColumnGroupDef(c) {
|
|
@@ -1144,7 +1155,7 @@ var _CellDescriptorCache = class _CellDescriptorCache {
|
|
|
1144
1155
|
const sr = input.selectionRange;
|
|
1145
1156
|
const cr = input.cutRange;
|
|
1146
1157
|
const cp = input.copyRange;
|
|
1147
|
-
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");
|
|
1158
|
+
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);
|
|
1148
1159
|
}
|
|
1149
1160
|
/**
|
|
1150
1161
|
* Get a cached descriptor or compute a new one.
|
|
@@ -1220,6 +1231,7 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
|
|
|
1220
1231
|
const isPinned = col.pinned != null;
|
|
1221
1232
|
const pinnedSide = col.pinned ?? void 0;
|
|
1222
1233
|
const cellValue = getCellValue(item, col);
|
|
1234
|
+
const formulaDisplay = input.hasFormula?.(colIdx, rowIndex) ? input.getFormulaValue?.(colIdx, rowIndex) : void 0;
|
|
1223
1235
|
let mode = "display";
|
|
1224
1236
|
let editorType;
|
|
1225
1237
|
if (isEditing && canEditInline) {
|
|
@@ -1251,7 +1263,8 @@ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
|
|
|
1251
1263
|
globalColIndex,
|
|
1252
1264
|
rowId,
|
|
1253
1265
|
rowIndex,
|
|
1254
|
-
displayValue: cellValue
|
|
1266
|
+
displayValue: formulaDisplay !== void 0 ? formulaDisplay : cellValue,
|
|
1267
|
+
columnType: col.type
|
|
1255
1268
|
};
|
|
1256
1269
|
}
|
|
1257
1270
|
function resolveCellDisplayContent(col, item, displayValue) {
|
|
@@ -1266,7 +1279,7 @@ function resolveCellDisplayContent(col, item, displayValue) {
|
|
|
1266
1279
|
if (displayValue == null) return null;
|
|
1267
1280
|
if (col.type === "date") {
|
|
1268
1281
|
const d = new Date(String(displayValue));
|
|
1269
|
-
if (!Number.isNaN(d.getTime())) return d.toLocaleDateString();
|
|
1282
|
+
if (!Number.isNaN(d.getTime())) return d.toLocaleDateString(void 0, { timeZone: "UTC" });
|
|
1270
1283
|
}
|
|
1271
1284
|
if (col.type === "boolean") {
|
|
1272
1285
|
return displayValue ? "True" : "False";
|
|
@@ -1392,6 +1405,8 @@ var CHECKBOX_COLUMN_WIDTH = 48;
|
|
|
1392
1405
|
var ROW_NUMBER_COLUMN_WIDTH = 50;
|
|
1393
1406
|
var DEFAULT_MIN_COLUMN_WIDTH = 80;
|
|
1394
1407
|
var CELL_PADDING = 16;
|
|
1408
|
+
var ROW_NUMBER_COLUMN_MIN_WIDTH = 30;
|
|
1409
|
+
var ROW_NUMBER_COLUMN_ID = "__row_number__";
|
|
1395
1410
|
var GRID_BORDER_RADIUS = 6;
|
|
1396
1411
|
|
|
1397
1412
|
// src/utils/columnAutosize.ts
|
|
@@ -2063,65 +2078,6 @@ function validateRowIds(items, getRowId) {
|
|
|
2063
2078
|
}
|
|
2064
2079
|
}
|
|
2065
2080
|
|
|
2066
|
-
// src/constants/timing.ts
|
|
2067
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
2068
|
-
var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
|
|
2069
|
-
var SIDEBAR_TRANSITION_MS = 300;
|
|
2070
|
-
|
|
2071
|
-
// src/constants/zIndex.ts
|
|
2072
|
-
var Z_INDEX = {
|
|
2073
|
-
/** Column resize drag handle */
|
|
2074
|
-
RESIZE_HANDLE: 1,
|
|
2075
|
-
/** Active/editing cell outline */
|
|
2076
|
-
ACTIVE_CELL: 2,
|
|
2077
|
-
/** Fill handle dot */
|
|
2078
|
-
FILL_HANDLE: 3,
|
|
2079
|
-
/** Selection range overlay (marching ants) */
|
|
2080
|
-
SELECTION_OVERLAY: 4,
|
|
2081
|
-
/** Row number column */
|
|
2082
|
-
ROW_NUMBER: 5,
|
|
2083
|
-
/** Clipboard overlay (copy/cut animation) */
|
|
2084
|
-
CLIPBOARD_OVERLAY: 5,
|
|
2085
|
-
/** Sticky pinned body cells */
|
|
2086
|
-
PINNED: 6,
|
|
2087
|
-
/** Selection checkbox column in body */
|
|
2088
|
-
SELECTION_CELL: 7,
|
|
2089
|
-
/** Sticky thead row */
|
|
2090
|
-
THEAD: 8,
|
|
2091
|
-
/** Pinned header cells (sticky both axes) */
|
|
2092
|
-
PINNED_HEADER: 10,
|
|
2093
|
-
/** Focused header cell */
|
|
2094
|
-
HEADER_FOCUS: 11,
|
|
2095
|
-
/** Checkbox column in sticky header (sticky both axes) */
|
|
2096
|
-
SELECTION_HEADER_PINNED: 12,
|
|
2097
|
-
/** Loading overlay within table */
|
|
2098
|
-
LOADING: 2,
|
|
2099
|
-
/** Column reorder drop indicator */
|
|
2100
|
-
DROP_INDICATOR: 100,
|
|
2101
|
-
/** Dropdown menus (column chooser, pagination size select) */
|
|
2102
|
-
DROPDOWN: 1e3,
|
|
2103
|
-
/** Filter popovers */
|
|
2104
|
-
FILTER_POPOVER: 1e3,
|
|
2105
|
-
/** Modal dialogs */
|
|
2106
|
-
MODAL: 2e3,
|
|
2107
|
-
/** Fullscreen grid container */
|
|
2108
|
-
FULLSCREEN: 9999,
|
|
2109
|
-
/** Context menus (right-click grid menu) */
|
|
2110
|
-
CONTEXT_MENU: 1e4
|
|
2111
|
-
};
|
|
2112
|
-
|
|
2113
|
-
// src/formula/errors.ts
|
|
2114
|
-
var REF_ERROR = new FormulaError("#REF!", "Invalid cell reference");
|
|
2115
|
-
var DIV_ZERO_ERROR = new FormulaError("#DIV/0!", "Division by zero");
|
|
2116
|
-
var VALUE_ERROR = new FormulaError("#VALUE!", "Wrong value type");
|
|
2117
|
-
var NAME_ERROR = new FormulaError("#NAME?", "Unknown function or name");
|
|
2118
|
-
var CIRC_ERROR = new FormulaError("#CIRC!", "Circular reference");
|
|
2119
|
-
var GENERAL_ERROR = new FormulaError("#ERROR!", "Formula error");
|
|
2120
|
-
var NA_ERROR = new FormulaError("#N/A", "No match found");
|
|
2121
|
-
function isFormulaError(value) {
|
|
2122
|
-
return value instanceof FormulaError;
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
2081
|
// src/formula/tokenizer.ts
|
|
2126
2082
|
var CELL_REF_PATTERN = /^\$?[A-Za-z]+\$?\d+$/;
|
|
2127
2083
|
var SINGLE_CHAR_OPERATORS = {
|
|
@@ -2244,6 +2200,12 @@ function tokenize(input) {
|
|
|
2244
2200
|
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] === "_")) {
|
|
2245
2201
|
pos++;
|
|
2246
2202
|
}
|
|
2203
|
+
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")) {
|
|
2204
|
+
pos++;
|
|
2205
|
+
while (pos < input.length && (input[pos] >= "A" && input[pos] <= "Z" || input[pos] >= "a" && input[pos] <= "z" || input[pos] >= "0" && input[pos] <= "9" || input[pos] === "_")) {
|
|
2206
|
+
pos++;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2247
2209
|
const word = input.slice(start, pos);
|
|
2248
2210
|
if (pos < input.length && input[pos] === "!") {
|
|
2249
2211
|
pos++;
|
|
@@ -2272,6 +2234,200 @@ function tokenize(input) {
|
|
|
2272
2234
|
return tokens;
|
|
2273
2235
|
}
|
|
2274
2236
|
|
|
2237
|
+
// src/utils/formulaBarHelpers.ts
|
|
2238
|
+
var CELL_REF_RE2 = /^\$?([A-Za-z]+)\$?(\d+)$/;
|
|
2239
|
+
function parseCellRefCoords(ref) {
|
|
2240
|
+
const m = ref.match(CELL_REF_RE2);
|
|
2241
|
+
if (!m) return null;
|
|
2242
|
+
return { col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 };
|
|
2243
|
+
}
|
|
2244
|
+
function handleFormulaBarKeyDown(key, preventDefault, onCommit, onCancel) {
|
|
2245
|
+
if (key === "Enter") {
|
|
2246
|
+
preventDefault();
|
|
2247
|
+
onCommit();
|
|
2248
|
+
} else if (key === "Escape") {
|
|
2249
|
+
preventDefault();
|
|
2250
|
+
onCancel();
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function processFormulaBarCommit(text, col, row, setFormula, onCellValueChanged) {
|
|
2254
|
+
const trimmed = text.trim();
|
|
2255
|
+
if (trimmed.startsWith("=")) {
|
|
2256
|
+
setFormula(col, row, trimmed);
|
|
2257
|
+
} else {
|
|
2258
|
+
setFormula(col, row, null);
|
|
2259
|
+
onCellValueChanged?.(col, row, trimmed);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function deriveFormulaBarText(col, row, getFormula, getRawValue) {
|
|
2263
|
+
if (col == null || row == null) return "";
|
|
2264
|
+
const formula = getFormula?.(col, row);
|
|
2265
|
+
if (formula) return formula;
|
|
2266
|
+
const raw = getRawValue?.(col, row);
|
|
2267
|
+
return raw != null ? String(raw) : "";
|
|
2268
|
+
}
|
|
2269
|
+
function extractFormulaReferences(formula) {
|
|
2270
|
+
if (!formula || formula[0] !== "=") return [];
|
|
2271
|
+
const refs = [];
|
|
2272
|
+
let colorIdx = 0;
|
|
2273
|
+
try {
|
|
2274
|
+
const tokens = tokenize(formula.substring(1));
|
|
2275
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2276
|
+
const tok = tokens[i];
|
|
2277
|
+
if (tok.type === "CELL_REF") {
|
|
2278
|
+
if (i + 2 < tokens.length && tokens[i + 1].type === "COLON" && tokens[i + 2].type === "CELL_REF") {
|
|
2279
|
+
const start = parseCellRefCoords(tok.value);
|
|
2280
|
+
const end = parseCellRefCoords(tokens[i + 2].value);
|
|
2281
|
+
if (start && end) {
|
|
2282
|
+
refs.push({
|
|
2283
|
+
type: "range",
|
|
2284
|
+
col: start.col,
|
|
2285
|
+
row: start.row,
|
|
2286
|
+
endCol: end.col,
|
|
2287
|
+
endRow: end.row,
|
|
2288
|
+
colorIndex: colorIdx++ % 6
|
|
2289
|
+
});
|
|
2290
|
+
i += 2;
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
const coords = parseCellRefCoords(tok.value);
|
|
2295
|
+
if (coords) {
|
|
2296
|
+
refs.push({
|
|
2297
|
+
type: "cell",
|
|
2298
|
+
col: coords.col,
|
|
2299
|
+
row: coords.row,
|
|
2300
|
+
colorIndex: colorIdx++ % 6
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
return refs;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// src/constants/timing.ts
|
|
2311
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
2312
|
+
var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
|
|
2313
|
+
var SIDEBAR_TRANSITION_MS = 300;
|
|
2314
|
+
|
|
2315
|
+
// src/constants/zIndex.ts
|
|
2316
|
+
var Z_INDEX = {
|
|
2317
|
+
/** Column resize drag handle */
|
|
2318
|
+
RESIZE_HANDLE: 1,
|
|
2319
|
+
/** Active/editing cell outline */
|
|
2320
|
+
ACTIVE_CELL: 2,
|
|
2321
|
+
/** Fill handle dot */
|
|
2322
|
+
FILL_HANDLE: 3,
|
|
2323
|
+
/** Selection range overlay (marching ants) */
|
|
2324
|
+
SELECTION_OVERLAY: 4,
|
|
2325
|
+
/** Row number column */
|
|
2326
|
+
ROW_NUMBER: 5,
|
|
2327
|
+
/** Clipboard overlay (copy/cut animation) */
|
|
2328
|
+
CLIPBOARD_OVERLAY: 5,
|
|
2329
|
+
/** Sticky pinned body cells */
|
|
2330
|
+
PINNED: 6,
|
|
2331
|
+
/** Selection checkbox column in body */
|
|
2332
|
+
SELECTION_CELL: 7,
|
|
2333
|
+
/** Sticky thead row */
|
|
2334
|
+
THEAD: 8,
|
|
2335
|
+
/** Pinned header cells (sticky both axes) */
|
|
2336
|
+
PINNED_HEADER: 10,
|
|
2337
|
+
/** Focused header cell */
|
|
2338
|
+
HEADER_FOCUS: 11,
|
|
2339
|
+
/** Checkbox column in sticky header (sticky both axes) */
|
|
2340
|
+
SELECTION_HEADER_PINNED: 12,
|
|
2341
|
+
/** Loading overlay within table */
|
|
2342
|
+
LOADING: 2,
|
|
2343
|
+
/** Column reorder drop indicator */
|
|
2344
|
+
DROP_INDICATOR: 100,
|
|
2345
|
+
/** Dropdown menus (column chooser, pagination size select) */
|
|
2346
|
+
DROPDOWN: 1e3,
|
|
2347
|
+
/** Filter popovers */
|
|
2348
|
+
FILTER_POPOVER: 1e3,
|
|
2349
|
+
/** Modal dialogs */
|
|
2350
|
+
MODAL: 2e3,
|
|
2351
|
+
/** Fullscreen grid container */
|
|
2352
|
+
FULLSCREEN: 9999,
|
|
2353
|
+
/** Context menus (right-click grid menu) */
|
|
2354
|
+
CONTEXT_MENU: 1e4
|
|
2355
|
+
};
|
|
2356
|
+
|
|
2357
|
+
// src/constants/formulaBar.ts
|
|
2358
|
+
var FORMULA_REF_COLORS = [
|
|
2359
|
+
"var(--ogrid-formula-ref-0, #4285f4)",
|
|
2360
|
+
"var(--ogrid-formula-ref-1, #ea4335)",
|
|
2361
|
+
"var(--ogrid-formula-ref-2, #34a853)",
|
|
2362
|
+
"var(--ogrid-formula-ref-3, #9334e6)",
|
|
2363
|
+
"var(--ogrid-formula-ref-4, #ff6d01)",
|
|
2364
|
+
"var(--ogrid-formula-ref-5, #46bdc6)"
|
|
2365
|
+
];
|
|
2366
|
+
var FORMULA_BAR_CSS = {
|
|
2367
|
+
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;",
|
|
2368
|
+
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;",
|
|
2369
|
+
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;",
|
|
2370
|
+
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;"
|
|
2371
|
+
};
|
|
2372
|
+
var FORMULA_BAR_STYLES = {
|
|
2373
|
+
bar: {
|
|
2374
|
+
display: "flex",
|
|
2375
|
+
alignItems: "center",
|
|
2376
|
+
borderBottom: "1px solid var(--ogrid-border, #e0e0e0)",
|
|
2377
|
+
background: "var(--ogrid-bg, #fff)",
|
|
2378
|
+
minHeight: "28px",
|
|
2379
|
+
fontSize: "13px"
|
|
2380
|
+
},
|
|
2381
|
+
nameBox: {
|
|
2382
|
+
fontFamily: "monospace",
|
|
2383
|
+
fontSize: "12px",
|
|
2384
|
+
fontWeight: 500,
|
|
2385
|
+
padding: "2px 8px",
|
|
2386
|
+
borderRight: "1px solid var(--ogrid-border, #e0e0e0)",
|
|
2387
|
+
background: "var(--ogrid-bg, #fff)",
|
|
2388
|
+
color: "var(--ogrid-fg, #242424)",
|
|
2389
|
+
minWidth: "52px",
|
|
2390
|
+
textAlign: "center",
|
|
2391
|
+
lineHeight: "24px",
|
|
2392
|
+
userSelect: "none",
|
|
2393
|
+
whiteSpace: "nowrap"
|
|
2394
|
+
},
|
|
2395
|
+
fxLabel: {
|
|
2396
|
+
padding: "2px 8px",
|
|
2397
|
+
fontStyle: "italic",
|
|
2398
|
+
fontWeight: 600,
|
|
2399
|
+
color: "var(--ogrid-muted-fg, #888)",
|
|
2400
|
+
userSelect: "none",
|
|
2401
|
+
borderRight: "1px solid var(--ogrid-border, #e0e0e0)",
|
|
2402
|
+
lineHeight: "24px",
|
|
2403
|
+
fontSize: "12px"
|
|
2404
|
+
},
|
|
2405
|
+
input: {
|
|
2406
|
+
flex: 1,
|
|
2407
|
+
border: "none",
|
|
2408
|
+
outline: "none",
|
|
2409
|
+
padding: "2px 8px",
|
|
2410
|
+
fontFamily: "monospace",
|
|
2411
|
+
fontSize: "12px",
|
|
2412
|
+
lineHeight: "24px",
|
|
2413
|
+
background: "transparent",
|
|
2414
|
+
color: "var(--ogrid-fg, #242424)",
|
|
2415
|
+
minWidth: 0
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
// src/formula/errors.ts
|
|
2420
|
+
var REF_ERROR = new FormulaError("#REF!", "Invalid cell reference");
|
|
2421
|
+
var DIV_ZERO_ERROR = new FormulaError("#DIV/0!", "Division by zero");
|
|
2422
|
+
var VALUE_ERROR = new FormulaError("#VALUE!", "Wrong value type");
|
|
2423
|
+
var NAME_ERROR = new FormulaError("#NAME?", "Unknown function or name");
|
|
2424
|
+
var CIRC_ERROR = new FormulaError("#CIRC!", "Circular reference");
|
|
2425
|
+
var GENERAL_ERROR = new FormulaError("#ERROR!", "Formula error");
|
|
2426
|
+
var NA_ERROR = new FormulaError("#N/A", "No match found");
|
|
2427
|
+
function isFormulaError(value) {
|
|
2428
|
+
return value instanceof FormulaError;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2275
2431
|
// src/formula/parser.ts
|
|
2276
2432
|
function parse(tokens, namedRanges) {
|
|
2277
2433
|
let pos = 0;
|
|
@@ -2923,7 +3079,7 @@ var DependencyGraph = class {
|
|
|
2923
3079
|
if (cellDependents) {
|
|
2924
3080
|
for (const dependent of cellDependents) {
|
|
2925
3081
|
if (affected.has(dependent)) {
|
|
2926
|
-
const newDegree = inDegree.get(dependent) - 1;
|
|
3082
|
+
const newDegree = (inDegree.get(dependent) ?? 0) - 1;
|
|
2927
3083
|
inDegree.set(dependent, newDegree);
|
|
2928
3084
|
if (newDegree === 0) {
|
|
2929
3085
|
queue.push(dependent);
|
|
@@ -3235,7 +3391,7 @@ function registerMathFunctions(registry) {
|
|
|
3235
3391
|
registry.set("SUMPRODUCT", {
|
|
3236
3392
|
minArgs: 1,
|
|
3237
3393
|
maxArgs: -1,
|
|
3238
|
-
evaluate(args, context,
|
|
3394
|
+
evaluate(args, context, _evaluator) {
|
|
3239
3395
|
const arrays = [];
|
|
3240
3396
|
for (const arg of args) {
|
|
3241
3397
|
if (arg.kind !== "range") {
|
|
@@ -3454,6 +3610,162 @@ function registerMathFunctions(registry) {
|
|
|
3454
3610
|
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
|
|
3455
3611
|
}
|
|
3456
3612
|
});
|
|
3613
|
+
registry.set("MROUND", {
|
|
3614
|
+
minArgs: 2,
|
|
3615
|
+
maxArgs: 2,
|
|
3616
|
+
evaluate(args, context, evaluator) {
|
|
3617
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3618
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3619
|
+
const num = toNumber(rawNum);
|
|
3620
|
+
if (num instanceof FormulaError) return num;
|
|
3621
|
+
const rawMul = evaluator.evaluate(args[1], context);
|
|
3622
|
+
if (rawMul instanceof FormulaError) return rawMul;
|
|
3623
|
+
const multiple = toNumber(rawMul);
|
|
3624
|
+
if (multiple instanceof FormulaError) return multiple;
|
|
3625
|
+
if (multiple === 0) return 0;
|
|
3626
|
+
if (num > 0 && multiple < 0 || num < 0 && multiple > 0) {
|
|
3627
|
+
return new FormulaError("#NUM!", "MROUND: number and multiple must have the same sign");
|
|
3628
|
+
}
|
|
3629
|
+
return Math.round(num / multiple) * multiple;
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
registry.set("QUOTIENT", {
|
|
3633
|
+
minArgs: 2,
|
|
3634
|
+
maxArgs: 2,
|
|
3635
|
+
evaluate(args, context, evaluator) {
|
|
3636
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
3637
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
3638
|
+
const num = toNumber(rawNum);
|
|
3639
|
+
if (num instanceof FormulaError) return num;
|
|
3640
|
+
const rawDen = evaluator.evaluate(args[1], context);
|
|
3641
|
+
if (rawDen instanceof FormulaError) return rawDen;
|
|
3642
|
+
const den = toNumber(rawDen);
|
|
3643
|
+
if (den instanceof FormulaError) return den;
|
|
3644
|
+
if (den === 0) return new FormulaError("#DIV/0!", "QUOTIENT: division by zero");
|
|
3645
|
+
return Math.trunc(num / den);
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
registry.set("COMBIN", {
|
|
3649
|
+
minArgs: 2,
|
|
3650
|
+
maxArgs: 2,
|
|
3651
|
+
evaluate(args, context, evaluator) {
|
|
3652
|
+
const rawN = evaluator.evaluate(args[0], context);
|
|
3653
|
+
if (rawN instanceof FormulaError) return rawN;
|
|
3654
|
+
const n = toNumber(rawN);
|
|
3655
|
+
if (n instanceof FormulaError) return n;
|
|
3656
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
3657
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
3658
|
+
const k = toNumber(rawK);
|
|
3659
|
+
if (k instanceof FormulaError) return k;
|
|
3660
|
+
const ni = Math.trunc(n);
|
|
3661
|
+
const ki = Math.trunc(k);
|
|
3662
|
+
if (ni < 0 || ki < 0) return new FormulaError("#NUM!", "COMBIN: n and k must be non-negative");
|
|
3663
|
+
if (ki > ni) return new FormulaError("#NUM!", "COMBIN: k must be <= n");
|
|
3664
|
+
if (ki === 0 || ki === ni) return 1;
|
|
3665
|
+
const kk = Math.min(ki, ni - ki);
|
|
3666
|
+
let result = 1;
|
|
3667
|
+
for (let i = 0; i < kk; i++) {
|
|
3668
|
+
result = result * (ni - i) / (i + 1);
|
|
3669
|
+
}
|
|
3670
|
+
return Math.round(result);
|
|
3671
|
+
}
|
|
3672
|
+
});
|
|
3673
|
+
registry.set("PERMUT", {
|
|
3674
|
+
minArgs: 2,
|
|
3675
|
+
maxArgs: 2,
|
|
3676
|
+
evaluate(args, context, evaluator) {
|
|
3677
|
+
const rawN = evaluator.evaluate(args[0], context);
|
|
3678
|
+
if (rawN instanceof FormulaError) return rawN;
|
|
3679
|
+
const n = toNumber(rawN);
|
|
3680
|
+
if (n instanceof FormulaError) return n;
|
|
3681
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
3682
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
3683
|
+
const k = toNumber(rawK);
|
|
3684
|
+
if (k instanceof FormulaError) return k;
|
|
3685
|
+
const ni = Math.trunc(n);
|
|
3686
|
+
const ki = Math.trunc(k);
|
|
3687
|
+
if (ni < 0 || ki < 0) return new FormulaError("#NUM!", "PERMUT: n and k must be non-negative");
|
|
3688
|
+
if (ki > ni) return new FormulaError("#NUM!", "PERMUT: k must be <= n");
|
|
3689
|
+
let result = 1;
|
|
3690
|
+
for (let i = 0; i < ki; i++) {
|
|
3691
|
+
result *= ni - i;
|
|
3692
|
+
}
|
|
3693
|
+
return result;
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
registry.set("FACT", {
|
|
3697
|
+
minArgs: 1,
|
|
3698
|
+
maxArgs: 1,
|
|
3699
|
+
evaluate(args, context, evaluator) {
|
|
3700
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
3701
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3702
|
+
const num = toNumber(rawVal);
|
|
3703
|
+
if (num instanceof FormulaError) return num;
|
|
3704
|
+
const n = Math.trunc(num);
|
|
3705
|
+
if (n < 0) return new FormulaError("#NUM!", "FACT: argument must be non-negative");
|
|
3706
|
+
if (n > 170) return new FormulaError("#NUM!", "FACT: argument too large (>170)");
|
|
3707
|
+
let result = 1;
|
|
3708
|
+
for (let i = 2; i <= n; i++) {
|
|
3709
|
+
result *= i;
|
|
3710
|
+
}
|
|
3711
|
+
return result;
|
|
3712
|
+
}
|
|
3713
|
+
});
|
|
3714
|
+
registry.set("GCD", {
|
|
3715
|
+
minArgs: 1,
|
|
3716
|
+
maxArgs: -1,
|
|
3717
|
+
evaluate(args, context, evaluator) {
|
|
3718
|
+
const nums = [];
|
|
3719
|
+
for (const arg of args) {
|
|
3720
|
+
const rawVal = evaluator.evaluate(arg, context);
|
|
3721
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3722
|
+
const v = toNumber(rawVal);
|
|
3723
|
+
if (v instanceof FormulaError) return v;
|
|
3724
|
+
const n = Math.trunc(Math.abs(v));
|
|
3725
|
+
nums.push(n);
|
|
3726
|
+
}
|
|
3727
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "GCD: no arguments");
|
|
3728
|
+
let result = nums[0];
|
|
3729
|
+
for (let i = 1; i < nums.length; i++) {
|
|
3730
|
+
result = gcdTwo(result, nums[i]);
|
|
3731
|
+
}
|
|
3732
|
+
return result;
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
registry.set("LCM", {
|
|
3736
|
+
minArgs: 1,
|
|
3737
|
+
maxArgs: -1,
|
|
3738
|
+
evaluate(args, context, evaluator) {
|
|
3739
|
+
const nums = [];
|
|
3740
|
+
for (const arg of args) {
|
|
3741
|
+
const rawVal = evaluator.evaluate(arg, context);
|
|
3742
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
3743
|
+
const v = toNumber(rawVal);
|
|
3744
|
+
if (v instanceof FormulaError) return v;
|
|
3745
|
+
const n = Math.trunc(Math.abs(v));
|
|
3746
|
+
nums.push(n);
|
|
3747
|
+
}
|
|
3748
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "LCM: no arguments");
|
|
3749
|
+
let result = nums[0];
|
|
3750
|
+
for (let i = 1; i < nums.length; i++) {
|
|
3751
|
+
const g = gcdTwo(result, nums[i]);
|
|
3752
|
+
if (g === 0) {
|
|
3753
|
+
result = 0;
|
|
3754
|
+
break;
|
|
3755
|
+
}
|
|
3756
|
+
result = result / g * nums[i];
|
|
3757
|
+
}
|
|
3758
|
+
return result;
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
function gcdTwo(a, b) {
|
|
3763
|
+
while (b !== 0) {
|
|
3764
|
+
const t = b;
|
|
3765
|
+
b = a % b;
|
|
3766
|
+
a = t;
|
|
3767
|
+
}
|
|
3768
|
+
return a;
|
|
3457
3769
|
}
|
|
3458
3770
|
|
|
3459
3771
|
// src/formula/functions/logical.ts
|
|
@@ -4294,36 +4606,182 @@ function registerTextFunctions(registry) {
|
|
|
4294
4606
|
return parts.join(delimiter);
|
|
4295
4607
|
}
|
|
4296
4608
|
});
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
}
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4609
|
+
registry.set("DOLLAR", {
|
|
4610
|
+
minArgs: 1,
|
|
4611
|
+
maxArgs: 2,
|
|
4612
|
+
evaluate(args, context, evaluator) {
|
|
4613
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
4614
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
4615
|
+
const num = toNumber(rawNum);
|
|
4616
|
+
if (num instanceof FormulaError) return num;
|
|
4617
|
+
let decimals = 2;
|
|
4618
|
+
if (args.length >= 2) {
|
|
4619
|
+
const rawDec = evaluator.evaluate(args[1], context);
|
|
4620
|
+
if (rawDec instanceof FormulaError) return rawDec;
|
|
4621
|
+
const d = toNumber(rawDec);
|
|
4622
|
+
if (d instanceof FormulaError) return d;
|
|
4623
|
+
decimals = Math.trunc(d);
|
|
4624
|
+
}
|
|
4625
|
+
const absNum = Math.abs(num);
|
|
4626
|
+
const rounded = decimals >= 0 ? absNum.toFixed(decimals) : (Math.round(absNum / Math.pow(10, -decimals)) * Math.pow(10, -decimals)).toFixed(0);
|
|
4627
|
+
const [intPart, decPart] = rounded.split(".");
|
|
4628
|
+
const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
4629
|
+
const formatted = decPart !== void 0 ? `${withCommas}.${decPart}` : withCommas;
|
|
4630
|
+
return num < 0 ? `($${formatted})` : `$${formatted}`;
|
|
4631
|
+
}
|
|
4632
|
+
});
|
|
4633
|
+
registry.set("FIXED", {
|
|
4634
|
+
minArgs: 1,
|
|
4635
|
+
maxArgs: 3,
|
|
4636
|
+
evaluate(args, context, evaluator) {
|
|
4637
|
+
const rawNum = evaluator.evaluate(args[0], context);
|
|
4638
|
+
if (rawNum instanceof FormulaError) return rawNum;
|
|
4639
|
+
const num = toNumber(rawNum);
|
|
4640
|
+
if (num instanceof FormulaError) return num;
|
|
4641
|
+
let decimals = 2;
|
|
4642
|
+
if (args.length >= 2) {
|
|
4643
|
+
const rawDec = evaluator.evaluate(args[1], context);
|
|
4644
|
+
if (rawDec instanceof FormulaError) return rawDec;
|
|
4645
|
+
const d = toNumber(rawDec);
|
|
4646
|
+
if (d instanceof FormulaError) return d;
|
|
4647
|
+
decimals = Math.trunc(d);
|
|
4648
|
+
}
|
|
4649
|
+
let noCommas = false;
|
|
4650
|
+
if (args.length >= 3) {
|
|
4651
|
+
const rawNoCommas = evaluator.evaluate(args[2], context);
|
|
4652
|
+
if (rawNoCommas instanceof FormulaError) return rawNoCommas;
|
|
4653
|
+
noCommas = !!rawNoCommas;
|
|
4654
|
+
}
|
|
4655
|
+
const absNum = Math.abs(num);
|
|
4656
|
+
const rounded = decimals >= 0 ? absNum.toFixed(decimals) : (Math.round(absNum / Math.pow(10, -decimals)) * Math.pow(10, -decimals)).toFixed(0);
|
|
4657
|
+
if (noCommas) {
|
|
4658
|
+
return num < 0 ? `-${rounded}` : rounded;
|
|
4659
|
+
}
|
|
4660
|
+
const [intPart, decPart] = rounded.split(".");
|
|
4661
|
+
const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
4662
|
+
const formatted = decPart !== void 0 ? `${withCommas}.${decPart}` : withCommas;
|
|
4663
|
+
return num < 0 ? `-${formatted}` : formatted;
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4666
|
+
registry.set("T", {
|
|
4667
|
+
minArgs: 1,
|
|
4668
|
+
maxArgs: 1,
|
|
4669
|
+
evaluate(args, context, evaluator) {
|
|
4670
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4671
|
+
if (val instanceof FormulaError) return val;
|
|
4672
|
+
return typeof val === "string" ? val : "";
|
|
4673
|
+
}
|
|
4674
|
+
});
|
|
4675
|
+
registry.set("N", {
|
|
4676
|
+
minArgs: 1,
|
|
4677
|
+
maxArgs: 1,
|
|
4678
|
+
evaluate(args, context, evaluator) {
|
|
4679
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4680
|
+
if (val instanceof FormulaError) return val;
|
|
4681
|
+
if (typeof val === "number") return val;
|
|
4682
|
+
if (typeof val === "boolean") return val ? 1 : 0;
|
|
4683
|
+
if (val instanceof Date) return val.getTime();
|
|
4684
|
+
return 0;
|
|
4685
|
+
}
|
|
4686
|
+
});
|
|
4687
|
+
registry.set("FORMULATEXT", {
|
|
4688
|
+
minArgs: 1,
|
|
4689
|
+
maxArgs: 1,
|
|
4690
|
+
evaluate(args, context, _evaluator) {
|
|
4691
|
+
const arg = args[0];
|
|
4692
|
+
if (arg.kind !== "cellRef") {
|
|
4693
|
+
return new FormulaError("#N/A", "FORMULATEXT requires a cell reference");
|
|
4694
|
+
}
|
|
4695
|
+
if (!context.getCellFormula) {
|
|
4696
|
+
return new FormulaError("#N/A", "FORMULATEXT not supported in this context");
|
|
4697
|
+
}
|
|
4698
|
+
const formula = context.getCellFormula(arg.address);
|
|
4699
|
+
if (formula === void 0) {
|
|
4700
|
+
return new FormulaError("#N/A", "Cell does not contain a formula");
|
|
4701
|
+
}
|
|
4702
|
+
return formula;
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
registry.set("NUMBERVALUE", {
|
|
4706
|
+
minArgs: 1,
|
|
4707
|
+
maxArgs: 3,
|
|
4708
|
+
evaluate(args, context, evaluator) {
|
|
4709
|
+
const rawText = evaluator.evaluate(args[0], context);
|
|
4710
|
+
if (rawText instanceof FormulaError) return rawText;
|
|
4711
|
+
if (typeof rawText === "number") return rawText;
|
|
4712
|
+
let text = toString(rawText).trim();
|
|
4713
|
+
let decimalSep = ".";
|
|
4714
|
+
let groupSep = ",";
|
|
4715
|
+
const hasDecimalArg = args.length >= 2;
|
|
4716
|
+
const hasGroupArg = args.length >= 3;
|
|
4717
|
+
if (hasDecimalArg) {
|
|
4718
|
+
const rawDec = evaluator.evaluate(args[1], context);
|
|
4719
|
+
if (rawDec instanceof FormulaError) return rawDec;
|
|
4720
|
+
decimalSep = toString(rawDec);
|
|
4721
|
+
if (decimalSep.length !== 1) return new FormulaError("#VALUE!", "NUMBERVALUE decimal separator must be 1 character");
|
|
4722
|
+
if (!hasGroupArg) {
|
|
4723
|
+
groupSep = decimalSep === "," ? "." : ",";
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
if (hasGroupArg) {
|
|
4727
|
+
const rawGrp = evaluator.evaluate(args[2], context);
|
|
4728
|
+
if (rawGrp instanceof FormulaError) return rawGrp;
|
|
4729
|
+
groupSep = toString(rawGrp);
|
|
4730
|
+
if (groupSep.length !== 1) return new FormulaError("#VALUE!", "NUMBERVALUE group separator must be 1 character");
|
|
4731
|
+
}
|
|
4732
|
+
if (decimalSep === groupSep) return new FormulaError("#VALUE!", "NUMBERVALUE separators must be different");
|
|
4733
|
+
const isPercent = text.endsWith("%");
|
|
4734
|
+
if (isPercent) text = text.slice(0, -1).trim();
|
|
4735
|
+
const escapedGroup = groupSep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4736
|
+
text = text.replace(new RegExp(escapedGroup, "g"), "");
|
|
4737
|
+
if (decimalSep !== ".") {
|
|
4738
|
+
const escapedDec = decimalSep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4739
|
+
text = text.replace(new RegExp(escapedDec), ".");
|
|
4740
|
+
}
|
|
4741
|
+
const n = Number(text);
|
|
4742
|
+
if (isNaN(n)) return new FormulaError("#VALUE!", `NUMBERVALUE cannot parse "${toString(rawText)}"`);
|
|
4743
|
+
return isPercent ? n / 100 : n;
|
|
4744
|
+
}
|
|
4745
|
+
});
|
|
4746
|
+
registry.set("PHONETIC", {
|
|
4747
|
+
minArgs: 1,
|
|
4748
|
+
maxArgs: 1,
|
|
4749
|
+
evaluate(args, context, evaluator) {
|
|
4750
|
+
const val = evaluator.evaluate(args[0], context);
|
|
4751
|
+
if (val instanceof FormulaError) return val;
|
|
4752
|
+
return toString(val);
|
|
4753
|
+
}
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
// src/formula/functions/date.ts
|
|
4758
|
+
function toDate(val) {
|
|
4759
|
+
if (val instanceof FormulaError) return val;
|
|
4760
|
+
if (val instanceof Date) {
|
|
4761
|
+
if (isNaN(val.getTime())) return new FormulaError("#VALUE!", "Invalid date");
|
|
4762
|
+
return val;
|
|
4763
|
+
}
|
|
4764
|
+
if (typeof val === "string") {
|
|
4765
|
+
const d = new Date(val);
|
|
4766
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `Cannot parse "${val}" as date`);
|
|
4767
|
+
return d;
|
|
4768
|
+
}
|
|
4769
|
+
if (typeof val === "number") {
|
|
4770
|
+
const d = new Date(val);
|
|
4771
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", "Invalid numeric date");
|
|
4772
|
+
return d;
|
|
4773
|
+
}
|
|
4774
|
+
return new FormulaError("#VALUE!", "Cannot convert value to date");
|
|
4775
|
+
}
|
|
4776
|
+
function registerDateFunctions(registry) {
|
|
4777
|
+
registry.set("TODAY", {
|
|
4778
|
+
minArgs: 0,
|
|
4779
|
+
maxArgs: 0,
|
|
4780
|
+
evaluate(_args, context) {
|
|
4781
|
+
const now = context.now();
|
|
4782
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4327
4785
|
registry.set("NOW", {
|
|
4328
4786
|
minArgs: 0,
|
|
4329
4787
|
maxArgs: 0,
|
|
@@ -4543,6 +5001,311 @@ function registerDateFunctions(registry) {
|
|
|
4543
5001
|
return count * sign;
|
|
4544
5002
|
}
|
|
4545
5003
|
});
|
|
5004
|
+
registry.set("DAYS", {
|
|
5005
|
+
minArgs: 2,
|
|
5006
|
+
maxArgs: 2,
|
|
5007
|
+
evaluate(args, context, evaluator) {
|
|
5008
|
+
const rawEnd = evaluator.evaluate(args[0], context);
|
|
5009
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
5010
|
+
const endDate = toDate(rawEnd);
|
|
5011
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
5012
|
+
const rawStart = evaluator.evaluate(args[1], context);
|
|
5013
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
5014
|
+
const startDate = toDate(rawStart);
|
|
5015
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
5016
|
+
const endMs = Date.UTC(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
|
5017
|
+
const startMs = Date.UTC(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
|
5018
|
+
return Math.round((endMs - startMs) / 864e5);
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
registry.set("DAYS360", {
|
|
5022
|
+
minArgs: 2,
|
|
5023
|
+
maxArgs: 3,
|
|
5024
|
+
evaluate(args, context, evaluator) {
|
|
5025
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
5026
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
5027
|
+
const startDate = toDate(rawStart);
|
|
5028
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
5029
|
+
const rawEnd = evaluator.evaluate(args[1], context);
|
|
5030
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
5031
|
+
const endDate = toDate(rawEnd);
|
|
5032
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
5033
|
+
let method = false;
|
|
5034
|
+
if (args.length >= 3) {
|
|
5035
|
+
const rawMethod = evaluator.evaluate(args[2], context);
|
|
5036
|
+
if (rawMethod instanceof FormulaError) return rawMethod;
|
|
5037
|
+
method = !!rawMethod;
|
|
5038
|
+
}
|
|
5039
|
+
const sm = startDate.getMonth() + 1;
|
|
5040
|
+
const em = endDate.getMonth() + 1;
|
|
5041
|
+
let sd = startDate.getDate();
|
|
5042
|
+
let ed = endDate.getDate();
|
|
5043
|
+
const sy = startDate.getFullYear();
|
|
5044
|
+
const ey = endDate.getFullYear();
|
|
5045
|
+
if (!method) {
|
|
5046
|
+
if (sd === 31) sd = 30;
|
|
5047
|
+
if (ed === 31 && sd === 30) ed = 30;
|
|
5048
|
+
} else {
|
|
5049
|
+
if (sd === 31) sd = 30;
|
|
5050
|
+
if (ed === 31) ed = 30;
|
|
5051
|
+
}
|
|
5052
|
+
return (ey - sy) * 360 + (em - sm) * 30 + (ed - sd);
|
|
5053
|
+
}
|
|
5054
|
+
});
|
|
5055
|
+
registry.set("ISOWEEKNUM", {
|
|
5056
|
+
minArgs: 1,
|
|
5057
|
+
maxArgs: 1,
|
|
5058
|
+
evaluate(args, context, evaluator) {
|
|
5059
|
+
const rawDate = evaluator.evaluate(args[0], context);
|
|
5060
|
+
if (rawDate instanceof FormulaError) return rawDate;
|
|
5061
|
+
const date = toDate(rawDate);
|
|
5062
|
+
if (date instanceof FormulaError) return date;
|
|
5063
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
5064
|
+
const day = d.getUTCDay() || 7;
|
|
5065
|
+
d.setUTCDate(d.getUTCDate() + 4 - day);
|
|
5066
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
5067
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
5068
|
+
}
|
|
5069
|
+
});
|
|
5070
|
+
registry.set("YEARFRAC", {
|
|
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 rawEnd = evaluator.evaluate(args[1], context);
|
|
5079
|
+
if (rawEnd instanceof FormulaError) return rawEnd;
|
|
5080
|
+
const endDate = toDate(rawEnd);
|
|
5081
|
+
if (endDate instanceof FormulaError) return endDate;
|
|
5082
|
+
let basis = 0;
|
|
5083
|
+
if (args.length >= 3) {
|
|
5084
|
+
const rawBasis = evaluator.evaluate(args[2], context);
|
|
5085
|
+
if (rawBasis instanceof FormulaError) return rawBasis;
|
|
5086
|
+
const b = toNumber(rawBasis);
|
|
5087
|
+
if (b instanceof FormulaError) return b;
|
|
5088
|
+
basis = Math.trunc(b);
|
|
5089
|
+
}
|
|
5090
|
+
const sy = startDate.getFullYear();
|
|
5091
|
+
const sm = startDate.getMonth() + 1;
|
|
5092
|
+
const sd = startDate.getDate();
|
|
5093
|
+
const ey = endDate.getFullYear();
|
|
5094
|
+
const em = endDate.getMonth() + 1;
|
|
5095
|
+
const ed = endDate.getDate();
|
|
5096
|
+
switch (basis) {
|
|
5097
|
+
case 0: {
|
|
5098
|
+
const startDay = sd === 31 ? 30 : sd;
|
|
5099
|
+
const endDay = ed === 31 && startDay === 30 ? 30 : ed;
|
|
5100
|
+
const days360 = (ey - sy) * 360 + (em - sm) * 30 + (endDay - startDay);
|
|
5101
|
+
return days360 / 360;
|
|
5102
|
+
}
|
|
5103
|
+
case 1: {
|
|
5104
|
+
const diffMs = Date.UTC(ey, em - 1, ed) - Date.UTC(sy, sm - 1, sd);
|
|
5105
|
+
const diffDays = diffMs / 864e5;
|
|
5106
|
+
const avgYear = ey === sy ? isLeapYear(sy) ? 366 : 365 : (Date.UTC(ey + 1, 0, 1) - Date.UTC(sy, 0, 1)) / 864e5 / (ey - sy + 1);
|
|
5107
|
+
return diffDays / avgYear;
|
|
5108
|
+
}
|
|
5109
|
+
case 3: {
|
|
5110
|
+
const diffMs = Date.UTC(ey, em - 1, ed) - Date.UTC(sy, sm - 1, sd);
|
|
5111
|
+
return diffMs / 864e5 / 365;
|
|
5112
|
+
}
|
|
5113
|
+
default:
|
|
5114
|
+
return new FormulaError("#VALUE!", "YEARFRAC basis must be 0, 1, or 3");
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
});
|
|
5118
|
+
registry.set("DATEVALUE", {
|
|
5119
|
+
minArgs: 1,
|
|
5120
|
+
maxArgs: 1,
|
|
5121
|
+
evaluate(args, context, evaluator) {
|
|
5122
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
5123
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
5124
|
+
const str = typeof rawVal === "string" ? rawVal : String(rawVal);
|
|
5125
|
+
const d = new Date(str);
|
|
5126
|
+
if (isNaN(d.getTime())) return new FormulaError("#VALUE!", `DATEVALUE cannot parse "${str}"`);
|
|
5127
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
5128
|
+
}
|
|
5129
|
+
});
|
|
5130
|
+
registry.set("TIMEVALUE", {
|
|
5131
|
+
minArgs: 1,
|
|
5132
|
+
maxArgs: 1,
|
|
5133
|
+
evaluate(args, context, evaluator) {
|
|
5134
|
+
const rawVal = evaluator.evaluate(args[0], context);
|
|
5135
|
+
if (rawVal instanceof FormulaError) return rawVal;
|
|
5136
|
+
const str = typeof rawVal === "string" ? rawVal : String(rawVal);
|
|
5137
|
+
const match = str.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*(AM|PM))?$/i);
|
|
5138
|
+
if (!match) return new FormulaError("#VALUE!", `TIMEVALUE cannot parse "${str}"`);
|
|
5139
|
+
let hours = parseInt(match[1], 10);
|
|
5140
|
+
const minutes = parseInt(match[2], 10);
|
|
5141
|
+
const seconds = match[3] ? parseInt(match[3], 10) : 0;
|
|
5142
|
+
const ampm = match[4] ? match[4].toUpperCase() : null;
|
|
5143
|
+
if (ampm === "PM" && hours < 12) hours += 12;
|
|
5144
|
+
if (ampm === "AM" && hours === 12) hours = 0;
|
|
5145
|
+
if (hours > 23 || minutes > 59 || seconds > 59) {
|
|
5146
|
+
return new FormulaError("#VALUE!", "TIMEVALUE: invalid time component");
|
|
5147
|
+
}
|
|
5148
|
+
return (hours * 3600 + minutes * 60 + seconds) / 86400;
|
|
5149
|
+
}
|
|
5150
|
+
});
|
|
5151
|
+
registry.set("TIME", {
|
|
5152
|
+
minArgs: 3,
|
|
5153
|
+
maxArgs: 3,
|
|
5154
|
+
evaluate(args, context, evaluator) {
|
|
5155
|
+
const rawH = evaluator.evaluate(args[0], context);
|
|
5156
|
+
if (rawH instanceof FormulaError) return rawH;
|
|
5157
|
+
const h = toNumber(rawH);
|
|
5158
|
+
if (h instanceof FormulaError) return h;
|
|
5159
|
+
const rawM = evaluator.evaluate(args[1], context);
|
|
5160
|
+
if (rawM instanceof FormulaError) return rawM;
|
|
5161
|
+
const m = toNumber(rawM);
|
|
5162
|
+
if (m instanceof FormulaError) return m;
|
|
5163
|
+
const rawS = evaluator.evaluate(args[2], context);
|
|
5164
|
+
if (rawS instanceof FormulaError) return rawS;
|
|
5165
|
+
const s = toNumber(rawS);
|
|
5166
|
+
if (s instanceof FormulaError) return s;
|
|
5167
|
+
const totalSeconds = Math.trunc(h) * 3600 + Math.trunc(m) * 60 + Math.trunc(s);
|
|
5168
|
+
return totalSeconds % 86400 / 86400;
|
|
5169
|
+
}
|
|
5170
|
+
});
|
|
5171
|
+
registry.set("WORKDAY", {
|
|
5172
|
+
minArgs: 2,
|
|
5173
|
+
maxArgs: 3,
|
|
5174
|
+
evaluate(args, context, evaluator) {
|
|
5175
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
5176
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
5177
|
+
const startDate = toDate(rawStart);
|
|
5178
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
5179
|
+
const rawDays = evaluator.evaluate(args[1], context);
|
|
5180
|
+
if (rawDays instanceof FormulaError) return rawDays;
|
|
5181
|
+
const daysNum = toNumber(rawDays);
|
|
5182
|
+
if (daysNum instanceof FormulaError) return daysNum;
|
|
5183
|
+
const days = Math.trunc(daysNum);
|
|
5184
|
+
const holidaySet = /* @__PURE__ */ new Set();
|
|
5185
|
+
if (args.length >= 3) {
|
|
5186
|
+
const rawHol = args[2];
|
|
5187
|
+
let holVals;
|
|
5188
|
+
if (rawHol.kind === "range") {
|
|
5189
|
+
holVals = context.getRangeValues({ start: rawHol.start, end: rawHol.end }).flat();
|
|
5190
|
+
} else {
|
|
5191
|
+
holVals = [evaluator.evaluate(rawHol, context)];
|
|
5192
|
+
}
|
|
5193
|
+
for (const hv of holVals) {
|
|
5194
|
+
if (hv === null || hv === void 0) continue;
|
|
5195
|
+
const hd = toDate(hv);
|
|
5196
|
+
if (!(hd instanceof FormulaError)) {
|
|
5197
|
+
holidaySet.add(`${hd.getFullYear()}-${hd.getMonth()}-${hd.getDate()}`);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
const current = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
|
5202
|
+
const step = days >= 0 ? 1 : -1;
|
|
5203
|
+
let remaining = Math.abs(days);
|
|
5204
|
+
while (remaining > 0) {
|
|
5205
|
+
current.setDate(current.getDate() + step);
|
|
5206
|
+
const dow = current.getDay();
|
|
5207
|
+
if (dow === 0 || dow === 6) continue;
|
|
5208
|
+
const key = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}`;
|
|
5209
|
+
if (holidaySet.has(key)) continue;
|
|
5210
|
+
remaining--;
|
|
5211
|
+
}
|
|
5212
|
+
return current;
|
|
5213
|
+
}
|
|
5214
|
+
});
|
|
5215
|
+
registry.set("WORKDAY.INTL", {
|
|
5216
|
+
minArgs: 2,
|
|
5217
|
+
maxArgs: 4,
|
|
5218
|
+
evaluate(args, context, evaluator) {
|
|
5219
|
+
const rawStart = evaluator.evaluate(args[0], context);
|
|
5220
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
5221
|
+
const startDate = toDate(rawStart);
|
|
5222
|
+
if (startDate instanceof FormulaError) return startDate;
|
|
5223
|
+
const rawDays = evaluator.evaluate(args[1], context);
|
|
5224
|
+
if (rawDays instanceof FormulaError) return rawDays;
|
|
5225
|
+
const daysNum = toNumber(rawDays);
|
|
5226
|
+
if (daysNum instanceof FormulaError) return daysNum;
|
|
5227
|
+
const days = Math.trunc(daysNum);
|
|
5228
|
+
let weekendMask = [false, false, false, false, false, true, true];
|
|
5229
|
+
if (args.length >= 3) {
|
|
5230
|
+
const rawWeekend = evaluator.evaluate(args[2], context);
|
|
5231
|
+
if (rawWeekend instanceof FormulaError) return rawWeekend;
|
|
5232
|
+
if (typeof rawWeekend === "string" && /^[01]{7}$/.test(rawWeekend)) {
|
|
5233
|
+
weekendMask = rawWeekend.split("").map((c) => c === "1");
|
|
5234
|
+
} else {
|
|
5235
|
+
const wn = toNumber(rawWeekend);
|
|
5236
|
+
if (wn instanceof FormulaError) return wn;
|
|
5237
|
+
const parsed = parseWeekendNumber(Math.trunc(wn));
|
|
5238
|
+
if (!parsed) return new FormulaError("#VALUE!", "WORKDAY.INTL invalid weekend number");
|
|
5239
|
+
weekendMask = parsed;
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
const holidaySet = /* @__PURE__ */ new Set();
|
|
5243
|
+
if (args.length >= 4) {
|
|
5244
|
+
const rawHol = args[3];
|
|
5245
|
+
let holVals;
|
|
5246
|
+
if (rawHol.kind === "range") {
|
|
5247
|
+
holVals = context.getRangeValues({ start: rawHol.start, end: rawHol.end }).flat();
|
|
5248
|
+
} else {
|
|
5249
|
+
holVals = [evaluator.evaluate(rawHol, context)];
|
|
5250
|
+
}
|
|
5251
|
+
for (const hv of holVals) {
|
|
5252
|
+
if (hv === null || hv === void 0) continue;
|
|
5253
|
+
const hd = toDate(hv);
|
|
5254
|
+
if (!(hd instanceof FormulaError)) {
|
|
5255
|
+
holidaySet.add(`${hd.getFullYear()}-${hd.getMonth()}-${hd.getDate()}`);
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
const current = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
|
5260
|
+
const step = days >= 0 ? 1 : -1;
|
|
5261
|
+
let remaining = Math.abs(days);
|
|
5262
|
+
while (remaining > 0) {
|
|
5263
|
+
current.setDate(current.getDate() + step);
|
|
5264
|
+
const dow = current.getDay();
|
|
5265
|
+
const maskIndex = dow === 0 ? 6 : dow - 1;
|
|
5266
|
+
if (weekendMask[maskIndex]) continue;
|
|
5267
|
+
const key = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}`;
|
|
5268
|
+
if (holidaySet.has(key)) continue;
|
|
5269
|
+
remaining--;
|
|
5270
|
+
}
|
|
5271
|
+
return current;
|
|
5272
|
+
}
|
|
5273
|
+
});
|
|
5274
|
+
}
|
|
5275
|
+
function isLeapYear(year) {
|
|
5276
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
5277
|
+
}
|
|
5278
|
+
function parseWeekendNumber(n) {
|
|
5279
|
+
const twoDay = [
|
|
5280
|
+
[5, 6],
|
|
5281
|
+
// 1: Sat+Sun
|
|
5282
|
+
[6, 0],
|
|
5283
|
+
// 2: Sun+Mon (Sun=index6, Mon=index0)
|
|
5284
|
+
[0, 1],
|
|
5285
|
+
// 3: Mon+Tue
|
|
5286
|
+
[1, 2],
|
|
5287
|
+
// 4: Tue+Wed
|
|
5288
|
+
[2, 3],
|
|
5289
|
+
// 5: Wed+Thu
|
|
5290
|
+
[3, 4],
|
|
5291
|
+
// 6: Thu+Fri
|
|
5292
|
+
[4, 5]
|
|
5293
|
+
// 7: Fri+Sat
|
|
5294
|
+
];
|
|
5295
|
+
if (n >= 1 && n <= 7) {
|
|
5296
|
+
const mask = [false, false, false, false, false, false, false];
|
|
5297
|
+
const [a, b] = twoDay[n - 1];
|
|
5298
|
+
mask[a] = true;
|
|
5299
|
+
mask[b] = true;
|
|
5300
|
+
return mask;
|
|
5301
|
+
}
|
|
5302
|
+
if (n >= 11 && n <= 17) {
|
|
5303
|
+
const mask = [false, false, false, false, false, false, false];
|
|
5304
|
+
const singleDay = [6, 0, 1, 2, 3, 4, 5];
|
|
5305
|
+
mask[singleDay[n - 11]] = true;
|
|
5306
|
+
return mask;
|
|
5307
|
+
}
|
|
5308
|
+
return null;
|
|
4546
5309
|
}
|
|
4547
5310
|
|
|
4548
5311
|
// src/formula/functions/stats.ts
|
|
@@ -4906,18 +5669,1067 @@ function registerInfoFunctions(registry) {
|
|
|
4906
5669
|
return 1;
|
|
4907
5670
|
}
|
|
4908
5671
|
});
|
|
5672
|
+
registry.set("ISODD", {
|
|
5673
|
+
minArgs: 1,
|
|
5674
|
+
maxArgs: 1,
|
|
5675
|
+
evaluate(args, context, evaluator) {
|
|
5676
|
+
const val = evaluator.evaluate(args[0], context);
|
|
5677
|
+
if (val instanceof FormulaError) return val;
|
|
5678
|
+
if (typeof val === "boolean") return new FormulaError("#VALUE!", "ISODD requires a number");
|
|
5679
|
+
const n = toNumber(val);
|
|
5680
|
+
if (n instanceof FormulaError) return n;
|
|
5681
|
+
return Math.trunc(n) % 2 !== 0;
|
|
5682
|
+
}
|
|
5683
|
+
});
|
|
5684
|
+
registry.set("ISEVEN", {
|
|
5685
|
+
minArgs: 1,
|
|
5686
|
+
maxArgs: 1,
|
|
5687
|
+
evaluate(args, context, evaluator) {
|
|
5688
|
+
const val = evaluator.evaluate(args[0], context);
|
|
5689
|
+
if (val instanceof FormulaError) return val;
|
|
5690
|
+
if (typeof val === "boolean") return new FormulaError("#VALUE!", "ISEVEN requires a number");
|
|
5691
|
+
const n = toNumber(val);
|
|
5692
|
+
if (n instanceof FormulaError) return n;
|
|
5693
|
+
return Math.trunc(n) % 2 === 0;
|
|
5694
|
+
}
|
|
5695
|
+
});
|
|
5696
|
+
registry.set("ISFORMULA", {
|
|
5697
|
+
minArgs: 1,
|
|
5698
|
+
maxArgs: 1,
|
|
5699
|
+
evaluate(args, context, _evaluator) {
|
|
5700
|
+
const arg = args[0];
|
|
5701
|
+
if (arg.kind !== "cellRef") return false;
|
|
5702
|
+
if (!context.getCellFormula) return false;
|
|
5703
|
+
const formula = context.getCellFormula(arg.address);
|
|
5704
|
+
return formula !== void 0;
|
|
5705
|
+
}
|
|
5706
|
+
});
|
|
5707
|
+
registry.set("ISLOGICAL", {
|
|
5708
|
+
minArgs: 1,
|
|
5709
|
+
maxArgs: 1,
|
|
5710
|
+
evaluate(args, context, evaluator) {
|
|
5711
|
+
const val = evaluator.evaluate(args[0], context);
|
|
5712
|
+
if (val instanceof FormulaError) return false;
|
|
5713
|
+
return typeof val === "boolean";
|
|
5714
|
+
}
|
|
5715
|
+
});
|
|
5716
|
+
registry.set("ISNONTEXT", {
|
|
5717
|
+
minArgs: 1,
|
|
5718
|
+
maxArgs: 1,
|
|
5719
|
+
evaluate(args, context, evaluator) {
|
|
5720
|
+
const val = evaluator.evaluate(args[0], context);
|
|
5721
|
+
if (val instanceof FormulaError) return true;
|
|
5722
|
+
return typeof val !== "string";
|
|
5723
|
+
}
|
|
5724
|
+
});
|
|
5725
|
+
registry.set("ISREF", {
|
|
5726
|
+
minArgs: 1,
|
|
5727
|
+
maxArgs: 1,
|
|
5728
|
+
evaluate(args, _context, _evaluator) {
|
|
5729
|
+
const arg = args[0];
|
|
5730
|
+
return arg.kind === "cellRef" || arg.kind === "range";
|
|
5731
|
+
}
|
|
5732
|
+
});
|
|
4909
5733
|
}
|
|
4910
5734
|
|
|
4911
|
-
// src/formula/functions/
|
|
4912
|
-
function
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
5735
|
+
// src/formula/functions/financial.ts
|
|
5736
|
+
function registerFinancialFunctions(registry) {
|
|
5737
|
+
registry.set("PMT", {
|
|
5738
|
+
minArgs: 3,
|
|
5739
|
+
maxArgs: 5,
|
|
5740
|
+
evaluate(args, context, evaluator) {
|
|
5741
|
+
const rawRate = evaluator.evaluate(args[0], context);
|
|
5742
|
+
if (rawRate instanceof FormulaError) return rawRate;
|
|
5743
|
+
const rate = toNumber(rawRate);
|
|
5744
|
+
if (rate instanceof FormulaError) return rate;
|
|
5745
|
+
const rawNper = evaluator.evaluate(args[1], context);
|
|
5746
|
+
if (rawNper instanceof FormulaError) return rawNper;
|
|
5747
|
+
const nper = toNumber(rawNper);
|
|
5748
|
+
if (nper instanceof FormulaError) return nper;
|
|
5749
|
+
const rawPv = evaluator.evaluate(args[2], context);
|
|
5750
|
+
if (rawPv instanceof FormulaError) return rawPv;
|
|
5751
|
+
const pv = toNumber(rawPv);
|
|
5752
|
+
if (pv instanceof FormulaError) return pv;
|
|
5753
|
+
let fv = 0;
|
|
5754
|
+
if (args.length >= 4) {
|
|
5755
|
+
const rawFv = evaluator.evaluate(args[3], context);
|
|
5756
|
+
if (rawFv instanceof FormulaError) return rawFv;
|
|
5757
|
+
const fvNum = toNumber(rawFv);
|
|
5758
|
+
if (fvNum instanceof FormulaError) return fvNum;
|
|
5759
|
+
fv = fvNum;
|
|
5760
|
+
}
|
|
5761
|
+
let type = 0;
|
|
5762
|
+
if (args.length >= 5) {
|
|
5763
|
+
const rawType = evaluator.evaluate(args[4], context);
|
|
5764
|
+
if (rawType instanceof FormulaError) return rawType;
|
|
5765
|
+
const typeNum = toNumber(rawType);
|
|
5766
|
+
if (typeNum instanceof FormulaError) return typeNum;
|
|
5767
|
+
type = typeNum;
|
|
5768
|
+
}
|
|
5769
|
+
if (nper === 0) return new FormulaError("#NUM!", "PMT: nper cannot be 0");
|
|
5770
|
+
if (rate === 0) {
|
|
5771
|
+
return -(pv + fv) / nper;
|
|
5772
|
+
}
|
|
5773
|
+
const factor = Math.pow(1 + rate, nper);
|
|
5774
|
+
const typeAdj = type !== 0 ? 1 + rate : 1;
|
|
5775
|
+
return -(pv * factor + fv) / ((factor - 1) / rate * typeAdj);
|
|
5776
|
+
}
|
|
5777
|
+
});
|
|
5778
|
+
registry.set("FV", {
|
|
5779
|
+
minArgs: 3,
|
|
5780
|
+
maxArgs: 5,
|
|
5781
|
+
evaluate(args, context, evaluator) {
|
|
5782
|
+
const rawRate = evaluator.evaluate(args[0], context);
|
|
5783
|
+
if (rawRate instanceof FormulaError) return rawRate;
|
|
5784
|
+
const rate = toNumber(rawRate);
|
|
5785
|
+
if (rate instanceof FormulaError) return rate;
|
|
5786
|
+
const rawNper = evaluator.evaluate(args[1], context);
|
|
5787
|
+
if (rawNper instanceof FormulaError) return rawNper;
|
|
5788
|
+
const nper = toNumber(rawNper);
|
|
5789
|
+
if (nper instanceof FormulaError) return nper;
|
|
5790
|
+
const rawPmt = evaluator.evaluate(args[2], context);
|
|
5791
|
+
if (rawPmt instanceof FormulaError) return rawPmt;
|
|
5792
|
+
const pmt = toNumber(rawPmt);
|
|
5793
|
+
if (pmt instanceof FormulaError) return pmt;
|
|
5794
|
+
let pv = 0;
|
|
5795
|
+
if (args.length >= 4) {
|
|
5796
|
+
const rawPv = evaluator.evaluate(args[3], context);
|
|
5797
|
+
if (rawPv instanceof FormulaError) return rawPv;
|
|
5798
|
+
const pvNum = toNumber(rawPv);
|
|
5799
|
+
if (pvNum instanceof FormulaError) return pvNum;
|
|
5800
|
+
pv = pvNum;
|
|
5801
|
+
}
|
|
5802
|
+
let type = 0;
|
|
5803
|
+
if (args.length >= 5) {
|
|
5804
|
+
const rawType = evaluator.evaluate(args[4], context);
|
|
5805
|
+
if (rawType instanceof FormulaError) return rawType;
|
|
5806
|
+
const typeNum = toNumber(rawType);
|
|
5807
|
+
if (typeNum instanceof FormulaError) return typeNum;
|
|
5808
|
+
type = typeNum;
|
|
5809
|
+
}
|
|
5810
|
+
if (rate === 0) {
|
|
5811
|
+
return -(pv + pmt * nper);
|
|
5812
|
+
}
|
|
5813
|
+
const factor = Math.pow(1 + rate, nper);
|
|
5814
|
+
const typeAdj = type !== 0 ? 1 + rate : 1;
|
|
5815
|
+
return -(pv * factor + pmt * typeAdj * (factor - 1) / rate);
|
|
5816
|
+
}
|
|
5817
|
+
});
|
|
5818
|
+
registry.set("PV", {
|
|
5819
|
+
minArgs: 3,
|
|
5820
|
+
maxArgs: 5,
|
|
5821
|
+
evaluate(args, context, evaluator) {
|
|
5822
|
+
const rawRate = evaluator.evaluate(args[0], context);
|
|
5823
|
+
if (rawRate instanceof FormulaError) return rawRate;
|
|
5824
|
+
const rate = toNumber(rawRate);
|
|
5825
|
+
if (rate instanceof FormulaError) return rate;
|
|
5826
|
+
const rawNper = evaluator.evaluate(args[1], context);
|
|
5827
|
+
if (rawNper instanceof FormulaError) return rawNper;
|
|
5828
|
+
const nper = toNumber(rawNper);
|
|
5829
|
+
if (nper instanceof FormulaError) return nper;
|
|
5830
|
+
const rawPmt = evaluator.evaluate(args[2], context);
|
|
5831
|
+
if (rawPmt instanceof FormulaError) return rawPmt;
|
|
5832
|
+
const pmt = toNumber(rawPmt);
|
|
5833
|
+
if (pmt instanceof FormulaError) return pmt;
|
|
5834
|
+
let fv = 0;
|
|
5835
|
+
if (args.length >= 4) {
|
|
5836
|
+
const rawFv = evaluator.evaluate(args[3], context);
|
|
5837
|
+
if (rawFv instanceof FormulaError) return rawFv;
|
|
5838
|
+
const fvNum = toNumber(rawFv);
|
|
5839
|
+
if (fvNum instanceof FormulaError) return fvNum;
|
|
5840
|
+
fv = fvNum;
|
|
5841
|
+
}
|
|
5842
|
+
let type = 0;
|
|
5843
|
+
if (args.length >= 5) {
|
|
5844
|
+
const rawType = evaluator.evaluate(args[4], context);
|
|
5845
|
+
if (rawType instanceof FormulaError) return rawType;
|
|
5846
|
+
const typeNum = toNumber(rawType);
|
|
5847
|
+
if (typeNum instanceof FormulaError) return typeNum;
|
|
5848
|
+
type = typeNum;
|
|
5849
|
+
}
|
|
5850
|
+
if (rate === 0) {
|
|
5851
|
+
return -pmt * nper - fv;
|
|
5852
|
+
}
|
|
5853
|
+
const factor = Math.pow(1 + rate, nper);
|
|
5854
|
+
const typeAdj = type !== 0 ? 1 + rate : 1;
|
|
5855
|
+
return -(fv + pmt * typeAdj * (factor - 1) / rate) / factor;
|
|
5856
|
+
}
|
|
5857
|
+
});
|
|
5858
|
+
registry.set("NPER", {
|
|
5859
|
+
minArgs: 3,
|
|
5860
|
+
maxArgs: 5,
|
|
5861
|
+
evaluate(args, context, evaluator) {
|
|
5862
|
+
const rawRate = evaluator.evaluate(args[0], context);
|
|
5863
|
+
if (rawRate instanceof FormulaError) return rawRate;
|
|
5864
|
+
const rate = toNumber(rawRate);
|
|
5865
|
+
if (rate instanceof FormulaError) return rate;
|
|
5866
|
+
const rawPmt = evaluator.evaluate(args[1], context);
|
|
5867
|
+
if (rawPmt instanceof FormulaError) return rawPmt;
|
|
5868
|
+
const pmt = toNumber(rawPmt);
|
|
5869
|
+
if (pmt instanceof FormulaError) return pmt;
|
|
5870
|
+
const rawPv = evaluator.evaluate(args[2], context);
|
|
5871
|
+
if (rawPv instanceof FormulaError) return rawPv;
|
|
5872
|
+
const pv = toNumber(rawPv);
|
|
5873
|
+
if (pv instanceof FormulaError) return pv;
|
|
5874
|
+
let fv = 0;
|
|
5875
|
+
if (args.length >= 4) {
|
|
5876
|
+
const rawFv = evaluator.evaluate(args[3], context);
|
|
5877
|
+
if (rawFv instanceof FormulaError) return rawFv;
|
|
5878
|
+
const fvNum = toNumber(rawFv);
|
|
5879
|
+
if (fvNum instanceof FormulaError) return fvNum;
|
|
5880
|
+
fv = fvNum;
|
|
5881
|
+
}
|
|
5882
|
+
let type = 0;
|
|
5883
|
+
if (args.length >= 5) {
|
|
5884
|
+
const rawType = evaluator.evaluate(args[4], context);
|
|
5885
|
+
if (rawType instanceof FormulaError) return rawType;
|
|
5886
|
+
const typeNum = toNumber(rawType);
|
|
5887
|
+
if (typeNum instanceof FormulaError) return typeNum;
|
|
5888
|
+
type = typeNum;
|
|
5889
|
+
}
|
|
5890
|
+
if (rate === 0) {
|
|
5891
|
+
if (pmt === 0) return new FormulaError("#NUM!", "NPER: pmt cannot be 0 when rate is 0");
|
|
5892
|
+
return -(pv + fv) / pmt;
|
|
5893
|
+
}
|
|
5894
|
+
const typeAdj = type !== 0 ? 1 + rate : 1;
|
|
5895
|
+
const pmtAdj = pmt * typeAdj;
|
|
5896
|
+
const num = pmtAdj - fv * rate;
|
|
5897
|
+
const den = pmtAdj + pv * rate;
|
|
5898
|
+
if (den === 0) return new FormulaError("#DIV/0!", "NPER: division by zero");
|
|
5899
|
+
if (num / den <= 0) return new FormulaError("#NUM!", "NPER: logarithm of non-positive number");
|
|
5900
|
+
return Math.log(num / den) / Math.log(1 + rate);
|
|
5901
|
+
}
|
|
5902
|
+
});
|
|
5903
|
+
registry.set("RATE", {
|
|
5904
|
+
minArgs: 3,
|
|
5905
|
+
maxArgs: 6,
|
|
5906
|
+
evaluate(args, context, evaluator) {
|
|
5907
|
+
const rawNper = evaluator.evaluate(args[0], context);
|
|
5908
|
+
if (rawNper instanceof FormulaError) return rawNper;
|
|
5909
|
+
const nper = toNumber(rawNper);
|
|
5910
|
+
if (nper instanceof FormulaError) return nper;
|
|
5911
|
+
const rawPmt = evaluator.evaluate(args[1], context);
|
|
5912
|
+
if (rawPmt instanceof FormulaError) return rawPmt;
|
|
5913
|
+
const pmt = toNumber(rawPmt);
|
|
5914
|
+
if (pmt instanceof FormulaError) return pmt;
|
|
5915
|
+
const rawPv = evaluator.evaluate(args[2], context);
|
|
5916
|
+
if (rawPv instanceof FormulaError) return rawPv;
|
|
5917
|
+
const pv = toNumber(rawPv);
|
|
5918
|
+
if (pv instanceof FormulaError) return pv;
|
|
5919
|
+
let fv = 0;
|
|
5920
|
+
if (args.length >= 4) {
|
|
5921
|
+
const rawFv = evaluator.evaluate(args[3], context);
|
|
5922
|
+
if (rawFv instanceof FormulaError) return rawFv;
|
|
5923
|
+
const fvNum = toNumber(rawFv);
|
|
5924
|
+
if (fvNum instanceof FormulaError) return fvNum;
|
|
5925
|
+
fv = fvNum;
|
|
5926
|
+
}
|
|
5927
|
+
let type = 0;
|
|
5928
|
+
if (args.length >= 5) {
|
|
5929
|
+
const rawType = evaluator.evaluate(args[4], context);
|
|
5930
|
+
if (rawType instanceof FormulaError) return rawType;
|
|
5931
|
+
const typeNum = toNumber(rawType);
|
|
5932
|
+
if (typeNum instanceof FormulaError) return typeNum;
|
|
5933
|
+
type = typeNum;
|
|
5934
|
+
}
|
|
5935
|
+
let guess = 0.1;
|
|
5936
|
+
if (args.length >= 6) {
|
|
5937
|
+
const rawGuess = evaluator.evaluate(args[5], context);
|
|
5938
|
+
if (rawGuess instanceof FormulaError) return rawGuess;
|
|
5939
|
+
const guessNum = toNumber(rawGuess);
|
|
5940
|
+
if (guessNum instanceof FormulaError) return guessNum;
|
|
5941
|
+
guess = guessNum;
|
|
5942
|
+
}
|
|
5943
|
+
const MAX_ITER = 20;
|
|
5944
|
+
const TOLERANCE = 1e-7;
|
|
5945
|
+
let r = guess;
|
|
5946
|
+
for (let i = 0; i < MAX_ITER; i++) {
|
|
5947
|
+
if (r <= -1) return new FormulaError("#NUM!", "RATE: rate converged to invalid value");
|
|
5948
|
+
const factor = Math.pow(1 + r, nper);
|
|
5949
|
+
const typeAdj = type !== 0 ? 1 + r : 1;
|
|
5950
|
+
let f;
|
|
5951
|
+
let df;
|
|
5952
|
+
if (Math.abs(r) < 1e-10) {
|
|
5953
|
+
f = pv + pmt * nper + fv;
|
|
5954
|
+
df = pv * nper + pmt * nper * (nper - 1) / 2;
|
|
5955
|
+
} else {
|
|
5956
|
+
f = pv * factor + pmt * typeAdj * (factor - 1) / r + fv;
|
|
5957
|
+
const dfactor = nper * Math.pow(1 + r, nper - 1);
|
|
5958
|
+
const dTypeAdj = type !== 0 ? 1 : 0;
|
|
5959
|
+
df = pv * dfactor + pmt * (dTypeAdj * (factor - 1) / r + typeAdj * (dfactor * r - (factor - 1)) / (r * r));
|
|
5960
|
+
}
|
|
5961
|
+
if (Math.abs(df) < 1e-15) return new FormulaError("#NUM!", "RATE: derivative too small, no convergence");
|
|
5962
|
+
const delta = f / df;
|
|
5963
|
+
r = r - delta;
|
|
5964
|
+
if (Math.abs(delta) < TOLERANCE) return r;
|
|
5965
|
+
}
|
|
5966
|
+
return new FormulaError("#NUM!", "RATE: did not converge");
|
|
5967
|
+
}
|
|
5968
|
+
});
|
|
5969
|
+
registry.set("NPV", {
|
|
5970
|
+
minArgs: 2,
|
|
5971
|
+
maxArgs: -1,
|
|
5972
|
+
evaluate(args, context, evaluator) {
|
|
5973
|
+
const rawRate = evaluator.evaluate(args[0], context);
|
|
5974
|
+
if (rawRate instanceof FormulaError) return rawRate;
|
|
5975
|
+
const rate = toNumber(rawRate);
|
|
5976
|
+
if (rate instanceof FormulaError) return rate;
|
|
5977
|
+
const values = flattenArgs(args.slice(1), context, evaluator);
|
|
5978
|
+
let npv = 0;
|
|
5979
|
+
let period = 1;
|
|
5980
|
+
for (const val of values) {
|
|
5981
|
+
if (val instanceof FormulaError) return val;
|
|
5982
|
+
if (typeof val === "number" || typeof val === "boolean") {
|
|
5983
|
+
const n = typeof val === "boolean" ? val ? 1 : 0 : val;
|
|
5984
|
+
npv += n / Math.pow(1 + rate, period);
|
|
5985
|
+
period++;
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
return npv;
|
|
5989
|
+
}
|
|
5990
|
+
});
|
|
5991
|
+
registry.set("IRR", {
|
|
5992
|
+
minArgs: 1,
|
|
5993
|
+
maxArgs: 2,
|
|
5994
|
+
evaluate(args, context, evaluator) {
|
|
5995
|
+
const cashFlows = flattenArgs([args[0]], context, evaluator);
|
|
5996
|
+
const nums = [];
|
|
5997
|
+
for (const val of cashFlows) {
|
|
5998
|
+
if (val instanceof FormulaError) return val;
|
|
5999
|
+
if (typeof val === "number") nums.push(val);
|
|
6000
|
+
else if (typeof val === "boolean") nums.push(val ? 1 : 0);
|
|
6001
|
+
}
|
|
6002
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "IRR: no values");
|
|
6003
|
+
let guess = 0.1;
|
|
6004
|
+
if (args.length >= 2) {
|
|
6005
|
+
const rawGuess = evaluator.evaluate(args[1], context);
|
|
6006
|
+
if (rawGuess instanceof FormulaError) return rawGuess;
|
|
6007
|
+
const guessNum = toNumber(rawGuess);
|
|
6008
|
+
if (guessNum instanceof FormulaError) return guessNum;
|
|
6009
|
+
guess = guessNum;
|
|
6010
|
+
}
|
|
6011
|
+
const MAX_ITER = 20;
|
|
6012
|
+
const TOLERANCE = 1e-7;
|
|
6013
|
+
let r = guess;
|
|
6014
|
+
for (let i = 0; i < MAX_ITER; i++) {
|
|
6015
|
+
if (r <= -1) return new FormulaError("#NUM!", "IRR: rate converged below -1");
|
|
6016
|
+
let f = 0;
|
|
6017
|
+
let df = 0;
|
|
6018
|
+
for (let j = 0; j < nums.length; j++) {
|
|
6019
|
+
const factor = Math.pow(1 + r, j);
|
|
6020
|
+
f += nums[j] / factor;
|
|
6021
|
+
if (j > 0) {
|
|
6022
|
+
df -= j * nums[j] / Math.pow(1 + r, j + 1);
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
if (Math.abs(df) < 1e-15) return new FormulaError("#NUM!", "IRR: derivative too small");
|
|
6026
|
+
const delta = f / df;
|
|
6027
|
+
r = r - delta;
|
|
6028
|
+
if (Math.abs(delta) < TOLERANCE) return r;
|
|
6029
|
+
}
|
|
6030
|
+
return new FormulaError("#NUM!", "IRR: did not converge");
|
|
6031
|
+
}
|
|
6032
|
+
});
|
|
6033
|
+
registry.set("SLN", {
|
|
6034
|
+
minArgs: 3,
|
|
6035
|
+
maxArgs: 3,
|
|
6036
|
+
evaluate(args, context, evaluator) {
|
|
6037
|
+
const rawCost = evaluator.evaluate(args[0], context);
|
|
6038
|
+
if (rawCost instanceof FormulaError) return rawCost;
|
|
6039
|
+
const cost = toNumber(rawCost);
|
|
6040
|
+
if (cost instanceof FormulaError) return cost;
|
|
6041
|
+
const rawSalvage = evaluator.evaluate(args[1], context);
|
|
6042
|
+
if (rawSalvage instanceof FormulaError) return rawSalvage;
|
|
6043
|
+
const salvage = toNumber(rawSalvage);
|
|
6044
|
+
if (salvage instanceof FormulaError) return salvage;
|
|
6045
|
+
const rawLife = evaluator.evaluate(args[2], context);
|
|
6046
|
+
if (rawLife instanceof FormulaError) return rawLife;
|
|
6047
|
+
const life = toNumber(rawLife);
|
|
6048
|
+
if (life instanceof FormulaError) return life;
|
|
6049
|
+
if (life === 0) return new FormulaError("#DIV/0!", "SLN: life cannot be 0");
|
|
6050
|
+
return (cost - salvage) / life;
|
|
6051
|
+
}
|
|
6052
|
+
});
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
// src/formula/functions/statistical-extended.ts
|
|
6056
|
+
function extractNums(args, context, evaluator) {
|
|
6057
|
+
const values = flattenArgs(args, context, evaluator);
|
|
6058
|
+
const nums = [];
|
|
6059
|
+
for (const val of values) {
|
|
6060
|
+
if (val instanceof FormulaError) return val;
|
|
6061
|
+
if (typeof val === "number") nums.push(val);
|
|
6062
|
+
else if (typeof val === "boolean") nums.push(val ? 1 : 0);
|
|
6063
|
+
}
|
|
6064
|
+
return nums;
|
|
6065
|
+
}
|
|
6066
|
+
function mean(nums) {
|
|
6067
|
+
let sum = 0;
|
|
6068
|
+
for (const n of nums) sum += n;
|
|
6069
|
+
return sum / nums.length;
|
|
6070
|
+
}
|
|
6071
|
+
function registerStatisticalExtendedFunctions(registry) {
|
|
6072
|
+
const stdevImpl = {
|
|
6073
|
+
minArgs: 1,
|
|
6074
|
+
maxArgs: -1,
|
|
6075
|
+
evaluate(args, context, evaluator) {
|
|
6076
|
+
const nums = extractNums(args, context, evaluator);
|
|
6077
|
+
if (nums instanceof FormulaError) return nums;
|
|
6078
|
+
if (nums.length < 2) return new FormulaError("#DIV/0!", "STDEV requires at least 2 values");
|
|
6079
|
+
const m = mean(nums);
|
|
6080
|
+
let sum = 0;
|
|
6081
|
+
for (const n of nums) sum += (n - m) * (n - m);
|
|
6082
|
+
return Math.sqrt(sum / (nums.length - 1));
|
|
6083
|
+
}
|
|
6084
|
+
};
|
|
6085
|
+
registry.set("STDEV", stdevImpl);
|
|
6086
|
+
registry.set("STDEV.S", stdevImpl);
|
|
6087
|
+
const stdevpImpl = {
|
|
6088
|
+
minArgs: 1,
|
|
6089
|
+
maxArgs: -1,
|
|
6090
|
+
evaluate(args, context, evaluator) {
|
|
6091
|
+
const nums = extractNums(args, context, evaluator);
|
|
6092
|
+
if (nums instanceof FormulaError) return nums;
|
|
6093
|
+
if (nums.length === 0) return new FormulaError("#DIV/0!", "STDEVP requires at least 1 value");
|
|
6094
|
+
const m = mean(nums);
|
|
6095
|
+
let sum = 0;
|
|
6096
|
+
for (const n of nums) sum += (n - m) * (n - m);
|
|
6097
|
+
return Math.sqrt(sum / nums.length);
|
|
6098
|
+
}
|
|
6099
|
+
};
|
|
6100
|
+
registry.set("STDEVP", stdevpImpl);
|
|
6101
|
+
registry.set("STDEV.P", stdevpImpl);
|
|
6102
|
+
const varImpl = {
|
|
6103
|
+
minArgs: 1,
|
|
6104
|
+
maxArgs: -1,
|
|
6105
|
+
evaluate(args, context, evaluator) {
|
|
6106
|
+
const nums = extractNums(args, context, evaluator);
|
|
6107
|
+
if (nums instanceof FormulaError) return nums;
|
|
6108
|
+
if (nums.length < 2) return new FormulaError("#DIV/0!", "VAR requires at least 2 values");
|
|
6109
|
+
const m = mean(nums);
|
|
6110
|
+
let sum = 0;
|
|
6111
|
+
for (const n of nums) sum += (n - m) * (n - m);
|
|
6112
|
+
return sum / (nums.length - 1);
|
|
6113
|
+
}
|
|
6114
|
+
};
|
|
6115
|
+
registry.set("VAR", varImpl);
|
|
6116
|
+
registry.set("VAR.S", varImpl);
|
|
6117
|
+
const varpImpl = {
|
|
6118
|
+
minArgs: 1,
|
|
6119
|
+
maxArgs: -1,
|
|
6120
|
+
evaluate(args, context, evaluator) {
|
|
6121
|
+
const nums = extractNums(args, context, evaluator);
|
|
6122
|
+
if (nums instanceof FormulaError) return nums;
|
|
6123
|
+
if (nums.length === 0) return new FormulaError("#DIV/0!", "VARP requires at least 1 value");
|
|
6124
|
+
const m = mean(nums);
|
|
6125
|
+
let sum = 0;
|
|
6126
|
+
for (const n of nums) sum += (n - m) * (n - m);
|
|
6127
|
+
return sum / nums.length;
|
|
6128
|
+
}
|
|
6129
|
+
};
|
|
6130
|
+
registry.set("VARP", varpImpl);
|
|
6131
|
+
registry.set("VAR.P", varpImpl);
|
|
6132
|
+
registry.set("CORREL", {
|
|
6133
|
+
minArgs: 2,
|
|
6134
|
+
maxArgs: 2,
|
|
6135
|
+
evaluate(args, context, evaluator) {
|
|
6136
|
+
const nums1 = extractNums([args[0]], context, evaluator);
|
|
6137
|
+
if (nums1 instanceof FormulaError) return nums1;
|
|
6138
|
+
const nums2 = extractNums([args[1]], context, evaluator);
|
|
6139
|
+
if (nums2 instanceof FormulaError) return nums2;
|
|
6140
|
+
if (nums1.length !== nums2.length) {
|
|
6141
|
+
return new FormulaError("#N/A", "CORREL: arrays must have same number of values");
|
|
6142
|
+
}
|
|
6143
|
+
if (nums1.length < 2) {
|
|
6144
|
+
return new FormulaError("#DIV/0!", "CORREL requires at least 2 paired values");
|
|
6145
|
+
}
|
|
6146
|
+
const m1 = mean(nums1);
|
|
6147
|
+
const m2 = mean(nums2);
|
|
6148
|
+
let cov = 0;
|
|
6149
|
+
let var1 = 0;
|
|
6150
|
+
let var2 = 0;
|
|
6151
|
+
for (let i = 0; i < nums1.length; i++) {
|
|
6152
|
+
const d1 = nums1[i] - m1;
|
|
6153
|
+
const d2 = nums2[i] - m2;
|
|
6154
|
+
cov += d1 * d2;
|
|
6155
|
+
var1 += d1 * d1;
|
|
6156
|
+
var2 += d2 * d2;
|
|
6157
|
+
}
|
|
6158
|
+
const denom = Math.sqrt(var1 * var2);
|
|
6159
|
+
if (denom === 0) return new FormulaError("#DIV/0!", "CORREL: zero variance");
|
|
6160
|
+
return cov / denom;
|
|
6161
|
+
}
|
|
6162
|
+
});
|
|
6163
|
+
function percentileCalc(nums, k) {
|
|
6164
|
+
if (k < 0 || k > 1) return new FormulaError("#NUM!", "PERCENTILE: k must be between 0 and 1");
|
|
6165
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "PERCENTILE: empty array");
|
|
6166
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
6167
|
+
const idx = k * (sorted.length - 1);
|
|
6168
|
+
const low = Math.floor(idx);
|
|
6169
|
+
const high = Math.ceil(idx);
|
|
6170
|
+
if (low === high) return sorted[low];
|
|
6171
|
+
const frac = idx - low;
|
|
6172
|
+
return sorted[low] + frac * (sorted[high] - sorted[low]);
|
|
6173
|
+
}
|
|
6174
|
+
const percentileImpl = {
|
|
6175
|
+
minArgs: 2,
|
|
6176
|
+
maxArgs: 2,
|
|
6177
|
+
evaluate(args, context, evaluator) {
|
|
6178
|
+
const nums = extractNums([args[0]], context, evaluator);
|
|
6179
|
+
if (nums instanceof FormulaError) return nums;
|
|
6180
|
+
const rawK = evaluator.evaluate(args[1], context);
|
|
6181
|
+
if (rawK instanceof FormulaError) return rawK;
|
|
6182
|
+
const k = toNumber(rawK);
|
|
6183
|
+
if (k instanceof FormulaError) return k;
|
|
6184
|
+
return percentileCalc(nums, k);
|
|
6185
|
+
}
|
|
6186
|
+
};
|
|
6187
|
+
registry.set("PERCENTILE", percentileImpl);
|
|
6188
|
+
registry.set("PERCENTILE.INC", percentileImpl);
|
|
6189
|
+
const quartileImpl = {
|
|
6190
|
+
minArgs: 2,
|
|
6191
|
+
maxArgs: 2,
|
|
6192
|
+
evaluate(args, context, evaluator) {
|
|
6193
|
+
const nums = extractNums([args[0]], context, evaluator);
|
|
6194
|
+
if (nums instanceof FormulaError) return nums;
|
|
6195
|
+
const rawQuart = evaluator.evaluate(args[1], context);
|
|
6196
|
+
if (rawQuart instanceof FormulaError) return rawQuart;
|
|
6197
|
+
const quart = toNumber(rawQuart);
|
|
6198
|
+
if (quart instanceof FormulaError) return quart;
|
|
6199
|
+
const quartInt = Math.trunc(quart);
|
|
6200
|
+
if (quartInt < 0 || quartInt > 4) {
|
|
6201
|
+
return new FormulaError("#NUM!", "QUARTILE: quart must be 0, 1, 2, 3, or 4");
|
|
6202
|
+
}
|
|
6203
|
+
return percentileCalc(nums, quartInt * 0.25);
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
6206
|
+
registry.set("QUARTILE", quartileImpl);
|
|
6207
|
+
registry.set("QUARTILE.INC", quartileImpl);
|
|
6208
|
+
const modeImpl = {
|
|
6209
|
+
minArgs: 1,
|
|
6210
|
+
maxArgs: -1,
|
|
6211
|
+
evaluate(args, context, evaluator) {
|
|
6212
|
+
const nums = extractNums(args, context, evaluator);
|
|
6213
|
+
if (nums instanceof FormulaError) return nums;
|
|
6214
|
+
if (nums.length === 0) return new FormulaError("#N/A", "MODE: no numeric values");
|
|
6215
|
+
const freq = /* @__PURE__ */ new Map();
|
|
6216
|
+
for (const n of nums) {
|
|
6217
|
+
freq.set(n, (freq.get(n) ?? 0) + 1);
|
|
6218
|
+
}
|
|
6219
|
+
let maxFreq = 0;
|
|
6220
|
+
let modeVal = null;
|
|
6221
|
+
for (const n of nums) {
|
|
6222
|
+
const f = freq.get(n) ?? 0;
|
|
6223
|
+
if (f > maxFreq) {
|
|
6224
|
+
maxFreq = f;
|
|
6225
|
+
modeVal = n;
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
if (maxFreq < 1 || modeVal === null) return new FormulaError("#N/A", "MODE: no values");
|
|
6229
|
+
return modeVal;
|
|
6230
|
+
}
|
|
6231
|
+
};
|
|
6232
|
+
registry.set("MODE", modeImpl);
|
|
6233
|
+
registry.set("MODE.SNGL", modeImpl);
|
|
6234
|
+
registry.set("GEOMEAN", {
|
|
6235
|
+
minArgs: 1,
|
|
6236
|
+
maxArgs: -1,
|
|
6237
|
+
evaluate(args, context, evaluator) {
|
|
6238
|
+
const nums = extractNums(args, context, evaluator);
|
|
6239
|
+
if (nums instanceof FormulaError) return nums;
|
|
6240
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "GEOMEAN: no numeric values");
|
|
6241
|
+
let sumLn = 0;
|
|
6242
|
+
for (const n of nums) {
|
|
6243
|
+
if (n <= 0) return new FormulaError("#NUM!", "GEOMEAN: all values must be positive");
|
|
6244
|
+
sumLn += Math.log(n);
|
|
6245
|
+
}
|
|
6246
|
+
return Math.exp(sumLn / nums.length);
|
|
6247
|
+
}
|
|
6248
|
+
});
|
|
6249
|
+
registry.set("HARMEAN", {
|
|
6250
|
+
minArgs: 1,
|
|
6251
|
+
maxArgs: -1,
|
|
6252
|
+
evaluate(args, context, evaluator) {
|
|
6253
|
+
const nums = extractNums(args, context, evaluator);
|
|
6254
|
+
if (nums instanceof FormulaError) return nums;
|
|
6255
|
+
if (nums.length === 0) return new FormulaError("#NUM!", "HARMEAN: no numeric values");
|
|
6256
|
+
let sumRecip = 0;
|
|
6257
|
+
for (const n of nums) {
|
|
6258
|
+
if (n <= 0) return new FormulaError("#NUM!", "HARMEAN: all values must be positive");
|
|
6259
|
+
sumRecip += 1 / n;
|
|
6260
|
+
}
|
|
6261
|
+
if (sumRecip === 0) return new FormulaError("#DIV/0!", "HARMEAN: sum of reciprocals is zero");
|
|
6262
|
+
return nums.length / sumRecip;
|
|
6263
|
+
}
|
|
6264
|
+
});
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
// src/formula/functions/reference.ts
|
|
6268
|
+
function registerReferenceFunctions(registry) {
|
|
6269
|
+
registry.set("INDIRECT", {
|
|
6270
|
+
minArgs: 1,
|
|
6271
|
+
maxArgs: 2,
|
|
6272
|
+
evaluate(args, context, evaluator) {
|
|
6273
|
+
const rawRef = evaluator.evaluate(args[0], context);
|
|
6274
|
+
if (rawRef instanceof FormulaError) return rawRef;
|
|
6275
|
+
const refText = String(rawRef ?? "");
|
|
6276
|
+
const range = parseRange(refText);
|
|
6277
|
+
if (range) {
|
|
6278
|
+
const start = range.start;
|
|
6279
|
+
const end = range.end;
|
|
6280
|
+
if (start.row === end.row && start.col === end.col) {
|
|
6281
|
+
return context.getCellValue(start);
|
|
6282
|
+
}
|
|
6283
|
+
return context.getCellValue(start);
|
|
6284
|
+
}
|
|
6285
|
+
const addr = parseCellRef(refText);
|
|
6286
|
+
if (!addr) {
|
|
6287
|
+
return new FormulaError("#REF!", `INDIRECT: invalid reference "${refText}"`);
|
|
6288
|
+
}
|
|
6289
|
+
return context.getCellValue(addr);
|
|
6290
|
+
}
|
|
6291
|
+
});
|
|
6292
|
+
registry.set("OFFSET", {
|
|
6293
|
+
minArgs: 3,
|
|
6294
|
+
maxArgs: 5,
|
|
6295
|
+
evaluate(args, context, evaluator) {
|
|
6296
|
+
let baseCol;
|
|
6297
|
+
let baseRow;
|
|
6298
|
+
if (args[0].kind === "cellRef") {
|
|
6299
|
+
baseCol = args[0].address.col;
|
|
6300
|
+
baseRow = args[0].address.row;
|
|
6301
|
+
} else if (args[0].kind === "range") {
|
|
6302
|
+
baseCol = args[0].start.col;
|
|
6303
|
+
baseRow = args[0].start.row;
|
|
6304
|
+
} else {
|
|
6305
|
+
return new FormulaError("#VALUE!", "OFFSET: first argument must be a cell reference");
|
|
6306
|
+
}
|
|
6307
|
+
const rawRows = evaluator.evaluate(args[1], context);
|
|
6308
|
+
if (rawRows instanceof FormulaError) return rawRows;
|
|
6309
|
+
const rowOffset = toNumber(rawRows);
|
|
6310
|
+
if (rowOffset instanceof FormulaError) return rowOffset;
|
|
6311
|
+
const rawCols = evaluator.evaluate(args[2], context);
|
|
6312
|
+
if (rawCols instanceof FormulaError) return rawCols;
|
|
6313
|
+
const colOffset = toNumber(rawCols);
|
|
6314
|
+
if (colOffset instanceof FormulaError) return colOffset;
|
|
6315
|
+
const targetRow = baseRow + Math.trunc(rowOffset);
|
|
6316
|
+
const targetCol = baseCol + Math.trunc(colOffset);
|
|
6317
|
+
if (targetRow < 0 || targetCol < 0) {
|
|
6318
|
+
return new FormulaError("#REF!", "OFFSET: reference out of bounds");
|
|
6319
|
+
}
|
|
6320
|
+
let height = 1;
|
|
6321
|
+
if (args.length >= 4) {
|
|
6322
|
+
const rawH = evaluator.evaluate(args[3], context);
|
|
6323
|
+
if (rawH instanceof FormulaError) return rawH;
|
|
6324
|
+
const h = toNumber(rawH);
|
|
6325
|
+
if (h instanceof FormulaError) return h;
|
|
6326
|
+
height = Math.trunc(h);
|
|
6327
|
+
}
|
|
6328
|
+
let width = 1;
|
|
6329
|
+
if (args.length >= 5) {
|
|
6330
|
+
const rawW = evaluator.evaluate(args[4], context);
|
|
6331
|
+
if (rawW instanceof FormulaError) return rawW;
|
|
6332
|
+
const w = toNumber(rawW);
|
|
6333
|
+
if (w instanceof FormulaError) return w;
|
|
6334
|
+
width = Math.trunc(w);
|
|
6335
|
+
}
|
|
6336
|
+
if (height <= 0 || width <= 0) {
|
|
6337
|
+
return new FormulaError("#VALUE!", "OFFSET: height and width must be >= 1");
|
|
6338
|
+
}
|
|
6339
|
+
if (height === 1 && width === 1) {
|
|
6340
|
+
return context.getCellValue({ col: targetCol, row: targetRow, absCol: false, absRow: false });
|
|
6341
|
+
}
|
|
6342
|
+
return context.getCellValue({ col: targetCol, row: targetRow, absCol: false, absRow: false });
|
|
6343
|
+
}
|
|
6344
|
+
});
|
|
6345
|
+
registry.set("ADDRESS", {
|
|
6346
|
+
minArgs: 2,
|
|
6347
|
+
maxArgs: 5,
|
|
6348
|
+
evaluate(args, context, evaluator) {
|
|
6349
|
+
const rawRow = evaluator.evaluate(args[0], context);
|
|
6350
|
+
if (rawRow instanceof FormulaError) return rawRow;
|
|
6351
|
+
const rowNum = toNumber(rawRow);
|
|
6352
|
+
if (rowNum instanceof FormulaError) return rowNum;
|
|
6353
|
+
const rawCol = evaluator.evaluate(args[1], context);
|
|
6354
|
+
if (rawCol instanceof FormulaError) return rawCol;
|
|
6355
|
+
const colNum = toNumber(rawCol);
|
|
6356
|
+
if (colNum instanceof FormulaError) return colNum;
|
|
6357
|
+
const row = Math.trunc(rowNum);
|
|
6358
|
+
const col = Math.trunc(colNum);
|
|
6359
|
+
if (row < 1 || col < 1) {
|
|
6360
|
+
return new FormulaError("#VALUE!", "ADDRESS: row and column must be >= 1");
|
|
6361
|
+
}
|
|
6362
|
+
let absNum = 1;
|
|
6363
|
+
if (args.length >= 3) {
|
|
6364
|
+
const rawAbs = evaluator.evaluate(args[2], context);
|
|
6365
|
+
if (rawAbs instanceof FormulaError) return rawAbs;
|
|
6366
|
+
const a = toNumber(rawAbs);
|
|
6367
|
+
if (a instanceof FormulaError) return a;
|
|
6368
|
+
absNum = Math.trunc(a);
|
|
6369
|
+
}
|
|
6370
|
+
let sheetText = "";
|
|
6371
|
+
if (args.length >= 5) {
|
|
6372
|
+
const rawSheet = evaluator.evaluate(args[4], context);
|
|
6373
|
+
if (rawSheet instanceof FormulaError) return rawSheet;
|
|
6374
|
+
if (rawSheet !== null && rawSheet !== void 0 && rawSheet !== false) {
|
|
6375
|
+
sheetText = String(rawSheet);
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
const colLetter = indexToColumnLetter(col - 1);
|
|
6379
|
+
let address;
|
|
6380
|
+
switch (absNum) {
|
|
6381
|
+
case 1:
|
|
6382
|
+
address = `$${colLetter}$${row}`;
|
|
6383
|
+
break;
|
|
6384
|
+
// $A$1
|
|
6385
|
+
case 2:
|
|
6386
|
+
address = `${colLetter}$${row}`;
|
|
6387
|
+
break;
|
|
6388
|
+
// A$1
|
|
6389
|
+
case 3:
|
|
6390
|
+
address = `$${colLetter}${row}`;
|
|
6391
|
+
break;
|
|
6392
|
+
// $A1
|
|
6393
|
+
case 4:
|
|
6394
|
+
address = `${colLetter}${row}`;
|
|
6395
|
+
break;
|
|
6396
|
+
// A1
|
|
6397
|
+
default:
|
|
6398
|
+
address = `$${colLetter}$${row}`;
|
|
6399
|
+
}
|
|
6400
|
+
if (sheetText) {
|
|
6401
|
+
const quoted = sheetText.includes(" ") ? `'${sheetText}'` : sheetText;
|
|
6402
|
+
return `${quoted}!${address}`;
|
|
6403
|
+
}
|
|
6404
|
+
return address;
|
|
6405
|
+
}
|
|
6406
|
+
});
|
|
6407
|
+
registry.set("ROW", {
|
|
6408
|
+
minArgs: 0,
|
|
6409
|
+
maxArgs: 1,
|
|
6410
|
+
evaluate(args, context, evaluator) {
|
|
6411
|
+
if (args.length === 0) {
|
|
6412
|
+
return 1;
|
|
6413
|
+
}
|
|
6414
|
+
const arg = args[0];
|
|
6415
|
+
if (arg.kind === "cellRef") {
|
|
6416
|
+
return arg.address.row + 1;
|
|
6417
|
+
}
|
|
6418
|
+
if (arg.kind === "range") {
|
|
6419
|
+
return arg.start.row + 1;
|
|
6420
|
+
}
|
|
6421
|
+
const rawRef = evaluator.evaluate(arg, context);
|
|
6422
|
+
if (rawRef instanceof FormulaError) return rawRef;
|
|
6423
|
+
const refText = String(rawRef ?? "");
|
|
6424
|
+
const addr = parseCellRef(refText);
|
|
6425
|
+
if (!addr) {
|
|
6426
|
+
const rng = parseRange(refText);
|
|
6427
|
+
if (rng) return rng.start.row + 1;
|
|
6428
|
+
return new FormulaError("#VALUE!", "ROW: invalid reference");
|
|
6429
|
+
}
|
|
6430
|
+
return addr.row + 1;
|
|
6431
|
+
}
|
|
6432
|
+
});
|
|
6433
|
+
registry.set("COLUMN", {
|
|
6434
|
+
minArgs: 0,
|
|
6435
|
+
maxArgs: 1,
|
|
6436
|
+
evaluate(args, context, evaluator) {
|
|
6437
|
+
if (args.length === 0) {
|
|
6438
|
+
return 1;
|
|
6439
|
+
}
|
|
6440
|
+
const arg = args[0];
|
|
6441
|
+
if (arg.kind === "cellRef") {
|
|
6442
|
+
return arg.address.col + 1;
|
|
6443
|
+
}
|
|
6444
|
+
if (arg.kind === "range") {
|
|
6445
|
+
return arg.start.col + 1;
|
|
6446
|
+
}
|
|
6447
|
+
const rawRef = evaluator.evaluate(arg, context);
|
|
6448
|
+
if (rawRef instanceof FormulaError) return rawRef;
|
|
6449
|
+
const refText = String(rawRef ?? "");
|
|
6450
|
+
const addr = parseCellRef(refText);
|
|
6451
|
+
if (!addr) {
|
|
6452
|
+
const rng = parseRange(refText);
|
|
6453
|
+
if (rng) return rng.start.col + 1;
|
|
6454
|
+
return new FormulaError("#VALUE!", "COLUMN: invalid reference");
|
|
6455
|
+
}
|
|
6456
|
+
return addr.col + 1;
|
|
6457
|
+
}
|
|
6458
|
+
});
|
|
6459
|
+
registry.set("ROWS", {
|
|
6460
|
+
minArgs: 1,
|
|
6461
|
+
maxArgs: 1,
|
|
6462
|
+
evaluate(args, _context, _evaluator) {
|
|
6463
|
+
const arg = args[0];
|
|
6464
|
+
if (arg.kind === "range") {
|
|
6465
|
+
return Math.abs(arg.end.row - arg.start.row) + 1;
|
|
6466
|
+
}
|
|
6467
|
+
if (arg.kind === "cellRef") {
|
|
6468
|
+
return 1;
|
|
6469
|
+
}
|
|
6470
|
+
return new FormulaError("#VALUE!", "ROWS: argument must be a range reference");
|
|
6471
|
+
}
|
|
6472
|
+
});
|
|
6473
|
+
registry.set("COLUMNS", {
|
|
6474
|
+
minArgs: 1,
|
|
6475
|
+
maxArgs: 1,
|
|
6476
|
+
evaluate(args, _context, _evaluator) {
|
|
6477
|
+
const arg = args[0];
|
|
6478
|
+
if (arg.kind === "range") {
|
|
6479
|
+
return Math.abs(arg.end.col - arg.start.col) + 1;
|
|
6480
|
+
}
|
|
6481
|
+
if (arg.kind === "cellRef") {
|
|
6482
|
+
return 1;
|
|
6483
|
+
}
|
|
6484
|
+
return new FormulaError("#VALUE!", "COLUMNS: argument must be a range reference");
|
|
6485
|
+
}
|
|
6486
|
+
});
|
|
6487
|
+
registry.set("SEQUENCE", {
|
|
6488
|
+
minArgs: 1,
|
|
6489
|
+
maxArgs: 4,
|
|
6490
|
+
evaluate(args, context, evaluator) {
|
|
6491
|
+
const rawRows = evaluator.evaluate(args[0], context);
|
|
6492
|
+
if (rawRows instanceof FormulaError) return rawRows;
|
|
6493
|
+
const rows = toNumber(rawRows);
|
|
6494
|
+
if (rows instanceof FormulaError) return rows;
|
|
6495
|
+
let cols = 1;
|
|
6496
|
+
if (args.length >= 2) {
|
|
6497
|
+
const rawCols = evaluator.evaluate(args[1], context);
|
|
6498
|
+
if (rawCols instanceof FormulaError) return rawCols;
|
|
6499
|
+
const c = toNumber(rawCols);
|
|
6500
|
+
if (c instanceof FormulaError) return c;
|
|
6501
|
+
cols = Math.trunc(c);
|
|
6502
|
+
}
|
|
6503
|
+
let start = 1;
|
|
6504
|
+
if (args.length >= 3) {
|
|
6505
|
+
const rawStart = evaluator.evaluate(args[2], context);
|
|
6506
|
+
if (rawStart instanceof FormulaError) return rawStart;
|
|
6507
|
+
const s = toNumber(rawStart);
|
|
6508
|
+
if (s instanceof FormulaError) return s;
|
|
6509
|
+
start = s;
|
|
6510
|
+
}
|
|
6511
|
+
let step = 1;
|
|
6512
|
+
if (args.length >= 4) {
|
|
6513
|
+
const rawStep = evaluator.evaluate(args[3], context);
|
|
6514
|
+
if (rawStep instanceof FormulaError) return rawStep;
|
|
6515
|
+
const st = toNumber(rawStep);
|
|
6516
|
+
if (st instanceof FormulaError) return st;
|
|
6517
|
+
step = st;
|
|
6518
|
+
}
|
|
6519
|
+
const rowCount = Math.trunc(rows);
|
|
6520
|
+
const colCount = Math.max(1, cols);
|
|
6521
|
+
if (rowCount < 1) {
|
|
6522
|
+
return new FormulaError("#VALUE!", "SEQUENCE: rows must be >= 1");
|
|
6523
|
+
}
|
|
6524
|
+
const result = [];
|
|
6525
|
+
let current = start;
|
|
6526
|
+
for (let r = 0; r < rowCount; r++) {
|
|
6527
|
+
const row = [];
|
|
6528
|
+
for (let c = 0; c < colCount; c++) {
|
|
6529
|
+
row.push(current);
|
|
6530
|
+
current += step;
|
|
6531
|
+
}
|
|
6532
|
+
result.push(row);
|
|
6533
|
+
}
|
|
6534
|
+
if (rowCount === 1 && colCount === 1) {
|
|
6535
|
+
return result[0][0];
|
|
6536
|
+
}
|
|
6537
|
+
return result[0][0];
|
|
6538
|
+
}
|
|
6539
|
+
});
|
|
6540
|
+
registry.set("TRANSPOSE", {
|
|
6541
|
+
minArgs: 1,
|
|
6542
|
+
maxArgs: 1,
|
|
6543
|
+
evaluate(args, context, _evaluator) {
|
|
6544
|
+
if (args[0].kind !== "range") {
|
|
6545
|
+
return new FormulaError("#VALUE!", "TRANSPOSE: argument must be a range");
|
|
6546
|
+
}
|
|
6547
|
+
const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
6548
|
+
if (data.length === 0) return null;
|
|
6549
|
+
const rows = data.length;
|
|
6550
|
+
const cols = data[0].length;
|
|
6551
|
+
const transposed = [];
|
|
6552
|
+
for (let c = 0; c < cols; c++) {
|
|
6553
|
+
const newRow = [];
|
|
6554
|
+
for (let r = 0; r < rows; r++) {
|
|
6555
|
+
newRow.push(data[r][c]);
|
|
6556
|
+
}
|
|
6557
|
+
transposed.push(newRow);
|
|
6558
|
+
}
|
|
6559
|
+
return transposed[0][0] ?? null;
|
|
6560
|
+
}
|
|
6561
|
+
});
|
|
6562
|
+
registry.set("MMULT", {
|
|
6563
|
+
minArgs: 2,
|
|
6564
|
+
maxArgs: 2,
|
|
6565
|
+
evaluate(args, context, _evaluator) {
|
|
6566
|
+
if (args[0].kind !== "range") {
|
|
6567
|
+
return new FormulaError("#VALUE!", "MMULT: array1 must be a range");
|
|
6568
|
+
}
|
|
6569
|
+
if (args[1].kind !== "range") {
|
|
6570
|
+
return new FormulaError("#VALUE!", "MMULT: array2 must be a range");
|
|
6571
|
+
}
|
|
6572
|
+
const a = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
6573
|
+
const b = context.getRangeValues({ start: args[1].start, end: args[1].end });
|
|
6574
|
+
if (a.length === 0 || b.length === 0) {
|
|
6575
|
+
return new FormulaError("#VALUE!", "MMULT: empty array");
|
|
6576
|
+
}
|
|
6577
|
+
const aRows = a.length;
|
|
6578
|
+
const aCols = a[0].length;
|
|
6579
|
+
const bRows = b.length;
|
|
6580
|
+
const bCols = b[0].length;
|
|
6581
|
+
if (aCols !== bRows) {
|
|
6582
|
+
return new FormulaError("#VALUE!", `MMULT: columns of array1 (${aCols}) must equal rows of array2 (${bRows})`);
|
|
6583
|
+
}
|
|
6584
|
+
const result = [];
|
|
6585
|
+
for (let r = 0; r < aRows; r++) {
|
|
6586
|
+
const row = [];
|
|
6587
|
+
for (let c = 0; c < bCols; c++) {
|
|
6588
|
+
let sum = 0;
|
|
6589
|
+
for (let k = 0; k < aCols; k++) {
|
|
6590
|
+
const av = toNumber(a[r][k]);
|
|
6591
|
+
const bv = toNumber(b[k][c]);
|
|
6592
|
+
if (av instanceof FormulaError) return av;
|
|
6593
|
+
if (bv instanceof FormulaError) return bv;
|
|
6594
|
+
sum += av * bv;
|
|
6595
|
+
}
|
|
6596
|
+
row.push(sum);
|
|
6597
|
+
}
|
|
6598
|
+
result.push(row);
|
|
6599
|
+
}
|
|
6600
|
+
return result[0][0];
|
|
6601
|
+
}
|
|
6602
|
+
});
|
|
6603
|
+
registry.set("MDETERM", {
|
|
6604
|
+
minArgs: 1,
|
|
6605
|
+
maxArgs: 1,
|
|
6606
|
+
evaluate(args, context, _evaluator) {
|
|
6607
|
+
if (args[0].kind !== "range") {
|
|
6608
|
+
return new FormulaError("#VALUE!", "MDETERM: argument must be a range");
|
|
6609
|
+
}
|
|
6610
|
+
const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
6611
|
+
if (data.length === 0) return new FormulaError("#VALUE!", "MDETERM: empty array");
|
|
6612
|
+
const n = data.length;
|
|
6613
|
+
if (data.some((row) => row.length !== n)) {
|
|
6614
|
+
return new FormulaError("#VALUE!", "MDETERM: array must be square");
|
|
6615
|
+
}
|
|
6616
|
+
const matrix = [];
|
|
6617
|
+
for (let r = 0; r < n; r++) {
|
|
6618
|
+
const row = [];
|
|
6619
|
+
for (let c = 0; c < n; c++) {
|
|
6620
|
+
const v = toNumber(data[r][c]);
|
|
6621
|
+
if (v instanceof FormulaError) return v;
|
|
6622
|
+
row.push(v);
|
|
6623
|
+
}
|
|
6624
|
+
matrix.push(row);
|
|
6625
|
+
}
|
|
6626
|
+
return determinant(matrix);
|
|
6627
|
+
}
|
|
6628
|
+
});
|
|
6629
|
+
registry.set("MINVERSE", {
|
|
6630
|
+
minArgs: 1,
|
|
6631
|
+
maxArgs: 1,
|
|
6632
|
+
evaluate(args, context, _evaluator) {
|
|
6633
|
+
if (args[0].kind !== "range") {
|
|
6634
|
+
return new FormulaError("#VALUE!", "MINVERSE: argument must be a range");
|
|
6635
|
+
}
|
|
6636
|
+
const data = context.getRangeValues({ start: args[0].start, end: args[0].end });
|
|
6637
|
+
if (data.length === 0) return new FormulaError("#VALUE!", "MINVERSE: empty array");
|
|
6638
|
+
const n = data.length;
|
|
6639
|
+
if (data.some((row) => row.length !== n)) {
|
|
6640
|
+
return new FormulaError("#VALUE!", "MINVERSE: array must be square");
|
|
6641
|
+
}
|
|
6642
|
+
const matrix = [];
|
|
6643
|
+
for (let r = 0; r < n; r++) {
|
|
6644
|
+
const row = [];
|
|
6645
|
+
for (let c = 0; c < n; c++) {
|
|
6646
|
+
const v = toNumber(data[r][c]);
|
|
6647
|
+
if (v instanceof FormulaError) return v;
|
|
6648
|
+
row.push(v);
|
|
6649
|
+
}
|
|
6650
|
+
matrix.push(row);
|
|
6651
|
+
}
|
|
6652
|
+
const inv = matrixInverse(matrix, n);
|
|
6653
|
+
if (inv instanceof FormulaError) return inv;
|
|
6654
|
+
return inv[0][0];
|
|
6655
|
+
}
|
|
6656
|
+
});
|
|
6657
|
+
}
|
|
6658
|
+
function determinant(m) {
|
|
6659
|
+
const n = m.length;
|
|
6660
|
+
if (n === 1) return m[0][0];
|
|
6661
|
+
if (n === 2) return m[0][0] * m[1][1] - m[0][1] * m[1][0];
|
|
6662
|
+
let det = 0;
|
|
6663
|
+
for (let c = 0; c < n; c++) {
|
|
6664
|
+
const minor = [];
|
|
6665
|
+
for (let r = 1; r < n; r++) {
|
|
6666
|
+
const row = [];
|
|
6667
|
+
for (let cc = 0; cc < n; cc++) {
|
|
6668
|
+
if (cc !== c) row.push(m[r][cc]);
|
|
6669
|
+
}
|
|
6670
|
+
minor.push(row);
|
|
6671
|
+
}
|
|
6672
|
+
det += (c % 2 === 0 ? 1 : -1) * m[0][c] * determinant(minor);
|
|
6673
|
+
}
|
|
6674
|
+
return det;
|
|
6675
|
+
}
|
|
6676
|
+
function matrixInverse(m, n) {
|
|
6677
|
+
const aug = [];
|
|
6678
|
+
for (let r = 0; r < n; r++) {
|
|
6679
|
+
const row = [...m[r]];
|
|
6680
|
+
for (let c = 0; c < n; c++) {
|
|
6681
|
+
row.push(c === r ? 1 : 0);
|
|
6682
|
+
}
|
|
6683
|
+
aug.push(row);
|
|
6684
|
+
}
|
|
6685
|
+
for (let col = 0; col < n; col++) {
|
|
6686
|
+
let pivotRow = -1;
|
|
6687
|
+
let pivotVal = 0;
|
|
6688
|
+
for (let r = col; r < n; r++) {
|
|
6689
|
+
if (Math.abs(aug[r][col]) > Math.abs(pivotVal)) {
|
|
6690
|
+
pivotVal = aug[r][col];
|
|
6691
|
+
pivotRow = r;
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
if (pivotRow === -1 || Math.abs(pivotVal) < 1e-12) {
|
|
6695
|
+
return new FormulaError("#NUM!", "MINVERSE: matrix is singular");
|
|
6696
|
+
}
|
|
6697
|
+
if (pivotRow !== col) {
|
|
6698
|
+
[aug[col], aug[pivotRow]] = [aug[pivotRow], aug[col]];
|
|
6699
|
+
}
|
|
6700
|
+
const scale = aug[col][col];
|
|
6701
|
+
for (let c = 0; c < 2 * n; c++) {
|
|
6702
|
+
aug[col][c] /= scale;
|
|
6703
|
+
}
|
|
6704
|
+
for (let r = 0; r < n; r++) {
|
|
6705
|
+
if (r !== col) {
|
|
6706
|
+
const factor = aug[r][col];
|
|
6707
|
+
for (let c = 0; c < 2 * n; c++) {
|
|
6708
|
+
aug[r][c] -= factor * aug[col][c];
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
const result = [];
|
|
6714
|
+
for (let r = 0; r < n; r++) {
|
|
6715
|
+
result.push(aug[r].slice(n));
|
|
6716
|
+
}
|
|
6717
|
+
return result;
|
|
6718
|
+
}
|
|
6719
|
+
|
|
6720
|
+
// src/formula/functions/index.ts
|
|
6721
|
+
function createBuiltInFunctions() {
|
|
6722
|
+
const registry = /* @__PURE__ */ new Map();
|
|
6723
|
+
registerMathFunctions(registry);
|
|
6724
|
+
registerLogicalFunctions(registry);
|
|
6725
|
+
registerLookupFunctions(registry);
|
|
4917
6726
|
registerTextFunctions(registry);
|
|
4918
6727
|
registerDateFunctions(registry);
|
|
4919
6728
|
registerStatsFunctions(registry);
|
|
4920
6729
|
registerInfoFunctions(registry);
|
|
6730
|
+
registerFinancialFunctions(registry);
|
|
6731
|
+
registerStatisticalExtendedFunctions(registry);
|
|
6732
|
+
registerReferenceFunctions(registry);
|
|
4921
6733
|
return registry;
|
|
4922
6734
|
}
|
|
4923
6735
|
|
|
@@ -5272,8 +7084,7 @@ var FormulaEngine = class {
|
|
|
5272
7084
|
return {
|
|
5273
7085
|
getCellValue: (addr) => {
|
|
5274
7086
|
const key = toCellKey(addr.col, addr.row, addr.sheet);
|
|
5275
|
-
|
|
5276
|
-
if (cached !== void 0) return cached;
|
|
7087
|
+
if (this.values.has(key)) return this.values.get(key);
|
|
5277
7088
|
if (addr.sheet) {
|
|
5278
7089
|
const sheetAccessor = this.sheetAccessors.get(addr.sheet);
|
|
5279
7090
|
if (!sheetAccessor) return new FormulaError("#REF!", `Unknown sheet: ${addr.sheet}`);
|
|
@@ -5296,18 +7107,21 @@ var FormulaEngine = class {
|
|
|
5296
7107
|
const row = [];
|
|
5297
7108
|
for (let c = minCol; c <= maxCol; c++) {
|
|
5298
7109
|
const key = toCellKey(c, r, sheet);
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
row.push(cached);
|
|
7110
|
+
if (this.values.has(key)) {
|
|
7111
|
+
row.push(this.values.get(key));
|
|
5302
7112
|
} else {
|
|
5303
|
-
row.push(rangeAccessor
|
|
7113
|
+
row.push(rangeAccessor?.getCellValue(c, r));
|
|
5304
7114
|
}
|
|
5305
7115
|
}
|
|
5306
7116
|
result.push(row);
|
|
5307
7117
|
}
|
|
5308
7118
|
return result;
|
|
5309
7119
|
},
|
|
5310
|
-
now: () => contextNow
|
|
7120
|
+
now: () => contextNow,
|
|
7121
|
+
getCellFormula: (addr) => {
|
|
7122
|
+
const key = toCellKey(addr.col, addr.row, addr.sheet);
|
|
7123
|
+
return this.formulas.get(key);
|
|
7124
|
+
}
|
|
5311
7125
|
};
|
|
5312
7126
|
}
|
|
5313
7127
|
recalcCells(order, accessor, updatedCells) {
|
|
@@ -5333,4 +7147,4 @@ var FormulaEngine = class {
|
|
|
5333
7147
|
}
|
|
5334
7148
|
};
|
|
5335
7149
|
|
|
5336
|
-
export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, CIRC_ERROR, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, DIV_ZERO_ERROR, DependencyGraph, FormulaEngine, FormulaError, FormulaEvaluator, GENERAL_ERROR, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, MAX_PAGE_BUTTONS, NAME_ERROR, NA_ERROR, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, REF_ERROR, ROW_NUMBER_COLUMN_WIDTH, SIDEBAR_TRANSITION_MS, UndoRedoStack, VALUE_ERROR, 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 };
|
|
7150
|
+
export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, CIRC_ERROR, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, DIV_ZERO_ERROR, DependencyGraph, FORMULA_BAR_CSS, FORMULA_BAR_STYLES, FORMULA_REF_COLORS, FormulaEngine, FormulaError, FormulaEvaluator, GENERAL_ERROR, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, MAX_PAGE_BUTTONS, NAME_ERROR, NA_ERROR, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, REF_ERROR, ROW_NUMBER_COLUMN_ID, ROW_NUMBER_COLUMN_MIN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, SIDEBAR_TRANSITION_MS, UndoRedoStack, VALUE_ERROR, 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 };
|