mulmoclaude 2.7.0 → 2.8.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/client/assets/Graph-ChAxkn9o.js +1 -0
- package/client/assets/dist-Bij44LdC.js +2 -0
- package/client/assets/echarts-GV3U9sVf.js +1 -0
- package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
- package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
- package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
- package/client/assets/ffi-4V2b7xu3.js +1 -0
- package/client/assets/{index-lkpgbKtk.js → index-D6xzCZhP.js} +600 -589
- package/client/assets/index-DzMsXDnj.css +2 -0
- package/client/assets/{install-wxcddUsO.js → install-C1moNcrn.js} +1 -1
- package/client/assets/{marp-DcTywifN.js → marp-Du5Sym5b.js} +1 -1
- package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
- package/client/index.html +4 -3
- package/package.json +5 -4
- package/server/api/routes/plugins.ts +8 -59
- package/server/build/mcp-server.mjs +609 -608
- package/server/index.ts +1 -0
- package/server/plugins/confined-file-ops.ts +136 -0
- package/server/plugins/runtime.ts +8 -115
- package/server/plugins/spreadsheet-builtin.ts +26 -0
- package/server/plugins/spreadsheet-save-outcome.ts +7 -0
- package/server/utils/files/store-resolvers.ts +1 -1
- package/src/lang/de.ts +0 -20
- package/src/lang/en.ts +0 -20
- package/src/lang/es.ts +0 -20
- package/src/lang/fr.ts +0 -20
- package/src/lang/ja.ts +0 -20
- package/src/lang/ko.ts +0 -20
- package/src/lang/pt-BR.ts +0 -20
- package/src/lang/zh.ts +0 -20
- package/src/plugins/spreadsheet/definition.ts +9 -127
- package/src/plugins/spreadsheet/index.ts +7 -4
- package/src/plugins/spreadsheet/meta.ts +2 -4
- package/client/assets/echarts-gl-Cq76Z6DA.js +0 -4282
- package/client/assets/index-Cz7JLE0k.css +0 -2
- package/server/utils/files/spreadsheet-store.ts +0 -26
- package/src/plugins/spreadsheet/Preview.vue +0 -31
- package/src/plugins/spreadsheet/View.vue +0 -1007
- package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
- package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
- package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
- package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
- package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
- package/src/plugins/spreadsheet/engine/condition.ts +0 -182
- package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
- package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
- package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
- package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
- package/src/plugins/spreadsheet/engine/engine.ts +0 -177
- package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
- package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
- package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
- package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
- package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
- package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
- package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
- package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
- package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
- package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
- package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
- package/src/plugins/spreadsheet/engine/index.ts +0 -34
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
- package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
- package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
- package/src/plugins/spreadsheet/engine/parser.ts +0 -40
- package/src/plugins/spreadsheet/engine/registry.ts +0 -201
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
- package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
- package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
- package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
- package/src/plugins/spreadsheet/engine/types.ts +0 -81
- package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure lookup rules, separated from the range-reading handlers so they can be
|
|
3
|
-
* unit-tested directly.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { CellValue } from "../types";
|
|
7
|
-
import { isSpreadsheetErrorValue } from "../spreadsheet-errors";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Interpret VLOOKUP/HLOOKUP's 4th argument (range_lookup): TRUE (or omitted) =
|
|
11
|
-
* approximate match, FALSE = exact.
|
|
12
|
-
*
|
|
13
|
-
* The literal `TRUE` reaches the handler as the STRING `"TRUE"` — the evaluator
|
|
14
|
-
* leaves bare words unquoted — so an accept-only-`true | 1 | "1"` check silently
|
|
15
|
-
* fell back to exact match and returned `#N/A` for a valid approximate lookup
|
|
16
|
-
* (#2360). Read it the way Excel coerces a logical instead: a boolean as-is, a
|
|
17
|
-
* non-zero number as TRUE, and the words `TRUE` / `FALSE` (case-insensitive)
|
|
18
|
-
* and `"1"` / `"0"` as their logical value. Anything else (blank, stray text)
|
|
19
|
-
* is treated as FALSE, matching Excel's coercion of a blank range_lookup — and
|
|
20
|
-
* so is a formula error, which is neither of the two logicals.
|
|
21
|
-
*/
|
|
22
|
-
export function isApproximateMatch(rangeLookup: CellValue): boolean {
|
|
23
|
-
if (typeof rangeLookup === "boolean") return rangeLookup;
|
|
24
|
-
if (typeof rangeLookup === "number") return rangeLookup !== 0;
|
|
25
|
-
if (isSpreadsheetErrorValue(rangeLookup)) return false;
|
|
26
|
-
const normalized = rangeLookup.trim().toUpperCase();
|
|
27
|
-
return normalized === "TRUE" || normalized === "1";
|
|
28
|
-
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lookup and Reference Functions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { functionRegistry, rawRangeReader, requiredArg, toNumber, parseCriteria, type FunctionHandler, type FunctionContext } from "../registry";
|
|
6
|
-
import { indexToColumn } from "../parser";
|
|
7
|
-
import { parseRangeBounds, resolveIndexTarget, resolveTableOffset } from "../formulaRefs";
|
|
8
|
-
import { isApproximateMatch } from "./lookup-math";
|
|
9
|
-
import type { CellValue } from "../types";
|
|
10
|
-
import { NA_ERROR, REF_ERROR } from "../spreadsheet-errors";
|
|
11
|
-
|
|
12
|
-
const inclusiveRange = (start: number, end: number): number[] => Array.from({ length: Math.max(0, end - start + 1) }, (_, i) => start + i);
|
|
13
|
-
|
|
14
|
-
// Read a vertical slice (one column, `startRow`..`endRow`) as VLOOKUP's lookup column.
|
|
15
|
-
const columnValues = (context: FunctionContext, sheetPrefix: string, colStr: string, startRow: number, endRow: number): CellValue[] =>
|
|
16
|
-
inclusiveRange(startRow, endRow).map((r) => context.getCellValue(`${sheetPrefix}${colStr}${r}`));
|
|
17
|
-
|
|
18
|
-
// Read a horizontal slice (one row, `startCol`..`endCol`) as HLOOKUP's lookup row.
|
|
19
|
-
const rowValues = (context: FunctionContext, sheetPrefix: string, row: number, startCol: number, endCol: number): CellValue[] =>
|
|
20
|
-
inclusiveRange(startCol, endCol).map((c) => context.getCellValue(`${sheetPrefix}${indexToColumn(c)}${row}`));
|
|
21
|
-
|
|
22
|
-
/** The index of the LAST value that satisfies `matches`, or -1. Array.findIndex
|
|
23
|
-
* only walks forward, and XLOOKUP's `searchMode: -1` searches from the end. */
|
|
24
|
-
const findLastMatchIndex = (values: CellValue[], matches: (value: CellValue) => boolean): number =>
|
|
25
|
-
values.reduce<number>((found, value, index) => (matches(value) ? index : found), -1);
|
|
26
|
-
|
|
27
|
-
/** The index of the last value in a SORTED list before `keepGoing` stops
|
|
28
|
-
* holding, or -1 when the very first value already fails. The list is assumed
|
|
29
|
-
* sorted (as Excel's approximate match requires), so the first failure ends the
|
|
30
|
-
* run. */
|
|
31
|
-
const lastIndexWhile = (values: CellValue[], keepGoing: (value: CellValue) => boolean): number => {
|
|
32
|
-
const stop = values.findIndex((value) => !keepGoing(value));
|
|
33
|
-
return (stop === -1 ? values.length : stop) - 1;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Helper to find match index
|
|
37
|
-
const findMatchIndex = (
|
|
38
|
-
lookupValue: CellValue,
|
|
39
|
-
lookupArray: CellValue[],
|
|
40
|
-
matchType: number = 1, // 1 = less than (sorted asc), 0 = exact, -1 = greater than (sorted desc)
|
|
41
|
-
searchMode: number = 1, // 1 = first to last, -1 = last to first (for XLOOKUP)
|
|
42
|
-
): number => {
|
|
43
|
-
const compare = (a: CellValue, b: CellValue) => {
|
|
44
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
45
|
-
return String(a).localeCompare(String(b));
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Exact match
|
|
49
|
-
if (matchType === 0) {
|
|
50
|
-
// Handle wildcards for strings if it's an exact match request.
|
|
51
|
-
// Loose equality otherwise, for "10" == 10.
|
|
52
|
-
const usesWildcards = typeof lookupValue === "string" && (lookupValue.includes("*") || lookupValue.includes("?"));
|
|
53
|
-
const isMatch = usesWildcards ? parseCriteria(lookupValue) : (item: CellValue) => item == lookupValue;
|
|
54
|
-
return searchMode === 1 ? lookupArray.findIndex(isMatch) : findLastMatchIndex(lookupArray, isMatch);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Approximate match (requires sorted array)
|
|
58
|
-
// We'll assume the user knows what they are doing regarding sorting, as per Excel behavior
|
|
59
|
-
|
|
60
|
-
// Less than or equal to, over an array sorted ascending: once we exceed, stop.
|
|
61
|
-
if (matchType === 1) return lastIndexWhile(lookupArray, (item) => compare(item, lookupValue) <= 0);
|
|
62
|
-
|
|
63
|
-
// Greater than or equal to, over an array sorted descending.
|
|
64
|
-
if (matchType === -1) return lastIndexWhile(lookupArray, (item) => compare(item, lookupValue) >= 0);
|
|
65
|
-
|
|
66
|
-
return -1;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const vlookupHandler: FunctionHandler = (args, context) => {
|
|
70
|
-
const lookupValue = context.evaluateFormula(requiredArg(context, args, 0));
|
|
71
|
-
const bounds = parseRangeBounds(requiredArg(context, args, 1));
|
|
72
|
-
if (!bounds) throw new Error("Invalid table array range");
|
|
73
|
-
const colIndexNum = toNumber(context.evaluateFormula(requiredArg(context, args, 2)));
|
|
74
|
-
const rangeLookup = args.length === 4 ? context.evaluateFormula(requiredArg(context, args, 3)) : true;
|
|
75
|
-
const matchType = isApproximateMatch(rangeLookup) ? 1 : 0;
|
|
76
|
-
|
|
77
|
-
// Excel rejects a col_index_num past the table's width; reading on regardless
|
|
78
|
-
// addressed a cell outside the range and usually returned a silent 0 (#2360).
|
|
79
|
-
// Checked before the search: an out-of-range index is an argument error, so it
|
|
80
|
-
// wins over the #N/A a missing key would otherwise mask it with (Codex review).
|
|
81
|
-
const colOffset = resolveTableOffset(colIndexNum, bounds.endCol - bounds.startCol + 1);
|
|
82
|
-
if (colOffset === null) return REF_ERROR;
|
|
83
|
-
|
|
84
|
-
const startColStr = indexToColumn(bounds.startCol);
|
|
85
|
-
const lookupArray = columnValues(context, bounds.sheetPrefix, startColStr, bounds.startRow, bounds.endRow);
|
|
86
|
-
const matchIdx = findMatchIndex(lookupValue, lookupArray, matchType);
|
|
87
|
-
if (matchIdx === -1) return NA_ERROR;
|
|
88
|
-
|
|
89
|
-
const resultColStr = indexToColumn(bounds.startCol + colOffset);
|
|
90
|
-
const resultRow = bounds.startRow + matchIdx;
|
|
91
|
-
return context.getCellValue(`${bounds.sheetPrefix}${resultColStr}${resultRow}`);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const hlookupHandler: FunctionHandler = (args, context) => {
|
|
95
|
-
const lookupValue = context.evaluateFormula(requiredArg(context, args, 0));
|
|
96
|
-
const bounds = parseRangeBounds(requiredArg(context, args, 1));
|
|
97
|
-
if (!bounds) throw new Error("Invalid range format");
|
|
98
|
-
const rowIndexNum = toNumber(context.evaluateFormula(requiredArg(context, args, 2)));
|
|
99
|
-
const rangeLookup = args.length === 4 ? context.evaluateFormula(requiredArg(context, args, 3)) : true;
|
|
100
|
-
const matchType = isApproximateMatch(rangeLookup) ? 1 : 0;
|
|
101
|
-
|
|
102
|
-
const rowOffset = resolveTableOffset(rowIndexNum, bounds.endRow - bounds.startRow + 1);
|
|
103
|
-
if (rowOffset === null) return REF_ERROR;
|
|
104
|
-
|
|
105
|
-
const lookupArray = rowValues(context, bounds.sheetPrefix, bounds.startRow, bounds.startCol, bounds.endCol);
|
|
106
|
-
const matchIdx = findMatchIndex(lookupValue, lookupArray, matchType);
|
|
107
|
-
if (matchIdx === -1) return NA_ERROR;
|
|
108
|
-
|
|
109
|
-
const resultColStr = indexToColumn(bounds.startCol + matchIdx);
|
|
110
|
-
const resultRow = bounds.startRow + rowOffset;
|
|
111
|
-
return context.getCellValue(`${bounds.sheetPrefix}${resultColStr}${resultRow}`);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const matchHandler: FunctionHandler = (args, context) => {
|
|
115
|
-
const lookupValue = context.evaluateFormula(requiredArg(context, args, 0));
|
|
116
|
-
const lookupArrayRange = requiredArg(context, args, 1);
|
|
117
|
-
const matchType = args.length === 3 ? toNumber(context.evaluateFormula(requiredArg(context, args, 2))) : 1;
|
|
118
|
-
|
|
119
|
-
// Raw, not numeric-only: MATCH answers with a POSITION, so a dropped text
|
|
120
|
-
// cell renumbers everything after it and returns a different row (#2765).
|
|
121
|
-
const lookupArray = rawRangeReader(context)(lookupArrayRange);
|
|
122
|
-
|
|
123
|
-
const index = findMatchIndex(lookupValue, lookupArray, matchType);
|
|
124
|
-
|
|
125
|
-
return index === -1 ? NA_ERROR : index + 1; // 1-based index
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const indexHandler: FunctionHandler = (args, context) => {
|
|
129
|
-
const bounds = parseRangeBounds(requiredArg(context, args, 0));
|
|
130
|
-
if (!bounds) throw new Error("Invalid range format");
|
|
131
|
-
const rowNum = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
132
|
-
const colNum = args.length >= 3 ? toNumber(context.evaluateFormula(requiredArg(context, args, 2))) : 1; // Default to 1 if omitted (for 1D arrays)
|
|
133
|
-
|
|
134
|
-
const target = resolveIndexTarget(bounds, rowNum, colNum);
|
|
135
|
-
if (!target) return REF_ERROR;
|
|
136
|
-
return context.getCellValue(`${bounds.sheetPrefix}${indexToColumn(target.colIndex)}${target.row}`);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const xlookupHandler: FunctionHandler = (args, context) => {
|
|
140
|
-
const lookupValue = context.evaluateFormula(requiredArg(context, args, 0));
|
|
141
|
-
const lookupArrayRange = requiredArg(context, args, 1);
|
|
142
|
-
const returnArrayRange = requiredArg(context, args, 2);
|
|
143
|
-
const ifNotFound = args.length >= 4 ? context.evaluateFormula(requiredArg(context, args, 3)) : NA_ERROR;
|
|
144
|
-
const matchMode = args.length >= 5 ? toNumber(context.evaluateFormula(requiredArg(context, args, 4))) : 0;
|
|
145
|
-
const searchMode = args.length >= 6 ? toNumber(context.evaluateFormula(requiredArg(context, args, 5))) : 1;
|
|
146
|
-
|
|
147
|
-
// Both raw, and for a second reason beyond MATCH's: the numeric-only reader
|
|
148
|
-
// filters these two INDEPENDENTLY, so a text cell in one range shifts it
|
|
149
|
-
// against the other and the match index reads a different row's value — a
|
|
150
|
-
// wrong number with no error, which is the worst outcome a formula can have.
|
|
151
|
-
const readRange = rawRangeReader(context);
|
|
152
|
-
const lookupArray = readRange(lookupArrayRange);
|
|
153
|
-
const returnArray = readRange(returnArrayRange);
|
|
154
|
-
|
|
155
|
-
// XLOOKUP match modes:
|
|
156
|
-
// 0 = Exact match (default)
|
|
157
|
-
// -1 = Exact match or next smaller
|
|
158
|
-
// 1 = Exact match or next larger
|
|
159
|
-
// 2 = Wildcard match
|
|
160
|
-
|
|
161
|
-
// Map XLOOKUP modes to our internal findMatchIndex modes
|
|
162
|
-
// Our internal: 0=exact, 1=less than (sorted), -1=greater than (sorted)
|
|
163
|
-
// XLOOKUP is more complex because it doesn't require sorted arrays for next smaller/larger
|
|
164
|
-
// For now, we'll implement exact (0) and wildcard (2 -> handled by exact with wildcard logic in findMatchIndex)
|
|
165
|
-
// For -1 and 1, we'll do a linear search for best match if not sorted
|
|
166
|
-
|
|
167
|
-
let matchIdx = -1;
|
|
168
|
-
|
|
169
|
-
if (matchMode === 0 || matchMode === 2) {
|
|
170
|
-
matchIdx = findMatchIndex(lookupValue, lookupArray, 0, searchMode);
|
|
171
|
-
} else {
|
|
172
|
-
// Implement exact or next smaller/larger for unsorted arrays
|
|
173
|
-
// This is O(N)
|
|
174
|
-
let bestDiff = Infinity;
|
|
175
|
-
|
|
176
|
-
for (let i = 0; i < lookupArray.length; i++) {
|
|
177
|
-
const idx = searchMode === 1 ? i : lookupArray.length - 1 - i;
|
|
178
|
-
const item = lookupArray[idx];
|
|
179
|
-
|
|
180
|
-
if (item == lookupValue) {
|
|
181
|
-
matchIdx = idx;
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (typeof item === "number" && typeof lookupValue === "number") {
|
|
186
|
-
const diff = item - lookupValue;
|
|
187
|
-
if (matchMode === -1 && diff < 0 && Math.abs(diff) < bestDiff) {
|
|
188
|
-
// Next smaller (closest negative difference)
|
|
189
|
-
bestDiff = Math.abs(diff);
|
|
190
|
-
matchIdx = idx;
|
|
191
|
-
} else if (matchMode === 1 && diff > 0 && diff < bestDiff) {
|
|
192
|
-
// Next larger (closest positive difference)
|
|
193
|
-
bestDiff = diff;
|
|
194
|
-
matchIdx = idx;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (matchIdx === -1) return ifNotFound;
|
|
201
|
-
|
|
202
|
-
// A match past the end of the return range has nothing to return — the two
|
|
203
|
-
// ranges are independent arguments and need not be the same length.
|
|
204
|
-
const found = returnArray[matchIdx];
|
|
205
|
-
return found === undefined ? NA_ERROR : found;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// Register functions
|
|
209
|
-
functionRegistry.register({
|
|
210
|
-
name: "VLOOKUP",
|
|
211
|
-
handler: vlookupHandler,
|
|
212
|
-
minArgs: 3,
|
|
213
|
-
maxArgs: 4,
|
|
214
|
-
description: "Looks for a value in the leftmost column of a table, and then returns a value in the same row from a column you specify",
|
|
215
|
-
examples: ["VLOOKUP(105, A2:C10, 2)", 'VLOOKUP("Smith", A2:E10, 5, FALSE)'],
|
|
216
|
-
category: "Lookup & Reference",
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
functionRegistry.register({
|
|
220
|
-
name: "HLOOKUP",
|
|
221
|
-
handler: hlookupHandler,
|
|
222
|
-
minArgs: 3,
|
|
223
|
-
maxArgs: 4,
|
|
224
|
-
description: "Looks for a value in the top row of a table, and then returns a value in the same column from a row you specify",
|
|
225
|
-
examples: ['HLOOKUP("Axles", A1:C10, 2, TRUE)'],
|
|
226
|
-
category: "Lookup & Reference",
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
functionRegistry.register({
|
|
230
|
-
name: "MATCH",
|
|
231
|
-
handler: matchHandler,
|
|
232
|
-
minArgs: 2,
|
|
233
|
-
maxArgs: 3,
|
|
234
|
-
description: "Returns the relative position of an item in an array that matches a specified value",
|
|
235
|
-
examples: ["MATCH(25, A1:A10, 0)", 'MATCH("b", A1:A5, 0)'],
|
|
236
|
-
category: "Lookup & Reference",
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
functionRegistry.register({
|
|
240
|
-
name: "INDEX",
|
|
241
|
-
handler: indexHandler,
|
|
242
|
-
minArgs: 2,
|
|
243
|
-
maxArgs: 4,
|
|
244
|
-
description: "Returns the value of an element in a table or an array, selected by the row and column number indexes",
|
|
245
|
-
examples: ["INDEX(A1:B5, 2, 2)", "INDEX(A1:A10, 5)"],
|
|
246
|
-
category: "Lookup & Reference",
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
functionRegistry.register({
|
|
250
|
-
name: "XLOOKUP",
|
|
251
|
-
handler: xlookupHandler,
|
|
252
|
-
minArgs: 3,
|
|
253
|
-
maxArgs: 6,
|
|
254
|
-
description: "Searches a range or an array, and returns an item corresponding to the first match it finds",
|
|
255
|
-
examples: ["XLOOKUP(A1, B1:B10, C1:C10)", 'XLOOKUP("USA", Countries, Populations)'],
|
|
256
|
-
category: "Lookup & Reference",
|
|
257
|
-
});
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mathematical Functions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { functionRegistry, requiredArg, toNumber, type FunctionHandler } from "../registry";
|
|
6
|
-
import {
|
|
7
|
-
roundTo,
|
|
8
|
-
roundUpTo,
|
|
9
|
-
roundDownTo,
|
|
10
|
-
floorToSignificance,
|
|
11
|
-
ceilingToSignificance,
|
|
12
|
-
modulo,
|
|
13
|
-
power,
|
|
14
|
-
safeLog,
|
|
15
|
-
safeLog10,
|
|
16
|
-
safeSqrt,
|
|
17
|
-
logWithBase,
|
|
18
|
-
} from "../math-ops";
|
|
19
|
-
import { toScalarNumber } from "../numericCoercion";
|
|
20
|
-
import { isSpreadsheetErrorValue } from "../spreadsheet-errors";
|
|
21
|
-
|
|
22
|
-
const roundHandler: FunctionHandler = (args, context) => {
|
|
23
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
24
|
-
const digits = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
25
|
-
return roundTo(number, digits);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const roundupHandler: FunctionHandler = (args, context) => {
|
|
29
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
30
|
-
const digits = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
31
|
-
return roundUpTo(number, digits);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const rounddownHandler: FunctionHandler = (args, context) => {
|
|
35
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
36
|
-
const digits = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
37
|
-
return roundDownTo(number, digits);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const floorHandler: FunctionHandler = (args, context) => {
|
|
41
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
42
|
-
const significance = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
43
|
-
return floorToSignificance(number, significance);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const ceilingHandler: FunctionHandler = (args, context) => {
|
|
47
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
48
|
-
const significance = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
49
|
-
return ceilingToSignificance(number, significance);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const absHandler: FunctionHandler = (args, context) => {
|
|
53
|
-
const number = toScalarNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
54
|
-
return isSpreadsheetErrorValue(number) ? number : Math.abs(number);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const powerHandler: FunctionHandler = (args, context) => {
|
|
58
|
-
const base = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
59
|
-
const exponent = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
60
|
-
return power(base, exponent);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const sqrtHandler: FunctionHandler = (args, context) => {
|
|
64
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
65
|
-
return safeSqrt(number);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const modHandler: FunctionHandler = (args, context) => {
|
|
69
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
70
|
-
const divisor = toNumber(context.evaluateFormula(requiredArg(context, args, 1)));
|
|
71
|
-
return modulo(number, divisor);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const intHandler: FunctionHandler = (args, context) => {
|
|
75
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
76
|
-
return Math.floor(number);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const truncHandler: FunctionHandler = (args, context) => {
|
|
80
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
81
|
-
const digits = args.length === 2 ? toNumber(context.evaluateFormula(requiredArg(context, args, 1))) : 0;
|
|
82
|
-
const multiplier = Math.pow(10, digits);
|
|
83
|
-
return Math.trunc(number * multiplier) / multiplier;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const signHandler: FunctionHandler = (args, context) => {
|
|
87
|
-
const number = toScalarNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
88
|
-
return isSpreadsheetErrorValue(number) ? number : Math.sign(number);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const piHandler: FunctionHandler = () => Math.PI;
|
|
92
|
-
|
|
93
|
-
const expHandler: FunctionHandler = (args, context) => {
|
|
94
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
95
|
-
return Math.exp(number);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const lnHandler: FunctionHandler = (args, context) => {
|
|
99
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
100
|
-
return safeLog(number);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const logHandler: FunctionHandler = (args, context) => {
|
|
104
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
105
|
-
const base = args.length === 2 ? toNumber(context.evaluateFormula(requiredArg(context, args, 1))) : 10;
|
|
106
|
-
return logWithBase(number, base);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const log10Handler: FunctionHandler = (args, context) => {
|
|
110
|
-
const number = toNumber(context.evaluateFormula(requiredArg(context, args, 0)));
|
|
111
|
-
return safeLog10(number);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// Register all mathematical functions
|
|
115
|
-
functionRegistry.register({
|
|
116
|
-
name: "ROUND",
|
|
117
|
-
handler: roundHandler,
|
|
118
|
-
minArgs: 2,
|
|
119
|
-
maxArgs: 2,
|
|
120
|
-
description: "Rounds a number to a specified number of digits",
|
|
121
|
-
examples: ["ROUND(3.14159, 2)", "ROUND(A1, 0)"],
|
|
122
|
-
category: "Mathematical",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
functionRegistry.register({
|
|
126
|
-
name: "ROUNDUP",
|
|
127
|
-
handler: roundupHandler,
|
|
128
|
-
minArgs: 2,
|
|
129
|
-
maxArgs: 2,
|
|
130
|
-
description: "Rounds a number up, away from zero",
|
|
131
|
-
examples: ["ROUNDUP(3.14159, 2)", "ROUNDUP(A1, 0)"],
|
|
132
|
-
category: "Mathematical",
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
functionRegistry.register({
|
|
136
|
-
name: "ROUNDDOWN",
|
|
137
|
-
handler: rounddownHandler,
|
|
138
|
-
minArgs: 2,
|
|
139
|
-
maxArgs: 2,
|
|
140
|
-
description: "Rounds a number down, toward zero",
|
|
141
|
-
examples: ["ROUNDDOWN(3.14159, 2)", "ROUNDDOWN(A1, 0)"],
|
|
142
|
-
category: "Mathematical",
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
functionRegistry.register({
|
|
146
|
-
name: "FLOOR",
|
|
147
|
-
handler: floorHandler,
|
|
148
|
-
minArgs: 2,
|
|
149
|
-
maxArgs: 2,
|
|
150
|
-
description: "Rounds a number down to the nearest multiple of significance",
|
|
151
|
-
examples: ["FLOOR(3.7, 1)", "FLOOR(24, 5)"],
|
|
152
|
-
category: "Mathematical",
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
functionRegistry.register({
|
|
156
|
-
name: "CEILING",
|
|
157
|
-
handler: ceilingHandler,
|
|
158
|
-
minArgs: 2,
|
|
159
|
-
maxArgs: 2,
|
|
160
|
-
description: "Rounds a number up to the nearest multiple of significance",
|
|
161
|
-
examples: ["CEILING(3.2, 1)", "CEILING(24, 5)"],
|
|
162
|
-
category: "Mathematical",
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
functionRegistry.register({
|
|
166
|
-
name: "ABS",
|
|
167
|
-
handler: absHandler,
|
|
168
|
-
minArgs: 1,
|
|
169
|
-
maxArgs: 1,
|
|
170
|
-
description: "Returns the absolute value of a number",
|
|
171
|
-
examples: ["ABS(-5)", "ABS(A1)"],
|
|
172
|
-
category: "Mathematical",
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
functionRegistry.register({
|
|
176
|
-
name: "POWER",
|
|
177
|
-
handler: powerHandler,
|
|
178
|
-
minArgs: 2,
|
|
179
|
-
maxArgs: 2,
|
|
180
|
-
description: "Returns the result of a number raised to a power",
|
|
181
|
-
examples: ["POWER(2, 3)", "POWER(A1, 2)"],
|
|
182
|
-
category: "Mathematical",
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
functionRegistry.register({
|
|
186
|
-
name: "SQRT",
|
|
187
|
-
handler: sqrtHandler,
|
|
188
|
-
minArgs: 1,
|
|
189
|
-
maxArgs: 1,
|
|
190
|
-
description: "Returns the square root of a number",
|
|
191
|
-
examples: ["SQRT(16)", "SQRT(A1)"],
|
|
192
|
-
category: "Mathematical",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
functionRegistry.register({
|
|
196
|
-
name: "MOD",
|
|
197
|
-
handler: modHandler,
|
|
198
|
-
minArgs: 2,
|
|
199
|
-
maxArgs: 2,
|
|
200
|
-
description: "Returns the remainder after division",
|
|
201
|
-
examples: ["MOD(10, 3)", "MOD(A1, 2)"],
|
|
202
|
-
category: "Mathematical",
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
functionRegistry.register({
|
|
206
|
-
name: "INT",
|
|
207
|
-
handler: intHandler,
|
|
208
|
-
minArgs: 1,
|
|
209
|
-
maxArgs: 1,
|
|
210
|
-
description: "Rounds a number down to the nearest integer",
|
|
211
|
-
examples: ["INT(3.7)", "INT(A1)"],
|
|
212
|
-
category: "Mathematical",
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
functionRegistry.register({
|
|
216
|
-
name: "TRUNC",
|
|
217
|
-
handler: truncHandler,
|
|
218
|
-
minArgs: 1,
|
|
219
|
-
maxArgs: 2,
|
|
220
|
-
description: "Truncates a number to a specified number of decimal places",
|
|
221
|
-
examples: ["TRUNC(3.14159, 2)", "TRUNC(A1)"],
|
|
222
|
-
category: "Mathematical",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
functionRegistry.register({
|
|
226
|
-
name: "SIGN",
|
|
227
|
-
handler: signHandler,
|
|
228
|
-
minArgs: 1,
|
|
229
|
-
maxArgs: 1,
|
|
230
|
-
description: "Returns the sign of a number (1, 0, or -1)",
|
|
231
|
-
examples: ["SIGN(-5)", "SIGN(A1)"],
|
|
232
|
-
category: "Mathematical",
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
functionRegistry.register({
|
|
236
|
-
name: "PI",
|
|
237
|
-
handler: piHandler,
|
|
238
|
-
minArgs: 0,
|
|
239
|
-
maxArgs: 0,
|
|
240
|
-
description: "Returns the value of pi (3.14159...)",
|
|
241
|
-
examples: ["PI()", "PI()*2"],
|
|
242
|
-
category: "Mathematical",
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
functionRegistry.register({
|
|
246
|
-
name: "EXP",
|
|
247
|
-
handler: expHandler,
|
|
248
|
-
minArgs: 1,
|
|
249
|
-
maxArgs: 1,
|
|
250
|
-
description: "Returns e raised to the power of a number",
|
|
251
|
-
examples: ["EXP(1)", "EXP(A1)"],
|
|
252
|
-
category: "Mathematical",
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
functionRegistry.register({
|
|
256
|
-
name: "LN",
|
|
257
|
-
handler: lnHandler,
|
|
258
|
-
minArgs: 1,
|
|
259
|
-
maxArgs: 1,
|
|
260
|
-
description: "Returns the natural logarithm of a number",
|
|
261
|
-
examples: ["LN(10)", "LN(A1)"],
|
|
262
|
-
category: "Mathematical",
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
functionRegistry.register({
|
|
266
|
-
name: "LOG",
|
|
267
|
-
handler: logHandler,
|
|
268
|
-
minArgs: 1,
|
|
269
|
-
maxArgs: 2,
|
|
270
|
-
description: "Returns the logarithm of a number to a specified base",
|
|
271
|
-
examples: ["LOG(100)", "LOG(8, 2)"],
|
|
272
|
-
category: "Mathematical",
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
functionRegistry.register({
|
|
276
|
-
name: "LOG10",
|
|
277
|
-
handler: log10Handler,
|
|
278
|
-
minArgs: 1,
|
|
279
|
-
maxArgs: 1,
|
|
280
|
-
description: "Returns the base-10 logarithm of a number",
|
|
281
|
-
examples: ["LOG10(100)", "LOG10(A1)"],
|
|
282
|
-
category: "Mathematical",
|
|
283
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure statistical rules, separated from the range-reading handlers so they can
|
|
3
|
-
* be unit-tested directly.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { DIV_ZERO_ERROR, NA_ERROR, NUM_ERROR, type SpreadsheetError } from "../spreadsheet-errors";
|
|
7
|
-
|
|
8
|
-
const arithmeticMean = (values: number[]): number => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The arithmetic mean, or `#DIV/0!` when there is nothing to average.
|
|
12
|
-
*
|
|
13
|
-
* Excel divides by the count of NUMBERS, so a range of blanks or text leaves a
|
|
14
|
-
* zero denominator and reports the division rather than a 0 that reads like a
|
|
15
|
-
* genuine average of zeros.
|
|
16
|
-
*/
|
|
17
|
-
export function computeAverage(values: number[]): number | SpreadsheetError {
|
|
18
|
-
if (values.length === 0) return DIV_ZERO_ERROR;
|
|
19
|
-
return arithmeticMean(values);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The middle value (the mean of the middle two when the count is even), or
|
|
24
|
-
* `#NUM!` when there is no value to sit in the middle.
|
|
25
|
-
*
|
|
26
|
-
* Excel's code here is `#NUM!`, not AVERAGE's `#DIV/0!` — nothing is divided by
|
|
27
|
-
* zero, the median of an empty set simply does not exist. Sorts a copy so the
|
|
28
|
-
* caller's array keeps its order.
|
|
29
|
-
*/
|
|
30
|
-
export function computeMedian(values: number[]): number | SpreadsheetError {
|
|
31
|
-
if (values.length === 0) return NUM_ERROR;
|
|
32
|
-
return arithmeticMean(middleValues([...values].sort((a, b) => a - b)));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The one or two values sitting in the middle of a sorted list — two when the
|
|
36
|
-
* count is even, one when it is odd. Sliced rather than indexed, so the middle
|
|
37
|
-
* of an empty list is simply nothing. */
|
|
38
|
-
function middleValues(sorted: number[]): number[] {
|
|
39
|
-
const half = Math.floor(sorted.length / 2);
|
|
40
|
-
return sorted.slice(sorted.length % 2 === 0 ? half - 1 : half, half + 1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The most frequently occurring value, or `#N/A` when no value repeats.
|
|
45
|
-
*
|
|
46
|
-
* Excel's MODE is undefined for an all-distinct set, so returning the first
|
|
47
|
-
* element (as a naive "highest frequency wins" loop does when every count is 1)
|
|
48
|
-
* is a silent wrong answer. Ties resolve to the value that appears first, which
|
|
49
|
-
* Map insertion order preserves.
|
|
50
|
-
*/
|
|
51
|
-
export function computeMode(values: number[]): number | SpreadsheetError {
|
|
52
|
-
const frequency = new Map<number, number>();
|
|
53
|
-
for (const value of values) {
|
|
54
|
-
frequency.set(value, (frequency.get(value) ?? 0) + 1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let topFrequency = 0;
|
|
58
|
-
let mode: number | SpreadsheetError = NA_ERROR;
|
|
59
|
-
for (const [value, count] of frequency.entries()) {
|
|
60
|
-
if (count > topFrequency) {
|
|
61
|
-
topFrequency = count;
|
|
62
|
-
mode = value;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const REPEAT_THRESHOLD = 2;
|
|
67
|
-
return topFrequency >= REPEAT_THRESHOLD ? mode : NA_ERROR;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Sample size below which a sample variance/stdev is undefined. */
|
|
71
|
-
const MIN_SAMPLE_SIZE = 2;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Sample variance (Excel VAR): the mean squared deviation divided by `n - 1`,
|
|
75
|
-
* not `n`. Dividing by `n` is the POPULATION variance (Excel's VARP); using it
|
|
76
|
-
* for VAR understates the spread. Fewer than two values leave no `n - 1` to
|
|
77
|
-
* divide by, so Excel reports `#DIV/0!` rather than a silent 0.
|
|
78
|
-
*/
|
|
79
|
-
export function sampleVariance(values: number[]): number | SpreadsheetError {
|
|
80
|
-
if (values.length < MIN_SAMPLE_SIZE) return DIV_ZERO_ERROR;
|
|
81
|
-
const mean = arithmeticMean(values);
|
|
82
|
-
const sumSquaredDiffs = values.reduce((sum, value) => sum + (value - mean) ** 2, 0);
|
|
83
|
-
return sumSquaredDiffs / (values.length - 1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Sample standard deviation (Excel STDEV): the square root of the sample
|
|
87
|
-
* variance, and `#DIV/0!` on the same fewer-than-two-values boundary. */
|
|
88
|
-
export function sampleStdev(values: number[]): number | SpreadsheetError {
|
|
89
|
-
const variance = sampleVariance(values);
|
|
90
|
-
return typeof variance === "number" ? Math.sqrt(variance) : variance;
|
|
91
|
-
}
|