@pixerate/editor 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -2
- package/dist/agent/index.cjs +625 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +286 -2
- package/dist/agent/index.d.ts +286 -2
- package/dist/agent/index.js +624 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +1240 -22
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +515 -3
- package/dist/canvas/index.d.ts +515 -3
- package/dist/canvas/index.js +1219 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-3TYMNULT.js +1367 -0
- package/dist/chunk-3TYMNULT.js.map +1 -0
- package/dist/chunk-4QPLEUL2.cjs +4480 -0
- package/dist/chunk-4QPLEUL2.cjs.map +1 -0
- package/dist/chunk-4SUNJZWB.cjs +553 -0
- package/dist/chunk-4SUNJZWB.cjs.map +1 -0
- package/dist/chunk-A7DKIA72.js +2657 -0
- package/dist/chunk-A7DKIA72.js.map +1 -0
- package/dist/chunk-AXV2ZW34.cjs +118 -0
- package/dist/chunk-AXV2ZW34.cjs.map +1 -0
- package/dist/chunk-B2ZRLN4S.js +118 -0
- package/dist/chunk-B2ZRLN4S.js.map +1 -0
- package/dist/chunk-CYFJCKC4.js +4480 -0
- package/dist/chunk-CYFJCKC4.js.map +1 -0
- package/dist/chunk-GVW4P7BG.js +2706 -0
- package/dist/chunk-GVW4P7BG.js.map +1 -0
- package/dist/chunk-JACER6TE.cjs +1 -0
- package/dist/chunk-JACER6TE.cjs.map +1 -0
- package/dist/chunk-JYRIOFEN.cjs +2657 -0
- package/dist/chunk-JYRIOFEN.cjs.map +1 -0
- package/dist/chunk-KYRZDWM4.js +299 -0
- package/dist/chunk-KYRZDWM4.js.map +1 -0
- package/dist/chunk-LA7PEGU3.cjs +299 -0
- package/dist/chunk-LA7PEGU3.cjs.map +1 -0
- package/dist/chunk-N7FG6CBO.js +1 -0
- package/dist/chunk-N7FG6CBO.js.map +1 -0
- package/dist/chunk-OYXSKQFR.js +553 -0
- package/dist/chunk-OYXSKQFR.js.map +1 -0
- package/dist/chunk-TVRC4KVT.cjs +1367 -0
- package/dist/chunk-TVRC4KVT.cjs.map +1 -0
- package/dist/chunk-U6NDQFOK.cjs +2706 -0
- package/dist/chunk-U6NDQFOK.cjs.map +1 -0
- package/dist/controller-OUdxPY-X.d.cts +754 -0
- package/dist/controller-OUdxPY-X.d.ts +754 -0
- package/dist/extensions/index.cjs +144 -3
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +9 -144
- package/dist/extensions/index.d.ts +9 -144
- package/dist/extensions/index.js +145 -4
- package/dist/grammar/index.cjs +31 -3
- package/dist/grammar/index.cjs.map +1 -1
- package/dist/grammar/index.d.cts +239 -19
- package/dist/grammar/index.d.ts +239 -19
- package/dist/grammar/index.js +31 -3
- package/dist/history/index.d.cts +2 -41
- package/dist/history/index.d.ts +2 -41
- package/dist/image-editor/index.cjs +732 -497
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +105 -9
- package/dist/image-editor/index.d.ts +105 -9
- package/dist/image-editor/index.js +721 -486
- package/dist/image-editor/index.js.map +1 -1
- package/dist/index-Ng1pG7JE.d.ts +899 -0
- package/dist/index-k3mFbPx_.d.cts +899 -0
- package/dist/index.cjs +457 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -12
- package/dist/index.d.ts +99 -12
- package/dist/index.js +457 -20
- package/dist/index.js.map +1 -1
- package/dist/registry-iAHbqkYt.d.cts +128 -0
- package/dist/registry-iAHbqkYt.d.ts +128 -0
- package/dist/serializers/index.cjs +26 -3
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +119 -6
- package/dist/serializers/index.d.ts +119 -6
- package/dist/serializers/index.js +24 -1
- package/dist/spreadsheet/index.cjs +122 -2
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +371 -313
- package/dist/spreadsheet/index.d.ts +371 -313
- package/dist/spreadsheet/index.js +127 -7
- package/dist/tokens-CyLZmIei.d.cts +76 -0
- package/dist/tokens-CyLZmIei.d.ts +76 -0
- package/dist/track-changes-G8JcWB8b.d.cts +306 -0
- package/dist/track-changes-G8JcWB8b.d.ts +306 -0
- package/dist/types-BZgcDsC5.d.cts +42 -0
- package/dist/types-BZgcDsC5.d.ts +42 -0
- package/dist/{types-JV3S8VeR.d.cts → types-DMe0vFB1.d.cts} +1 -1
- package/dist/{types-JV3S8VeR.d.ts → types-DMe0vFB1.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-7FWEWE6T.js +0 -1888
- package/dist/chunk-7FWEWE6T.js.map +0 -1
- package/dist/chunk-AIZUKWTL.js +0 -1715
- package/dist/chunk-AIZUKWTL.js.map +0 -1
- package/dist/chunk-GLSIZKOX.js +0 -17
- package/dist/chunk-GLSIZKOX.js.map +0 -1
- package/dist/chunk-LDES3EOS.cjs +0 -1888
- package/dist/chunk-LDES3EOS.cjs.map +0 -1
- package/dist/chunk-LLUAKTLW.cjs +0 -532
- package/dist/chunk-LLUAKTLW.cjs.map +0 -1
- package/dist/chunk-MPPXH7U5.cjs +0 -336
- package/dist/chunk-MPPXH7U5.cjs.map +0 -1
- package/dist/chunk-O7LOPQMH.js +0 -336
- package/dist/chunk-O7LOPQMH.js.map +0 -1
- package/dist/chunk-QGVMBCT5.js +0 -532
- package/dist/chunk-QGVMBCT5.js.map +0 -1
- package/dist/chunk-SEDR75VH.cjs +0 -17
- package/dist/chunk-SEDR75VH.cjs.map +0 -1
- package/dist/chunk-YQCI4CCN.cjs +0 -1715
- package/dist/chunk-YQCI4CCN.cjs.map +0 -1
- package/dist/mention-Dcevh3ff.d.cts +0 -32
- package/dist/mention-Dcevh3ff.d.ts +0 -32
- package/dist/tokens-7kRTEtPb.d.cts +0 -39
- package/dist/tokens-7kRTEtPb.d.ts +0 -39
|
@@ -0,0 +1,2657 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;// src/spreadsheet/formula/lexer.ts
|
|
2
|
+
function tokenizeFormula(formula) {
|
|
3
|
+
const tokens = [];
|
|
4
|
+
let input = formula.trim();
|
|
5
|
+
let offset = 0;
|
|
6
|
+
if (input.startsWith("=")) {
|
|
7
|
+
input = input.slice(1);
|
|
8
|
+
offset = 1;
|
|
9
|
+
}
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (i < input.length) {
|
|
12
|
+
const char = input[i];
|
|
13
|
+
if (/\s/.test(char)) {
|
|
14
|
+
i++;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (/\d/.test(char) || char === "." && i + 1 < input.length && /\d/.test(input[i + 1])) {
|
|
18
|
+
const start = i;
|
|
19
|
+
let hasDot = char === ".";
|
|
20
|
+
i++;
|
|
21
|
+
while (i < input.length && (/\d/.test(input[i]) || input[i] === "." && !hasDot)) {
|
|
22
|
+
if (input[i] === ".") hasDot = true;
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
tokens.push({
|
|
26
|
+
type: "NUMBER",
|
|
27
|
+
value: input.slice(start, i),
|
|
28
|
+
position: start + offset
|
|
29
|
+
});
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (char === '"' || char === "'") {
|
|
33
|
+
const quote2 = char;
|
|
34
|
+
const start = i;
|
|
35
|
+
i++;
|
|
36
|
+
let str = "";
|
|
37
|
+
while (i < input.length && input[i] !== quote2) {
|
|
38
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
str += input[i];
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
if (i >= input.length) {
|
|
45
|
+
throw new Error(`Unterminated string starting at position ${start + offset}`);
|
|
46
|
+
}
|
|
47
|
+
i++;
|
|
48
|
+
tokens.push({
|
|
49
|
+
type: "STRING",
|
|
50
|
+
value: str,
|
|
51
|
+
position: start + offset
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (char === "#") {
|
|
56
|
+
const match = /^#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|CYCLE!|ERROR!)/.exec(input.slice(i));
|
|
57
|
+
if (!match) {
|
|
58
|
+
throw new Error(`Unexpected character '#' at position ${i + offset}`);
|
|
59
|
+
}
|
|
60
|
+
tokens.push({ type: "ERROR", value: match[0], position: i + offset });
|
|
61
|
+
i += match[0].length;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (char === "[") {
|
|
65
|
+
const start = i;
|
|
66
|
+
i++;
|
|
67
|
+
let prop = "";
|
|
68
|
+
while (i < input.length && input[i] !== "]") {
|
|
69
|
+
prop += input[i];
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
if (i < input.length && input[i] === "]") {
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
tokens.push({
|
|
76
|
+
type: "PROPERTY_REF",
|
|
77
|
+
value: prop.trim(),
|
|
78
|
+
position: start + offset
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === "(") {
|
|
83
|
+
tokens.push({ type: "LPAREN", value: "(", position: i + offset });
|
|
84
|
+
i++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === ")") {
|
|
88
|
+
tokens.push({ type: "RPAREN", value: ")", position: i + offset });
|
|
89
|
+
i++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === ",") {
|
|
93
|
+
tokens.push({ type: "COMMA", value: ",", position: i + offset });
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const twoChars = input.slice(i, i + 2);
|
|
98
|
+
if (["<=", ">=", "<>", "!="].includes(twoChars)) {
|
|
99
|
+
tokens.push({
|
|
100
|
+
type: "OPERATOR",
|
|
101
|
+
value: twoChars === "!=" ? "<>" : twoChars,
|
|
102
|
+
position: i + offset
|
|
103
|
+
});
|
|
104
|
+
i += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (["+", "-", "*", "/", "^", "&", "=", "<", ">"].includes(char)) {
|
|
108
|
+
tokens.push({ type: "OPERATOR", value: char, position: i + offset });
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (/[a-zA-Z_$]/.test(char)) {
|
|
113
|
+
const start = i;
|
|
114
|
+
while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
const rawWord = input.slice(start, i);
|
|
118
|
+
if (i < input.length && input[i] === ":") {
|
|
119
|
+
const colonPos = i;
|
|
120
|
+
i++;
|
|
121
|
+
const secondStart = i;
|
|
122
|
+
while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
const secondWord = input.slice(secondStart, i);
|
|
126
|
+
if (secondWord.length > 0) {
|
|
127
|
+
tokens.push({
|
|
128
|
+
type: "RANGE",
|
|
129
|
+
value: `${rawWord}:${secondWord}`.toUpperCase(),
|
|
130
|
+
position: start + offset
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
} else {
|
|
134
|
+
i = colonPos;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const upper = rawWord.toUpperCase();
|
|
138
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
139
|
+
tokens.push({
|
|
140
|
+
type: "BOOLEAN",
|
|
141
|
+
value: upper,
|
|
142
|
+
position: start + offset
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (/^\$?[A-Za-z]+\$?[0-9]+$/.test(rawWord)) {
|
|
147
|
+
tokens.push({
|
|
148
|
+
type: "CELL_REF",
|
|
149
|
+
value: upper.replace(/\$/g, ""),
|
|
150
|
+
position: start + offset
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
tokens.push({
|
|
155
|
+
type: "IDENTIFIER",
|
|
156
|
+
value: upper,
|
|
157
|
+
position: start + offset
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Unexpected character '${char}' at position ${i + offset}`);
|
|
162
|
+
}
|
|
163
|
+
tokens.push({ type: "EOF", value: "", position: input.length + offset });
|
|
164
|
+
return tokens;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/spreadsheet/formula/parser.ts
|
|
168
|
+
var WHOLE_COLUMN_END_ROW = 1048576;
|
|
169
|
+
function parseCellReference(ref) {
|
|
170
|
+
const match = ref.match(/^([A-Za-z]+)(\d+)$/);
|
|
171
|
+
if (!match) {
|
|
172
|
+
throw new Error(`Invalid cell reference: ${ref}`);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
col: match[1].toUpperCase(),
|
|
176
|
+
row: parseInt(match[2], 10)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function parseRange(rangeStr) {
|
|
180
|
+
const [start, end] = rangeStr.replace(/\$/g, "").split(":");
|
|
181
|
+
const raw = rangeStr.toUpperCase();
|
|
182
|
+
if (/^[A-Za-z]+$/.test(start) && /^[A-Za-z]+$/.test(end)) {
|
|
183
|
+
return {
|
|
184
|
+
type: "Range",
|
|
185
|
+
raw,
|
|
186
|
+
startCol: start.toUpperCase(),
|
|
187
|
+
startRow: 1,
|
|
188
|
+
endCol: end.toUpperCase(),
|
|
189
|
+
endRow: WHOLE_COLUMN_END_ROW
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const startRef = parseCellReference(start);
|
|
193
|
+
const endRef = parseCellReference(end);
|
|
194
|
+
return {
|
|
195
|
+
type: "Range",
|
|
196
|
+
raw,
|
|
197
|
+
startCol: startRef.col,
|
|
198
|
+
startRow: startRef.row,
|
|
199
|
+
endCol: endRef.col,
|
|
200
|
+
endRow: endRef.row
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
var FormulaParser = (_class = class {
|
|
204
|
+
__init() {this.tokens = []}
|
|
205
|
+
__init2() {this.current = 0}
|
|
206
|
+
constructor(tokens) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
|
|
207
|
+
this.tokens = tokens;
|
|
208
|
+
}
|
|
209
|
+
parse() {
|
|
210
|
+
if (this.peek().type === "EOF") {
|
|
211
|
+
return { type: "Literal", value: "" };
|
|
212
|
+
}
|
|
213
|
+
const expr = this.expression();
|
|
214
|
+
if (this.peek().type !== "EOF") {
|
|
215
|
+
throw new Error(`Unexpected token at position ${this.peek().position}: ${this.peek().value}`);
|
|
216
|
+
}
|
|
217
|
+
return expr;
|
|
218
|
+
}
|
|
219
|
+
expression() {
|
|
220
|
+
return this.comparison();
|
|
221
|
+
}
|
|
222
|
+
comparison() {
|
|
223
|
+
let expr = this.concatenation();
|
|
224
|
+
while (this.matchOperator("=", "<>", "<", ">", "<=", ">=")) {
|
|
225
|
+
const op = this.previous().value;
|
|
226
|
+
const right = this.concatenation();
|
|
227
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
228
|
+
}
|
|
229
|
+
return expr;
|
|
230
|
+
}
|
|
231
|
+
concatenation() {
|
|
232
|
+
let expr = this.addition();
|
|
233
|
+
while (this.matchOperator("&")) {
|
|
234
|
+
const op = this.previous().value;
|
|
235
|
+
const right = this.addition();
|
|
236
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
237
|
+
}
|
|
238
|
+
return expr;
|
|
239
|
+
}
|
|
240
|
+
addition() {
|
|
241
|
+
let expr = this.multiplication();
|
|
242
|
+
while (this.matchOperator("+", "-")) {
|
|
243
|
+
const op = this.previous().value;
|
|
244
|
+
const right = this.multiplication();
|
|
245
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
246
|
+
}
|
|
247
|
+
return expr;
|
|
248
|
+
}
|
|
249
|
+
multiplication() {
|
|
250
|
+
let expr = this.exponentiation();
|
|
251
|
+
while (this.matchOperator("*", "/")) {
|
|
252
|
+
const op = this.previous().value;
|
|
253
|
+
const right = this.exponentiation();
|
|
254
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
255
|
+
}
|
|
256
|
+
return expr;
|
|
257
|
+
}
|
|
258
|
+
exponentiation() {
|
|
259
|
+
let expr = this.unary();
|
|
260
|
+
while (this.matchOperator("^")) {
|
|
261
|
+
const op = this.previous().value;
|
|
262
|
+
const right = this.unary();
|
|
263
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
264
|
+
}
|
|
265
|
+
return expr;
|
|
266
|
+
}
|
|
267
|
+
unary() {
|
|
268
|
+
if (this.matchOperator("-", "+")) {
|
|
269
|
+
const op = this.previous().value;
|
|
270
|
+
const argument = this.unary();
|
|
271
|
+
return { type: "UnaryOp", op, argument };
|
|
272
|
+
}
|
|
273
|
+
return this.primary();
|
|
274
|
+
}
|
|
275
|
+
primary() {
|
|
276
|
+
const token = this.peek();
|
|
277
|
+
if (token.type === "NUMBER") {
|
|
278
|
+
this.advance();
|
|
279
|
+
return { type: "Literal", value: parseFloat(token.value) };
|
|
280
|
+
}
|
|
281
|
+
if (token.type === "STRING") {
|
|
282
|
+
this.advance();
|
|
283
|
+
return { type: "Literal", value: token.value };
|
|
284
|
+
}
|
|
285
|
+
if (token.type === "BOOLEAN") {
|
|
286
|
+
this.advance();
|
|
287
|
+
return { type: "Literal", value: token.value === "TRUE" };
|
|
288
|
+
}
|
|
289
|
+
if (token.type === "ERROR") {
|
|
290
|
+
this.advance();
|
|
291
|
+
return { type: "Literal", value: token.value };
|
|
292
|
+
}
|
|
293
|
+
if (token.type === "CELL_REF") {
|
|
294
|
+
this.advance();
|
|
295
|
+
const parsed = parseCellReference(token.value);
|
|
296
|
+
return {
|
|
297
|
+
type: "CellRef",
|
|
298
|
+
ref: token.value,
|
|
299
|
+
col: parsed.col,
|
|
300
|
+
row: parsed.row
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (token.type === "RANGE") {
|
|
304
|
+
this.advance();
|
|
305
|
+
return parseRange(token.value);
|
|
306
|
+
}
|
|
307
|
+
if (token.type === "PROPERTY_REF") {
|
|
308
|
+
this.advance();
|
|
309
|
+
return {
|
|
310
|
+
type: "PropertyRef",
|
|
311
|
+
property: token.value
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (token.type === "IDENTIFIER") {
|
|
315
|
+
const name = token.value;
|
|
316
|
+
this.advance();
|
|
317
|
+
if (this.check("LPAREN")) {
|
|
318
|
+
this.consume("LPAREN", `Expected '(' after function ${name}`);
|
|
319
|
+
const args = [];
|
|
320
|
+
if (!this.check("RPAREN")) {
|
|
321
|
+
do {
|
|
322
|
+
args.push(this.expression());
|
|
323
|
+
} while (this.match("COMMA"));
|
|
324
|
+
}
|
|
325
|
+
this.consume("RPAREN", `Expected ')' after function arguments`);
|
|
326
|
+
return {
|
|
327
|
+
type: "FunctionCall",
|
|
328
|
+
name,
|
|
329
|
+
args
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return { type: "Literal", value: "#NAME?" };
|
|
333
|
+
}
|
|
334
|
+
if (this.match("LPAREN")) {
|
|
335
|
+
const expr = this.expression();
|
|
336
|
+
this.consume("RPAREN", `Expected ')' after expression`);
|
|
337
|
+
return expr;
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`Unexpected token at position ${token.position}: ${token.value || token.type}`);
|
|
340
|
+
}
|
|
341
|
+
match(...types) {
|
|
342
|
+
for (const type of types) {
|
|
343
|
+
if (this.check(type)) {
|
|
344
|
+
this.advance();
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
matchOperator(...ops) {
|
|
351
|
+
if (this.check("OPERATOR") && ops.includes(this.peek().value)) {
|
|
352
|
+
this.advance();
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
check(type) {
|
|
358
|
+
if (this.isAtEnd()) return false;
|
|
359
|
+
return this.peek().type === type;
|
|
360
|
+
}
|
|
361
|
+
advance() {
|
|
362
|
+
if (!this.isAtEnd()) this.current++;
|
|
363
|
+
return this.previous();
|
|
364
|
+
}
|
|
365
|
+
isAtEnd() {
|
|
366
|
+
return this.peek().type === "EOF";
|
|
367
|
+
}
|
|
368
|
+
peek() {
|
|
369
|
+
return this.tokens[this.current];
|
|
370
|
+
}
|
|
371
|
+
previous() {
|
|
372
|
+
return this.tokens[this.current - 1];
|
|
373
|
+
}
|
|
374
|
+
consume(type, message) {
|
|
375
|
+
if (this.check(type)) return this.advance();
|
|
376
|
+
throw new Error(message);
|
|
377
|
+
}
|
|
378
|
+
}, _class);
|
|
379
|
+
function parseFormula(formula) {
|
|
380
|
+
const tokens = tokenizeFormula(formula);
|
|
381
|
+
const parser = new FormulaParser(tokens);
|
|
382
|
+
return parser.parse();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/spreadsheet/formula/values.ts
|
|
386
|
+
var FORMULA_ERRORS = /* @__PURE__ */ new Set([
|
|
387
|
+
"#DIV/0!",
|
|
388
|
+
"#VALUE!",
|
|
389
|
+
"#REF!",
|
|
390
|
+
"#NAME?",
|
|
391
|
+
"#N/A",
|
|
392
|
+
"#NUM!",
|
|
393
|
+
"#CYCLE!",
|
|
394
|
+
"#ERROR!"
|
|
395
|
+
]);
|
|
396
|
+
function isFormulaError(value) {
|
|
397
|
+
return typeof value === "string" && FORMULA_ERRORS.has(value);
|
|
398
|
+
}
|
|
399
|
+
var isBlank = (value) => value === "" || value === null || value === void 0;
|
|
400
|
+
function toNumber(value) {
|
|
401
|
+
if (isFormulaError(value)) return value;
|
|
402
|
+
if (isBlank(value)) return 0;
|
|
403
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
404
|
+
if (typeof value === "number") return value;
|
|
405
|
+
const trimmed = String(value).trim();
|
|
406
|
+
const num = trimmed === "" ? NaN : Number(trimmed);
|
|
407
|
+
return isNaN(num) ? "#VALUE!" : num;
|
|
408
|
+
}
|
|
409
|
+
function toBoolean(value) {
|
|
410
|
+
if (isFormulaError(value)) return value;
|
|
411
|
+
if (isBlank(value)) return false;
|
|
412
|
+
if (typeof value === "boolean") return value;
|
|
413
|
+
if (typeof value === "number") return value !== 0;
|
|
414
|
+
const upper = String(value).trim().toUpperCase();
|
|
415
|
+
if (upper === "TRUE") return true;
|
|
416
|
+
if (upper === "FALSE") return false;
|
|
417
|
+
return "#VALUE!";
|
|
418
|
+
}
|
|
419
|
+
var toText = (value) => isBlank(value) ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
420
|
+
function numericValue(value) {
|
|
421
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
422
|
+
if (typeof value !== "string" || FORMULA_ERRORS.has(value)) return null;
|
|
423
|
+
const trimmed = value.trim();
|
|
424
|
+
if (trimmed === "") return null;
|
|
425
|
+
const num = Number(trimmed);
|
|
426
|
+
return Number.isFinite(num) ? num : null;
|
|
427
|
+
}
|
|
428
|
+
function normalizeValue(value) {
|
|
429
|
+
const num = typeof value === "string" ? numericValue(value) : null;
|
|
430
|
+
return num === null ? value : num;
|
|
431
|
+
}
|
|
432
|
+
function compareValues(left, right) {
|
|
433
|
+
const rank = (v) => typeof v === "boolean" ? 2 : typeof v === "string" ? 1 : 0;
|
|
434
|
+
const l = isBlank(left) ? typeof right === "string" ? "" : 0 : left;
|
|
435
|
+
const r = isBlank(right) ? typeof l === "string" ? "" : 0 : right;
|
|
436
|
+
if (rank(l) !== rank(r)) return rank(l) - rank(r);
|
|
437
|
+
if (typeof l === "string") {
|
|
438
|
+
const a = l.toLowerCase();
|
|
439
|
+
const b = r.toLowerCase();
|
|
440
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
441
|
+
}
|
|
442
|
+
return Number(l) - Number(r);
|
|
443
|
+
}
|
|
444
|
+
function roundHalfAwayFromZero(value, decimals) {
|
|
445
|
+
const factor = Math.pow(10, decimals);
|
|
446
|
+
const scaled = Number((Math.abs(value) * factor).toPrecision(15));
|
|
447
|
+
return Math.sign(value) * Math.round(scaled) / factor;
|
|
448
|
+
}
|
|
449
|
+
var cleanFloat = (value) => Number(value.toPrecision(15));
|
|
450
|
+
function colNameToIndex(col) {
|
|
451
|
+
let index = 0;
|
|
452
|
+
const upper = col.toUpperCase();
|
|
453
|
+
for (let i = 0; i < upper.length; i++) {
|
|
454
|
+
index = index * 26 + (upper.charCodeAt(i) - 65 + 1);
|
|
455
|
+
}
|
|
456
|
+
return index - 1;
|
|
457
|
+
}
|
|
458
|
+
function indexToColName(index) {
|
|
459
|
+
let num = index + 1;
|
|
460
|
+
let name = "";
|
|
461
|
+
while (num > 0) {
|
|
462
|
+
const mod = (num - 1) % 26;
|
|
463
|
+
name = String.fromCharCode(65 + mod) + name;
|
|
464
|
+
num = Math.floor((num - mod) / 26);
|
|
465
|
+
}
|
|
466
|
+
return name;
|
|
467
|
+
}
|
|
468
|
+
var rangeShapes = /* @__PURE__ */ new WeakMap();
|
|
469
|
+
function withRangeShape(values, rows, cols) {
|
|
470
|
+
rangeShapes.set(values, { rows, cols });
|
|
471
|
+
return values;
|
|
472
|
+
}
|
|
473
|
+
function toFormulaRange(values) {
|
|
474
|
+
const shape = rangeShapes.get(values);
|
|
475
|
+
if (shape && shape.rows * shape.cols === values.length) return { ...shape, values };
|
|
476
|
+
return { rows: values.length, cols: 1, values };
|
|
477
|
+
}
|
|
478
|
+
var rangeGet = (range, row, col) => row < range.rows && col < range.cols ? range.values[col * range.rows + row] : "";
|
|
479
|
+
function sliceRange(range, rowStart, rowCount, colStart, colCount) {
|
|
480
|
+
const out = [];
|
|
481
|
+
for (let c = colStart; c < colStart + colCount; c++) {
|
|
482
|
+
for (let r = rowStart; r < rowStart + rowCount; r++) out.push(rangeGet(range, r, c));
|
|
483
|
+
}
|
|
484
|
+
return withRangeShape(out, rowCount, colCount);
|
|
485
|
+
}
|
|
486
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
487
|
+
var pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
488
|
+
function toDate(value) {
|
|
489
|
+
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
|
|
490
|
+
if (typeof value === "number") return serialToDate(value);
|
|
491
|
+
if (typeof value !== "string" || FORMULA_ERRORS.has(value)) return null;
|
|
492
|
+
const text = value.trim();
|
|
493
|
+
if (text === "") return null;
|
|
494
|
+
const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(text);
|
|
495
|
+
if (iso) {
|
|
496
|
+
const [, y, m, d, hh = "0", mm = "0", ss = "0"] = iso;
|
|
497
|
+
const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));
|
|
498
|
+
return date.getMonth() === Number(m) - 1 ? date : null;
|
|
499
|
+
}
|
|
500
|
+
const serial = numericValue(text);
|
|
501
|
+
if (serial !== null) return serialToDate(serial);
|
|
502
|
+
if (/[a-z/]/i.test(text)) {
|
|
503
|
+
const parsed = Date.parse(text);
|
|
504
|
+
return isNaN(parsed) ? null : new Date(parsed);
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
function dateToSerial(date) {
|
|
509
|
+
const utc = Date.UTC(
|
|
510
|
+
date.getFullYear(),
|
|
511
|
+
date.getMonth(),
|
|
512
|
+
date.getDate(),
|
|
513
|
+
date.getHours(),
|
|
514
|
+
date.getMinutes(),
|
|
515
|
+
date.getSeconds()
|
|
516
|
+
);
|
|
517
|
+
return cleanFloat((utc - Date.UTC(1899, 11, 30)) / MS_PER_DAY);
|
|
518
|
+
}
|
|
519
|
+
function serialToDate(serial) {
|
|
520
|
+
if (!Number.isFinite(serial)) return null;
|
|
521
|
+
const utc = new Date(Date.UTC(1899, 11, 30) + Math.round(serial * MS_PER_DAY));
|
|
522
|
+
return new Date(
|
|
523
|
+
utc.getUTCFullYear(),
|
|
524
|
+
utc.getUTCMonth(),
|
|
525
|
+
utc.getUTCDate(),
|
|
526
|
+
utc.getUTCHours(),
|
|
527
|
+
utc.getUTCMinutes(),
|
|
528
|
+
utc.getUTCSeconds()
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
var formatIsoDate = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
532
|
+
var formatIsoDateTime = (date) => `${formatIsoDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
533
|
+
var daysBetween = (start, end) => Math.round(
|
|
534
|
+
(Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()) - Date.UTC(start.getFullYear(), start.getMonth(), start.getDate())) / MS_PER_DAY
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// src/spreadsheet/formula/criteria.ts
|
|
538
|
+
function wildcardToRegExp(pattern) {
|
|
539
|
+
let source = "";
|
|
540
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
541
|
+
const char = pattern[i];
|
|
542
|
+
if (char === "~" && i + 1 < pattern.length) {
|
|
543
|
+
source += escapeRegExp(pattern[++i]);
|
|
544
|
+
} else if (char === "*") {
|
|
545
|
+
source += "[\\s\\S]*";
|
|
546
|
+
} else if (char === "?") {
|
|
547
|
+
source += "[\\s\\S]";
|
|
548
|
+
} else {
|
|
549
|
+
source += escapeRegExp(char);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return new RegExp(`^${source}$`, "i");
|
|
553
|
+
}
|
|
554
|
+
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
555
|
+
var hasWildcards = (pattern) => /(^|[^~])[*?]/.test(pattern);
|
|
556
|
+
function parseCriteria(criterion) {
|
|
557
|
+
if (typeof criterion === "number") {
|
|
558
|
+
return (value) => numericValue(value) === criterion;
|
|
559
|
+
}
|
|
560
|
+
if (typeof criterion === "boolean") {
|
|
561
|
+
return (value) => value === criterion;
|
|
562
|
+
}
|
|
563
|
+
if (isBlank(criterion)) return (value) => isBlank(value);
|
|
564
|
+
const text = String(criterion);
|
|
565
|
+
const match = /^(<>|<=|>=|=|<|>)?([\s\S]*)$/.exec(text);
|
|
566
|
+
const op = _nullishCoalesce(match[1], () => ( "="));
|
|
567
|
+
const operand = match[2];
|
|
568
|
+
if (operand === "") {
|
|
569
|
+
if (op === "=") return (value) => isBlank(value);
|
|
570
|
+
if (op === "<>") return (value) => !isBlank(value);
|
|
571
|
+
return () => false;
|
|
572
|
+
}
|
|
573
|
+
const operandNumber = numericValue(operand);
|
|
574
|
+
const operandUpper = operand.trim().toUpperCase();
|
|
575
|
+
const operandBoolean = operandUpper === "TRUE" ? true : operandUpper === "FALSE" ? false : null;
|
|
576
|
+
if (op === "=" || op === "<>") {
|
|
577
|
+
let equals;
|
|
578
|
+
if (operandNumber !== null) {
|
|
579
|
+
equals = (value) => numericValue(value) === operandNumber;
|
|
580
|
+
} else if (operandBoolean !== null) {
|
|
581
|
+
equals = (value) => value === operandBoolean || typeof value === "string" && value.trim().toUpperCase() === operandUpper;
|
|
582
|
+
} else if (isFormulaError(operand)) {
|
|
583
|
+
return op === "=" ? (value) => value === operand : (value) => value !== operand && !isBlank(value);
|
|
584
|
+
} else {
|
|
585
|
+
const regex = wildcardToRegExp(operand);
|
|
586
|
+
equals = (value) => typeof value === "string" && !isFormulaError(value) && regex.test(value);
|
|
587
|
+
}
|
|
588
|
+
return op === "=" ? (value) => !isFormulaError(value) && equals(value) : (value) => !isFormulaError(value) && !equals(value);
|
|
589
|
+
}
|
|
590
|
+
const test = (cmp) => op === "<" ? cmp < 0 : op === "<=" ? cmp <= 0 : op === ">" ? cmp > 0 : cmp >= 0;
|
|
591
|
+
if (operandNumber !== null) {
|
|
592
|
+
return (value) => {
|
|
593
|
+
const num = numericValue(value);
|
|
594
|
+
return num !== null && test(num - operandNumber);
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const lower = operand.toLowerCase();
|
|
598
|
+
return (value) => {
|
|
599
|
+
if (typeof value !== "string" || value === "" || FORMULA_ERRORS.has(value) || numericValue(value) !== null) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const v = value.toLowerCase();
|
|
603
|
+
return test(v < lower ? -1 : v > lower ? 1 : 0);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/spreadsheet/formula/format.ts
|
|
608
|
+
function formatValueWithCode(value, format) {
|
|
609
|
+
if (isDateFormat(format)) {
|
|
610
|
+
const date = toDate(value);
|
|
611
|
+
return date ? formatDate(date, format) : null;
|
|
612
|
+
}
|
|
613
|
+
const num = typeof value === "boolean" ? null : numericValue(value);
|
|
614
|
+
return num === null ? toText(value) : formatNumber(num, format);
|
|
615
|
+
}
|
|
616
|
+
function tokenize(format) {
|
|
617
|
+
const out = [];
|
|
618
|
+
const push = (literal, text) => {
|
|
619
|
+
const last = out[out.length - 1];
|
|
620
|
+
if (last && last.literal === literal) last.text += text;
|
|
621
|
+
else out.push({ literal, text });
|
|
622
|
+
};
|
|
623
|
+
for (let i = 0; i < format.length; i++) {
|
|
624
|
+
const char = format[i];
|
|
625
|
+
if (char === '"') {
|
|
626
|
+
const end = format.indexOf('"', i + 1);
|
|
627
|
+
const stop = end === -1 ? format.length : end;
|
|
628
|
+
push(true, format.slice(i + 1, stop));
|
|
629
|
+
i = stop;
|
|
630
|
+
} else if (char === "\\" && i + 1 < format.length) {
|
|
631
|
+
push(true, format[++i]);
|
|
632
|
+
} else {
|
|
633
|
+
push(false, char);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
var codeText = (format) => tokenize(format).filter((t) => !t.literal).map((t) => t.text).join("");
|
|
639
|
+
function isDateFormat(format) {
|
|
640
|
+
const code = codeText(format);
|
|
641
|
+
if (/[0#?]/.test(code)) return false;
|
|
642
|
+
return /[ydhs]|m/i.test(code);
|
|
643
|
+
}
|
|
644
|
+
function formatNumber(num, format) {
|
|
645
|
+
const sections = splitSections(format);
|
|
646
|
+
let section = sections[0];
|
|
647
|
+
let value = num;
|
|
648
|
+
let sign = num < 0 ? "-" : "";
|
|
649
|
+
if (num < 0 && sections.length > 1) {
|
|
650
|
+
section = sections[1];
|
|
651
|
+
value = Math.abs(num);
|
|
652
|
+
sign = "";
|
|
653
|
+
} else if (num === 0 && sections.length > 2) {
|
|
654
|
+
section = sections[2];
|
|
655
|
+
}
|
|
656
|
+
let prefix = "";
|
|
657
|
+
let suffix = "";
|
|
658
|
+
let pattern = "";
|
|
659
|
+
for (const token of tokenize(section)) {
|
|
660
|
+
if (token.literal) {
|
|
661
|
+
if (pattern) suffix += token.text;
|
|
662
|
+
else prefix += token.text;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
for (let i = 0; i < token.text.length; i++) {
|
|
666
|
+
const char = token.text[i];
|
|
667
|
+
const isExponent = /e/i.test(char) && /[+-]/.test(_nullishCoalesce(token.text[i + 1], () => ( "")));
|
|
668
|
+
const isExponentSign = /[+-]/.test(char) && /e$/i.test(pattern);
|
|
669
|
+
if (/[0#?.,]/.test(char) || pattern && (isExponent || isExponentSign)) {
|
|
670
|
+
pattern += char;
|
|
671
|
+
} else if (pattern) {
|
|
672
|
+
suffix += char;
|
|
673
|
+
} else {
|
|
674
|
+
prefix += char;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const percents = (prefix + suffix).split("%").length - 1;
|
|
679
|
+
value = Math.abs(value) * Math.pow(100, percents);
|
|
680
|
+
if (!pattern) return `${sign}${prefix}${suffix}`;
|
|
681
|
+
const expMatch = /^([^Ee]*)[Ee]([+-])(0+)$/.exec(pattern);
|
|
682
|
+
if (expMatch) {
|
|
683
|
+
const [, mantissa, expSign, expDigits] = expMatch;
|
|
684
|
+
const decimals = (_nullishCoalesce(mantissa.split(".")[1], () => ( ""))).length;
|
|
685
|
+
const [m, e] = value.toExponential(decimals).split("e");
|
|
686
|
+
const exponent = Number(e);
|
|
687
|
+
const expText = String(Math.abs(exponent)).padStart(expDigits.length, "0");
|
|
688
|
+
const shownSign = exponent < 0 ? "-" : expSign === "+" ? "+" : "";
|
|
689
|
+
return `${sign}${prefix}${m}E${shownSign}${expText}${suffix}`;
|
|
690
|
+
}
|
|
691
|
+
const [intPattern, decPattern = ""] = pattern.split(".");
|
|
692
|
+
const maxDecimals = (_nullishCoalesce(decPattern.match(/[0#?]/g), () => ( []))).length;
|
|
693
|
+
const minDecimals = (_nullishCoalesce(decPattern.match(/0/g), () => ( []))).length;
|
|
694
|
+
const minInt = (_nullishCoalesce(intPattern.match(/0/g), () => ( []))).length;
|
|
695
|
+
const grouping = intPattern.includes(",");
|
|
696
|
+
const rounded = roundHalfAwayFromZero(value, maxDecimals);
|
|
697
|
+
let [intPart, decPart = ""] = rounded.toFixed(maxDecimals).split(".");
|
|
698
|
+
while (decPart.length > minDecimals && decPart.endsWith("0")) decPart = decPart.slice(0, -1);
|
|
699
|
+
if (intPart === "0" && minInt === 0) intPart = "";
|
|
700
|
+
intPart = intPart.padStart(minInt, "0");
|
|
701
|
+
if (grouping) intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
702
|
+
const showSign = sign && /[1-9]/.test(intPart + decPart) ? sign : "";
|
|
703
|
+
return `${showSign}${prefix}${intPart}${decPart ? `.${decPart}` : ""}${suffix}`;
|
|
704
|
+
}
|
|
705
|
+
function splitSections(format) {
|
|
706
|
+
const sections = [""];
|
|
707
|
+
let quoted = false;
|
|
708
|
+
for (let i = 0; i < format.length; i++) {
|
|
709
|
+
const char = format[i];
|
|
710
|
+
if (char === '"') quoted = !quoted;
|
|
711
|
+
if (char === "\\" && i + 1 < format.length) {
|
|
712
|
+
sections[sections.length - 1] += char + format[++i];
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (char === ";" && !quoted) sections.push("");
|
|
716
|
+
else sections[sections.length - 1] += char;
|
|
717
|
+
}
|
|
718
|
+
return sections;
|
|
719
|
+
}
|
|
720
|
+
var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
721
|
+
var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
722
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
723
|
+
function formatDate(date, format) {
|
|
724
|
+
const parts = [];
|
|
725
|
+
for (const token of tokenize(format)) {
|
|
726
|
+
if (token.literal) {
|
|
727
|
+
parts.push(token);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const re = /AM\/PM|am\/pm|A\/P|a\/p|y+|m+|d+|h+|s+|[^ymdhsaA]+|./gi;
|
|
731
|
+
for (const match of _nullishCoalesce(token.text.match(re), () => ( []))) {
|
|
732
|
+
const isCode = /^(AM\/PM|A\/P|y+|m+|d+|h+|s+)$/i.test(match);
|
|
733
|
+
parts.push({ literal: !isCode, text: match });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const codes = parts.map((p, i) => ({ ...p, i })).filter((p) => !p.literal);
|
|
737
|
+
const twelveHour = codes.some((p) => /^(AM\/PM|A\/P)$/i.test(p.text));
|
|
738
|
+
return parts.map((part, index) => {
|
|
739
|
+
if (part.literal) return part.text;
|
|
740
|
+
const code = part.text;
|
|
741
|
+
const lower = code.toLowerCase();
|
|
742
|
+
if (/^(am\/pm|a\/p)$/.test(lower)) {
|
|
743
|
+
const pm = date.getHours() >= 12;
|
|
744
|
+
const full = pm ? "PM" : "AM";
|
|
745
|
+
const text = lower === "a/p" ? full[0] : full;
|
|
746
|
+
return code[0] === code[0].toLowerCase() ? text.toLowerCase() : text;
|
|
747
|
+
}
|
|
748
|
+
if (lower.startsWith("y")) {
|
|
749
|
+
return lower.length <= 2 ? pad2(date.getFullYear() % 100) : String(date.getFullYear());
|
|
750
|
+
}
|
|
751
|
+
if (lower.startsWith("d")) {
|
|
752
|
+
if (lower.length >= 4) return DAYS[date.getDay()];
|
|
753
|
+
if (lower.length === 3) return DAYS[date.getDay()].slice(0, 3);
|
|
754
|
+
return lower.length === 2 ? pad2(date.getDate()) : String(date.getDate());
|
|
755
|
+
}
|
|
756
|
+
if (lower.startsWith("h")) {
|
|
757
|
+
let hours = date.getHours();
|
|
758
|
+
if (twelveHour) hours = hours % 12 || 12;
|
|
759
|
+
return lower.length >= 2 ? pad2(hours) : String(hours);
|
|
760
|
+
}
|
|
761
|
+
if (lower.startsWith("s")) {
|
|
762
|
+
return lower.length >= 2 ? pad2(date.getSeconds()) : String(date.getSeconds());
|
|
763
|
+
}
|
|
764
|
+
const position = codes.findIndex((c) => c.i === index);
|
|
765
|
+
const prev = _nullishCoalesce(_optionalChain([codes, 'access', _2 => _2[position - 1], 'optionalAccess', _3 => _3.text, 'access', _4 => _4.toLowerCase, 'call', _5 => _5()]), () => ( ""));
|
|
766
|
+
const next = _nullishCoalesce(_optionalChain([codes, 'access', _6 => _6[position + 1], 'optionalAccess', _7 => _7.text, 'access', _8 => _8.toLowerCase, 'call', _9 => _9()]), () => ( ""));
|
|
767
|
+
if (lower.length <= 2 && (prev.startsWith("h") || next.startsWith("s"))) {
|
|
768
|
+
return lower.length === 2 ? pad2(date.getMinutes()) : String(date.getMinutes());
|
|
769
|
+
}
|
|
770
|
+
if (lower.length >= 4) return MONTHS[date.getMonth()];
|
|
771
|
+
if (lower.length === 3) return MONTHS[date.getMonth()].slice(0, 3);
|
|
772
|
+
return lower.length === 2 ? pad2(date.getMonth() + 1) : String(date.getMonth() + 1);
|
|
773
|
+
}).join("");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/spreadsheet/formula/functions/helpers.ts
|
|
777
|
+
var arg = (name, description, type = "any", extra = {}) => ({ name, description, type, ...extra });
|
|
778
|
+
var optional = (name, description, type = "any") => arg(name, description, type, { optional: true });
|
|
779
|
+
var repeating = (name, description, type = "any") => arg(name, description, type, { repeating: true });
|
|
780
|
+
function failed(value) {
|
|
781
|
+
return isFormulaError(value);
|
|
782
|
+
}
|
|
783
|
+
function unaryMath(ctx, fn) {
|
|
784
|
+
const value = ctx.number(0);
|
|
785
|
+
if (failed(value)) return value;
|
|
786
|
+
const result = fn(value);
|
|
787
|
+
if (typeof result === "number" && !Number.isFinite(result)) {
|
|
788
|
+
return ctx.error("#NUM!", `${ctx.name} produced a number that is out of range.`);
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/spreadsheet/formula/functions/math.ts
|
|
794
|
+
var NUMBER = arg("number", "The number to use.", "number");
|
|
795
|
+
var NUM_DIGITS = optional("num_digits", "Decimal places to round to; negative values round to the left of the decimal point (default 0).", "number");
|
|
796
|
+
function roundWith(value, digits, mode) {
|
|
797
|
+
const factor = Math.pow(10, Math.trunc(digits));
|
|
798
|
+
const scaled = Number((Math.abs(value) * factor).toPrecision(15));
|
|
799
|
+
const rounded = mode === "up" ? Math.ceil(scaled) : Math.floor(scaled);
|
|
800
|
+
return cleanFloat(Math.sign(value) * rounded / factor);
|
|
801
|
+
}
|
|
802
|
+
var MATH_FUNCTIONS = [
|
|
803
|
+
{
|
|
804
|
+
name: "SUM",
|
|
805
|
+
category: "math",
|
|
806
|
+
description: "Adds numbers and the numbers in ranges. Text and blank cells are ignored.",
|
|
807
|
+
args: [repeating("value", "A number, cell or range to add.", "number")],
|
|
808
|
+
evaluate: (ctx) => {
|
|
809
|
+
const nums = ctx.numbers();
|
|
810
|
+
if (failed(nums)) return nums;
|
|
811
|
+
let total = 0;
|
|
812
|
+
for (const num of nums) total += num;
|
|
813
|
+
return total;
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
name: "PRODUCT",
|
|
818
|
+
category: "math",
|
|
819
|
+
description: "Multiplies numbers and the numbers in ranges. Text and blank cells are ignored.",
|
|
820
|
+
args: [repeating("value", "A number, cell or range to multiply.", "number")],
|
|
821
|
+
evaluate: (ctx) => {
|
|
822
|
+
const nums = ctx.numbers();
|
|
823
|
+
if (failed(nums)) return nums;
|
|
824
|
+
if (nums.length === 0) return 0;
|
|
825
|
+
let product = 1;
|
|
826
|
+
for (const num of nums) product *= num;
|
|
827
|
+
return cleanFloat(product);
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "ROUND",
|
|
832
|
+
category: "math",
|
|
833
|
+
description: "Rounds a number to a number of decimal places, halves away from zero.",
|
|
834
|
+
args: [NUMBER, NUM_DIGITS],
|
|
835
|
+
evaluate: (ctx) => {
|
|
836
|
+
const num = ctx.number(0);
|
|
837
|
+
if (failed(num)) return num;
|
|
838
|
+
const decimals = ctx.number(1, 0);
|
|
839
|
+
if (failed(decimals)) return decimals;
|
|
840
|
+
return roundHalfAwayFromZero(num, Math.trunc(decimals));
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "ROUNDUP",
|
|
845
|
+
category: "math",
|
|
846
|
+
description: "Rounds a number away from zero to a number of decimal places.",
|
|
847
|
+
args: [NUMBER, NUM_DIGITS],
|
|
848
|
+
evaluate: (ctx) => {
|
|
849
|
+
const num = ctx.number(0);
|
|
850
|
+
if (failed(num)) return num;
|
|
851
|
+
const digits = ctx.number(1, 0);
|
|
852
|
+
if (failed(digits)) return digits;
|
|
853
|
+
return roundWith(num, digits, "up");
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
name: "ROUNDDOWN",
|
|
858
|
+
category: "math",
|
|
859
|
+
description: "Rounds a number toward zero to a number of decimal places.",
|
|
860
|
+
args: [NUMBER, NUM_DIGITS],
|
|
861
|
+
evaluate: (ctx) => {
|
|
862
|
+
const num = ctx.number(0);
|
|
863
|
+
if (failed(num)) return num;
|
|
864
|
+
const digits = ctx.number(1, 0);
|
|
865
|
+
if (failed(digits)) return digits;
|
|
866
|
+
return roundWith(num, digits, "down");
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: "INT",
|
|
871
|
+
category: "math",
|
|
872
|
+
description: "Rounds a number down to the nearest integer (INT(-1.5) is -2).",
|
|
873
|
+
args: [NUMBER],
|
|
874
|
+
evaluate: (ctx) => unaryMath(ctx, Math.floor)
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
name: "ABS",
|
|
878
|
+
category: "math",
|
|
879
|
+
description: "Returns the absolute value of a number.",
|
|
880
|
+
args: [NUMBER],
|
|
881
|
+
evaluate: (ctx) => unaryMath(ctx, Math.abs)
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
name: "SIGN",
|
|
885
|
+
category: "math",
|
|
886
|
+
description: "Returns 1 for positive numbers, -1 for negative numbers and 0 for zero.",
|
|
887
|
+
args: [NUMBER],
|
|
888
|
+
evaluate: (ctx) => unaryMath(ctx, (n) => n > 0 ? 1 : n < 0 ? -1 : 0)
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
name: "SQRT",
|
|
892
|
+
category: "math",
|
|
893
|
+
description: "Returns the square root of a number.",
|
|
894
|
+
args: [NUMBER],
|
|
895
|
+
evaluate: (ctx) => unaryMath(
|
|
896
|
+
ctx,
|
|
897
|
+
(n) => n < 0 ? ctx.argError(0, "#NUM!", "can't be negative.") : Math.sqrt(n)
|
|
898
|
+
)
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "POWER",
|
|
902
|
+
category: "math",
|
|
903
|
+
description: "Raises a number to a power.",
|
|
904
|
+
args: [arg("number", "The base.", "number"), arg("power", "The exponent.", "number")],
|
|
905
|
+
evaluate: (ctx) => {
|
|
906
|
+
const base = ctx.number(0);
|
|
907
|
+
if (failed(base)) return base;
|
|
908
|
+
const exponent = ctx.number(1);
|
|
909
|
+
if (failed(exponent)) return exponent;
|
|
910
|
+
if (base === 0 && exponent < 0) return ctx.error("#DIV/0!", "POWER of zero to a negative exponent divides by zero.");
|
|
911
|
+
const result = Math.pow(base, exponent);
|
|
912
|
+
return Number.isFinite(result) ? result : ctx.error("#NUM!", "POWER produced a number that is out of range.");
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "MOD",
|
|
917
|
+
category: "math",
|
|
918
|
+
description: "Returns the remainder after division. The result has the sign of the divisor.",
|
|
919
|
+
args: [arg("number", "The number to divide.", "number"), arg("divisor", "The number to divide by.", "number")],
|
|
920
|
+
evaluate: (ctx) => {
|
|
921
|
+
const num = ctx.number(0);
|
|
922
|
+
if (failed(num)) return num;
|
|
923
|
+
const divisor = ctx.number(1);
|
|
924
|
+
if (failed(divisor)) return divisor;
|
|
925
|
+
if (divisor === 0) return ctx.argError(1, "#DIV/0!", "can't be zero.");
|
|
926
|
+
return cleanFloat(num - divisor * Math.floor(num / divisor));
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
name: "CEILING",
|
|
931
|
+
category: "math",
|
|
932
|
+
description: "Rounds a number up to the nearest multiple of significance.",
|
|
933
|
+
args: [NUMBER, optional("significance", "The multiple to round to (default 1).", "number")],
|
|
934
|
+
evaluate: (ctx) => roundToMultiple(ctx.name, ctx.number(0), ctx.number(1, 1), Math.ceil, ctx)
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
name: "FLOOR",
|
|
938
|
+
category: "math",
|
|
939
|
+
description: "Rounds a number down to the nearest multiple of significance.",
|
|
940
|
+
args: [NUMBER, optional("significance", "The multiple to round to (default 1).", "number")],
|
|
941
|
+
evaluate: (ctx) => roundToMultiple(ctx.name, ctx.number(0), ctx.number(1, 1), Math.floor, ctx)
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "PI",
|
|
945
|
+
category: "math",
|
|
946
|
+
description: "Returns the number \u03C0 (3.14159\u2026).",
|
|
947
|
+
args: [],
|
|
948
|
+
evaluate: () => Math.PI
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: "RAND",
|
|
952
|
+
category: "math",
|
|
953
|
+
description: "Returns a random number between 0 (inclusive) and 1 (exclusive). Changes on every recalculation.",
|
|
954
|
+
args: [],
|
|
955
|
+
volatile: true,
|
|
956
|
+
evaluate: () => Math.random()
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: "RANDBETWEEN",
|
|
960
|
+
category: "math",
|
|
961
|
+
description: "Returns a random integer between bottom and top, inclusive. Changes on every recalculation.",
|
|
962
|
+
args: [arg("bottom", "The smallest integer to return.", "number"), arg("top", "The largest integer to return.", "number")],
|
|
963
|
+
volatile: true,
|
|
964
|
+
evaluate: (ctx) => {
|
|
965
|
+
const bottom = ctx.number(0);
|
|
966
|
+
if (failed(bottom)) return bottom;
|
|
967
|
+
const top = ctx.number(1);
|
|
968
|
+
if (failed(top)) return top;
|
|
969
|
+
const lo = Math.ceil(bottom);
|
|
970
|
+
const hi = Math.floor(top);
|
|
971
|
+
if (lo > hi) return ctx.error("#NUM!", "RANDBETWEEN needs bottom to be less than or equal to top.");
|
|
972
|
+
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
];
|
|
976
|
+
function roundToMultiple(name, number, significance, round, ctx) {
|
|
977
|
+
if (failed(number)) return number;
|
|
978
|
+
if (failed(significance)) return significance;
|
|
979
|
+
if (number === 0) return 0;
|
|
980
|
+
if (significance === 0) {
|
|
981
|
+
return name === "FLOOR" ? ctx.error("#DIV/0!", "FLOOR needs a significance other than zero.") : 0;
|
|
982
|
+
}
|
|
983
|
+
if (number > 0 && significance < 0) {
|
|
984
|
+
return ctx.error("#NUM!", `${name} can't round a positive number to a negative significance.`);
|
|
985
|
+
}
|
|
986
|
+
return cleanFloat(round(cleanFloat(number / significance)) * significance);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/spreadsheet/formula/functions/statistical.ts
|
|
990
|
+
var VALUES = repeating("value", "A number, cell or range.", "number");
|
|
991
|
+
function kth(ctx, pick) {
|
|
992
|
+
const nums = ctx.numbers(0, 1);
|
|
993
|
+
if (failed(nums)) return nums;
|
|
994
|
+
const k = ctx.number(1);
|
|
995
|
+
if (failed(k)) return k;
|
|
996
|
+
const index = Math.ceil(k);
|
|
997
|
+
if (index < 1 || index > nums.length) {
|
|
998
|
+
return ctx.argError(1, "#NUM!", `must be between 1 and the number of values (${nums.length}).`);
|
|
999
|
+
}
|
|
1000
|
+
return pick([...nums].sort((a, b) => a - b), index);
|
|
1001
|
+
}
|
|
1002
|
+
function variance(ctx) {
|
|
1003
|
+
const nums = ctx.numbers();
|
|
1004
|
+
if (failed(nums)) return nums;
|
|
1005
|
+
if (nums.length < 2) return ctx.error("#DIV/0!", `${ctx.name} needs at least two numbers.`);
|
|
1006
|
+
let mean = 0;
|
|
1007
|
+
for (const num of nums) mean += num;
|
|
1008
|
+
mean /= nums.length;
|
|
1009
|
+
let squares = 0;
|
|
1010
|
+
for (const num of nums) squares += (num - mean) ** 2;
|
|
1011
|
+
return squares / (nums.length - 1);
|
|
1012
|
+
}
|
|
1013
|
+
var STATISTICAL_FUNCTIONS = [
|
|
1014
|
+
{
|
|
1015
|
+
name: "AVERAGE",
|
|
1016
|
+
category: "statistical",
|
|
1017
|
+
description: "Returns the arithmetic mean of numbers. Blank cells and text are ignored.",
|
|
1018
|
+
args: [VALUES],
|
|
1019
|
+
evaluate: (ctx) => {
|
|
1020
|
+
const items = ctx.flatValues();
|
|
1021
|
+
const error = items.find(isFormulaError);
|
|
1022
|
+
if (error) return error;
|
|
1023
|
+
let total = 0;
|
|
1024
|
+
let count = 0;
|
|
1025
|
+
for (const item of items) {
|
|
1026
|
+
const num = Number(item);
|
|
1027
|
+
if (!isNaN(num) && !isBlank(item)) {
|
|
1028
|
+
total += num;
|
|
1029
|
+
count++;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return count === 0 ? 0 : total / count;
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
name: "COUNT",
|
|
1037
|
+
category: "statistical",
|
|
1038
|
+
description: "Counts the values that are numbers.",
|
|
1039
|
+
args: [repeating("value", "A value, cell or range to count.")],
|
|
1040
|
+
evaluate: (ctx) => {
|
|
1041
|
+
let count = 0;
|
|
1042
|
+
for (const item of ctx.flatValues()) {
|
|
1043
|
+
if (!isNaN(Number(item)) && !isBlank(item)) count++;
|
|
1044
|
+
}
|
|
1045
|
+
return count;
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
name: "COUNTA",
|
|
1050
|
+
category: "statistical",
|
|
1051
|
+
description: "Counts the values that are not blank.",
|
|
1052
|
+
args: [repeating("value", "A value, cell or range to count.")],
|
|
1053
|
+
evaluate: (ctx) => ctx.flatValues().filter((item) => !isBlank(item)).length
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
name: "COUNTBLANK",
|
|
1057
|
+
category: "statistical",
|
|
1058
|
+
description: "Counts the blank cells in a range.",
|
|
1059
|
+
args: [arg("range", "The range to check.", "range")],
|
|
1060
|
+
evaluate: (ctx) => {
|
|
1061
|
+
const range = ctx.range(0);
|
|
1062
|
+
if (failed(range)) return range;
|
|
1063
|
+
return range.values.filter(isBlank).length;
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: "MIN",
|
|
1068
|
+
category: "statistical",
|
|
1069
|
+
description: "Returns the smallest number. Text and blank cells are ignored.",
|
|
1070
|
+
args: [VALUES],
|
|
1071
|
+
evaluate: (ctx) => {
|
|
1072
|
+
const nums = ctx.numbers();
|
|
1073
|
+
if (failed(nums)) return nums;
|
|
1074
|
+
if (nums.length === 0) return 0;
|
|
1075
|
+
let result = nums[0];
|
|
1076
|
+
for (const num of nums) result = Math.min(result, num);
|
|
1077
|
+
return result;
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
name: "MAX",
|
|
1082
|
+
category: "statistical",
|
|
1083
|
+
description: "Returns the largest number. Text and blank cells are ignored.",
|
|
1084
|
+
args: [VALUES],
|
|
1085
|
+
evaluate: (ctx) => {
|
|
1086
|
+
const nums = ctx.numbers();
|
|
1087
|
+
if (failed(nums)) return nums;
|
|
1088
|
+
if (nums.length === 0) return 0;
|
|
1089
|
+
let result = nums[0];
|
|
1090
|
+
for (const num of nums) result = Math.max(result, num);
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
name: "MEDIAN",
|
|
1096
|
+
category: "statistical",
|
|
1097
|
+
description: "Returns the middle number of a set of numbers.",
|
|
1098
|
+
args: [VALUES],
|
|
1099
|
+
evaluate: (ctx) => {
|
|
1100
|
+
const nums = ctx.numbers();
|
|
1101
|
+
if (failed(nums)) return nums;
|
|
1102
|
+
if (nums.length === 0) return ctx.error("#NUM!", "MEDIAN needs at least one number.");
|
|
1103
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
1104
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1105
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: "MODE",
|
|
1110
|
+
category: "statistical",
|
|
1111
|
+
description: "Returns the most frequently occurring number. Ties go to the number that appears first.",
|
|
1112
|
+
args: [VALUES],
|
|
1113
|
+
evaluate: (ctx) => {
|
|
1114
|
+
const nums = ctx.numbers();
|
|
1115
|
+
if (failed(nums)) return nums;
|
|
1116
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1117
|
+
let best = null;
|
|
1118
|
+
let bestCount = 1;
|
|
1119
|
+
for (const num of nums) {
|
|
1120
|
+
const count = (_nullishCoalesce(counts.get(num), () => ( 0))) + 1;
|
|
1121
|
+
counts.set(num, count);
|
|
1122
|
+
if (count > bestCount) {
|
|
1123
|
+
best = num;
|
|
1124
|
+
bestCount = count;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (best !== null) {
|
|
1128
|
+
for (const num of nums) {
|
|
1129
|
+
if (counts.get(num) === bestCount) return num;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return ctx.error("#N/A", "MODE found no number that appears more than once.");
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: "STDEV",
|
|
1137
|
+
category: "statistical",
|
|
1138
|
+
description: "Estimates the standard deviation of a sample.",
|
|
1139
|
+
args: [VALUES],
|
|
1140
|
+
evaluate: (ctx) => {
|
|
1141
|
+
const v = variance(ctx);
|
|
1142
|
+
return failed(v) ? v : cleanFloat(Math.sqrt(v));
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
name: "VAR",
|
|
1147
|
+
category: "statistical",
|
|
1148
|
+
description: "Estimates the variance of a sample.",
|
|
1149
|
+
args: [VALUES],
|
|
1150
|
+
evaluate: (ctx) => {
|
|
1151
|
+
const v = variance(ctx);
|
|
1152
|
+
return failed(v) ? v : cleanFloat(v);
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
name: "LARGE",
|
|
1157
|
+
category: "statistical",
|
|
1158
|
+
description: "Returns the k-th largest number (LARGE(range, 1) is the maximum).",
|
|
1159
|
+
args: [arg("array", "The numbers to look through.", "range"), arg("k", "Position from the largest (1 = largest).", "number")],
|
|
1160
|
+
evaluate: (ctx) => kth(ctx, (sorted, k) => sorted[sorted.length - k])
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
name: "SMALL",
|
|
1164
|
+
category: "statistical",
|
|
1165
|
+
description: "Returns the k-th smallest number (SMALL(range, 1) is the minimum).",
|
|
1166
|
+
args: [arg("array", "The numbers to look through.", "range"), arg("k", "Position from the smallest (1 = smallest).", "number")],
|
|
1167
|
+
evaluate: (ctx) => kth(ctx, (sorted, k) => sorted[k - 1])
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
name: "RANK",
|
|
1171
|
+
category: "statistical",
|
|
1172
|
+
description: "Returns the rank of a number in a list. Equal numbers share a rank.",
|
|
1173
|
+
args: [
|
|
1174
|
+
arg("number", "The number to rank.", "number"),
|
|
1175
|
+
arg("ref", "The list of numbers to rank against.", "range"),
|
|
1176
|
+
optional("order", "0 or omitted ranks the largest as 1; any other value ranks the smallest as 1.", "number")
|
|
1177
|
+
],
|
|
1178
|
+
evaluate: (ctx) => {
|
|
1179
|
+
const num = ctx.number(0);
|
|
1180
|
+
if (failed(num)) return num;
|
|
1181
|
+
const nums = ctx.numbers(1, 2);
|
|
1182
|
+
if (failed(nums)) return nums;
|
|
1183
|
+
const order = ctx.number(2, 0);
|
|
1184
|
+
if (failed(order)) return order;
|
|
1185
|
+
if (!nums.includes(num)) return ctx.argError(0, "#N/A", `(${num}) must be one of the numbers in argument 2.`);
|
|
1186
|
+
let rank = 1;
|
|
1187
|
+
for (const other of nums) {
|
|
1188
|
+
if (order === 0 ? other > num : other < num) rank++;
|
|
1189
|
+
}
|
|
1190
|
+
return rank;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
];
|
|
1194
|
+
|
|
1195
|
+
// src/spreadsheet/formula/functions/conditional.ts
|
|
1196
|
+
var CRITERIA_DESCRIPTION = 'The condition, e.g. 5, ">5", "<>done", "a*" (wildcards * and ?), or "" for blank cells.';
|
|
1197
|
+
function matchMask(ctx, base, baseIndex, firstPair) {
|
|
1198
|
+
const mask = new Array(base.rows * base.cols).fill(true);
|
|
1199
|
+
for (let i = firstPair; i + 1 < ctx.args.length; i += 2) {
|
|
1200
|
+
const range = ctx.range(i);
|
|
1201
|
+
if (failed(range)) return range;
|
|
1202
|
+
if (i !== baseIndex && (range.rows !== base.rows || range.cols !== base.cols)) {
|
|
1203
|
+
return ctx.argError(i, "#VALUE!", `must be the same size as argument ${baseIndex + 1} (${base.rows}\xD7${base.cols}).`);
|
|
1204
|
+
}
|
|
1205
|
+
const criterion = ctx.scalar(i + 1);
|
|
1206
|
+
if (isFormulaError(criterion)) return criterion;
|
|
1207
|
+
const matches = parseCriteria(criterion);
|
|
1208
|
+
for (let k = 0; k < mask.length; k++) {
|
|
1209
|
+
if (mask[k] && !matches(range.values[k])) mask[k] = false;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return mask;
|
|
1213
|
+
}
|
|
1214
|
+
function matchedNumbers(target, base, mask) {
|
|
1215
|
+
const nums = [];
|
|
1216
|
+
for (let c = 0; c < base.cols; c++) {
|
|
1217
|
+
for (let r = 0; r < base.rows; r++) {
|
|
1218
|
+
if (!mask[c * base.rows + r]) continue;
|
|
1219
|
+
const value = rangeGet(target, r, c);
|
|
1220
|
+
if (isFormulaError(value)) return value;
|
|
1221
|
+
const num = numericValue(value);
|
|
1222
|
+
if (num !== null) nums.push(num);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return nums;
|
|
1226
|
+
}
|
|
1227
|
+
var sum = (nums) => {
|
|
1228
|
+
let total = 0;
|
|
1229
|
+
for (const num of nums) total += num;
|
|
1230
|
+
return total;
|
|
1231
|
+
};
|
|
1232
|
+
var average = (nums, ctx) => nums.length === 0 ? ctx.error("#DIV/0!", `${ctx.name} found no matching numbers to average.`) : sum(nums, ctx) / nums.length;
|
|
1233
|
+
var max = (nums) => nums.reduce((a, b) => Math.max(a, b), nums.length ? -Infinity : 0);
|
|
1234
|
+
var min = (nums) => nums.reduce((a, b) => Math.min(a, b), nums.length ? Infinity : 0);
|
|
1235
|
+
function singleCriterion(aggregate) {
|
|
1236
|
+
return (ctx) => {
|
|
1237
|
+
const base = ctx.range(0);
|
|
1238
|
+
if (failed(base)) return base;
|
|
1239
|
+
const mask = matchMask(ctx, base, 0, 0);
|
|
1240
|
+
if (failed(mask)) return mask;
|
|
1241
|
+
let target = base;
|
|
1242
|
+
if (ctx.has(2)) {
|
|
1243
|
+
const range = ctx.range(2);
|
|
1244
|
+
if (failed(range)) return range;
|
|
1245
|
+
target = range;
|
|
1246
|
+
}
|
|
1247
|
+
const nums = matchedNumbers(target, base, mask);
|
|
1248
|
+
return failed(nums) ? nums : aggregate(nums, ctx);
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function multipleCriteria(aggregate) {
|
|
1252
|
+
return (ctx) => {
|
|
1253
|
+
const target = ctx.range(0);
|
|
1254
|
+
if (failed(target)) return target;
|
|
1255
|
+
const mask = matchMask(ctx, target, 0, 1);
|
|
1256
|
+
if (failed(mask)) return mask;
|
|
1257
|
+
const nums = matchedNumbers(target, target, mask);
|
|
1258
|
+
return failed(nums) ? nums : aggregate(nums, ctx);
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
var criteriaPair = [
|
|
1262
|
+
repeating("criteria_range", "A range to test. Must be the same size as the other ranges.", "range"),
|
|
1263
|
+
repeating("criteria", CRITERIA_DESCRIPTION, "criteria")
|
|
1264
|
+
];
|
|
1265
|
+
var CONDITIONAL_FUNCTIONS = [
|
|
1266
|
+
{
|
|
1267
|
+
name: "SUMIF",
|
|
1268
|
+
category: "math",
|
|
1269
|
+
description: "Adds the cells that meet a condition.",
|
|
1270
|
+
args: [
|
|
1271
|
+
arg("range", "The range to test.", "range"),
|
|
1272
|
+
arg("criteria", CRITERIA_DESCRIPTION, "criteria"),
|
|
1273
|
+
optional("sum_range", "The cells to add (default: range itself).", "range")
|
|
1274
|
+
],
|
|
1275
|
+
evaluate: singleCriterion(sum)
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
name: "SUMIFS",
|
|
1279
|
+
category: "math",
|
|
1280
|
+
description: "Adds the cells that meet every condition.",
|
|
1281
|
+
args: [arg("sum_range", "The cells to add.", "range"), ...criteriaPair],
|
|
1282
|
+
evaluate: multipleCriteria(sum)
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
name: "COUNTIF",
|
|
1286
|
+
category: "statistical",
|
|
1287
|
+
description: "Counts the cells that meet a condition.",
|
|
1288
|
+
args: [arg("range", "The range to test.", "range"), arg("criteria", CRITERIA_DESCRIPTION, "criteria")],
|
|
1289
|
+
evaluate: (ctx) => {
|
|
1290
|
+
const base = ctx.range(0);
|
|
1291
|
+
if (failed(base)) return base;
|
|
1292
|
+
const mask = matchMask(ctx, base, 0, 0);
|
|
1293
|
+
return failed(mask) ? mask : mask.filter(Boolean).length;
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
name: "COUNTIFS",
|
|
1298
|
+
category: "statistical",
|
|
1299
|
+
description: "Counts the cells that meet every condition.",
|
|
1300
|
+
args: criteriaPair,
|
|
1301
|
+
evaluate: (ctx) => {
|
|
1302
|
+
const base = ctx.range(0);
|
|
1303
|
+
if (failed(base)) return base;
|
|
1304
|
+
const mask = matchMask(ctx, base, 0, 0);
|
|
1305
|
+
return failed(mask) ? mask : mask.filter(Boolean).length;
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
name: "AVERAGEIF",
|
|
1310
|
+
category: "statistical",
|
|
1311
|
+
description: "Averages the cells that meet a condition.",
|
|
1312
|
+
args: [
|
|
1313
|
+
arg("range", "The range to test.", "range"),
|
|
1314
|
+
arg("criteria", CRITERIA_DESCRIPTION, "criteria"),
|
|
1315
|
+
optional("average_range", "The cells to average (default: range itself).", "range")
|
|
1316
|
+
],
|
|
1317
|
+
evaluate: singleCriterion(average)
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
name: "AVERAGEIFS",
|
|
1321
|
+
category: "statistical",
|
|
1322
|
+
description: "Averages the cells that meet every condition.",
|
|
1323
|
+
args: [arg("average_range", "The cells to average.", "range"), ...criteriaPair],
|
|
1324
|
+
evaluate: multipleCriteria(average)
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
name: "MAXIFS",
|
|
1328
|
+
category: "statistical",
|
|
1329
|
+
description: "Returns the largest number among the cells that meet every condition (0 if none match).",
|
|
1330
|
+
args: [arg("max_range", "The cells to take the maximum of.", "range"), ...criteriaPair],
|
|
1331
|
+
evaluate: multipleCriteria(max)
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: "MINIFS",
|
|
1335
|
+
category: "statistical",
|
|
1336
|
+
description: "Returns the smallest number among the cells that meet every condition (0 if none match).",
|
|
1337
|
+
args: [arg("min_range", "The cells to take the minimum of.", "range"), ...criteriaPair],
|
|
1338
|
+
evaluate: multipleCriteria(min)
|
|
1339
|
+
}
|
|
1340
|
+
];
|
|
1341
|
+
|
|
1342
|
+
// src/spreadsheet/formula/functions/logical.ts
|
|
1343
|
+
function logicalValues(ctx) {
|
|
1344
|
+
const values = [];
|
|
1345
|
+
for (let i = 0; i < ctx.args.length; i++) {
|
|
1346
|
+
const value = ctx.value(i);
|
|
1347
|
+
if (Array.isArray(value)) {
|
|
1348
|
+
for (const item of value) {
|
|
1349
|
+
if (isFormulaError(item)) return item;
|
|
1350
|
+
if (isBlank(item)) continue;
|
|
1351
|
+
if (typeof item === "boolean") values.push(item);
|
|
1352
|
+
else if (typeof item === "number") values.push(item !== 0);
|
|
1353
|
+
else {
|
|
1354
|
+
const num = numericValue(item);
|
|
1355
|
+
const bool2 = num !== null ? num !== 0 : toBoolean(item);
|
|
1356
|
+
if (typeof bool2 === "boolean") values.push(bool2);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const bool = ctx.boolean(i);
|
|
1362
|
+
if (failed(bool)) return bool;
|
|
1363
|
+
values.push(bool);
|
|
1364
|
+
}
|
|
1365
|
+
if (values.length === 0) return ctx.error("#VALUE!", `${ctx.name} found no logical values to test.`);
|
|
1366
|
+
return values;
|
|
1367
|
+
}
|
|
1368
|
+
var LOGICALS = repeating("logical", "A condition, logical value or range of logical values.", "logical");
|
|
1369
|
+
var LOGICAL_FUNCTIONS = [
|
|
1370
|
+
{
|
|
1371
|
+
name: "IF",
|
|
1372
|
+
category: "logical",
|
|
1373
|
+
description: "Returns one value if a condition is true and another if it is false.",
|
|
1374
|
+
args: [
|
|
1375
|
+
arg("logical_test", "The condition to test.", "logical"),
|
|
1376
|
+
arg("value_if_true", "The result when the condition is TRUE."),
|
|
1377
|
+
optional("value_if_false", "The result when the condition is FALSE (default FALSE).")
|
|
1378
|
+
],
|
|
1379
|
+
evaluate: (ctx) => {
|
|
1380
|
+
const condition = ctx.boolean(0);
|
|
1381
|
+
if (failed(condition)) return condition;
|
|
1382
|
+
if (condition) return ctx.value(1);
|
|
1383
|
+
return ctx.has(2) ? ctx.value(2) : false;
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
name: "IFERROR",
|
|
1388
|
+
category: "logical",
|
|
1389
|
+
description: "Returns a value, or an alternative if the value is an error.",
|
|
1390
|
+
args: [arg("value", "The value to check."), arg("value_if_error", "The result when value is an error.")],
|
|
1391
|
+
evaluate: (ctx) => {
|
|
1392
|
+
const value = ctx.value(0);
|
|
1393
|
+
return isFormulaError(value) ? ctx.value(1) : value;
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
name: "IFNA",
|
|
1398
|
+
category: "logical",
|
|
1399
|
+
description: "Returns a value, or an alternative if the value is #N/A.",
|
|
1400
|
+
args: [arg("value", "The value to check."), arg("value_if_na", "The result when value is #N/A.")],
|
|
1401
|
+
evaluate: (ctx) => {
|
|
1402
|
+
const value = ctx.value(0);
|
|
1403
|
+
return value === "#N/A" ? ctx.value(1) : value;
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
name: "IFS",
|
|
1408
|
+
category: "logical",
|
|
1409
|
+
description: "Returns the value for the first condition that is TRUE.",
|
|
1410
|
+
args: [
|
|
1411
|
+
repeating("logical_test", "A condition to test.", "logical"),
|
|
1412
|
+
repeating("value_if_true", "The result when this condition is the first TRUE one.")
|
|
1413
|
+
],
|
|
1414
|
+
evaluate: (ctx) => {
|
|
1415
|
+
for (let i = 0; i + 1 < ctx.args.length; i += 2) {
|
|
1416
|
+
const condition = ctx.boolean(i);
|
|
1417
|
+
if (failed(condition)) return condition;
|
|
1418
|
+
if (condition) return ctx.value(i + 1);
|
|
1419
|
+
}
|
|
1420
|
+
return ctx.error("#N/A", "None of the conditions in IFS were TRUE.");
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
name: "SWITCH",
|
|
1425
|
+
category: "logical",
|
|
1426
|
+
description: "Compares an expression with a list of values and returns the result for the first match. An extra last argument is the default.",
|
|
1427
|
+
args: [
|
|
1428
|
+
arg("expression", "The value to compare."),
|
|
1429
|
+
repeating("value", "A value to compare the expression with."),
|
|
1430
|
+
repeating("result", "The result when the expression equals this value.")
|
|
1431
|
+
],
|
|
1432
|
+
allowTrailingArgument: true,
|
|
1433
|
+
evaluate: (ctx) => {
|
|
1434
|
+
const expression = ctx.scalar(0);
|
|
1435
|
+
if (isFormulaError(expression)) return expression;
|
|
1436
|
+
const target = normalizeValue(expression);
|
|
1437
|
+
let i = 1;
|
|
1438
|
+
for (; i + 1 < ctx.args.length; i += 2) {
|
|
1439
|
+
const candidate = ctx.scalar(i);
|
|
1440
|
+
if (isFormulaError(candidate)) return candidate;
|
|
1441
|
+
if (compareValues(target, normalizeValue(candidate)) === 0) return ctx.value(i + 1);
|
|
1442
|
+
}
|
|
1443
|
+
if (i < ctx.args.length) return ctx.value(i);
|
|
1444
|
+
return ctx.error("#N/A", "SWITCH found no matching value and has no default.");
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
name: "AND",
|
|
1449
|
+
category: "logical",
|
|
1450
|
+
description: "Returns TRUE if every argument is TRUE.",
|
|
1451
|
+
args: [LOGICALS],
|
|
1452
|
+
evaluate: (ctx) => {
|
|
1453
|
+
const values = logicalValues(ctx);
|
|
1454
|
+
return failed(values) ? values : values.every(Boolean);
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
name: "OR",
|
|
1459
|
+
category: "logical",
|
|
1460
|
+
description: "Returns TRUE if any argument is TRUE.",
|
|
1461
|
+
args: [LOGICALS],
|
|
1462
|
+
evaluate: (ctx) => {
|
|
1463
|
+
const values = logicalValues(ctx);
|
|
1464
|
+
return failed(values) ? values : values.some(Boolean);
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
name: "XOR",
|
|
1469
|
+
category: "logical",
|
|
1470
|
+
description: "Returns TRUE if an odd number of arguments are TRUE.",
|
|
1471
|
+
args: [LOGICALS],
|
|
1472
|
+
evaluate: (ctx) => {
|
|
1473
|
+
const values = logicalValues(ctx);
|
|
1474
|
+
return failed(values) ? values : values.filter(Boolean).length % 2 === 1;
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
name: "NOT",
|
|
1479
|
+
category: "logical",
|
|
1480
|
+
description: "Reverses a logical value.",
|
|
1481
|
+
args: [arg("logical", "The value to reverse.", "logical")],
|
|
1482
|
+
evaluate: (ctx) => {
|
|
1483
|
+
const value = ctx.boolean(0);
|
|
1484
|
+
return failed(value) ? value : !value;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
];
|
|
1488
|
+
|
|
1489
|
+
// src/spreadsheet/formula/functions/text.ts
|
|
1490
|
+
var TEXT = arg("text", "The text to use.", "text");
|
|
1491
|
+
function textAndCount(ctx, countIndex, fallback) {
|
|
1492
|
+
const text = ctx.text(0);
|
|
1493
|
+
if (failed(text)) return text;
|
|
1494
|
+
const count = ctx.integer(countIndex, fallback);
|
|
1495
|
+
if (failed(count)) return count;
|
|
1496
|
+
if (count < 0) return ctx.argError(countIndex, "#VALUE!", "can't be negative.");
|
|
1497
|
+
return { text, count };
|
|
1498
|
+
}
|
|
1499
|
+
function parseNumericText(text) {
|
|
1500
|
+
let trimmed = text.trim();
|
|
1501
|
+
if (trimmed === "") return null;
|
|
1502
|
+
let scale = 1;
|
|
1503
|
+
if (trimmed.endsWith("%")) {
|
|
1504
|
+
trimmed = trimmed.slice(0, -1).trim();
|
|
1505
|
+
scale = 0.01;
|
|
1506
|
+
}
|
|
1507
|
+
const negative = /^\(.*\)$/.test(trimmed);
|
|
1508
|
+
if (negative) trimmed = trimmed.slice(1, -1);
|
|
1509
|
+
const cleaned = trimmed.replace(/^([+-]?)[$€£¥]/, "$1").replace(/,(?=\d{3}(\D|$))/g, "");
|
|
1510
|
+
if (cleaned !== "" && /^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(cleaned)) {
|
|
1511
|
+
const num = Number(cleaned) * scale;
|
|
1512
|
+
return negative ? -num : num;
|
|
1513
|
+
}
|
|
1514
|
+
if (scale === 1 && !negative) {
|
|
1515
|
+
const date = /^\d{4}-\d{1,2}-\d{1,2}/.test(trimmed) ? toDate(trimmed) : null;
|
|
1516
|
+
if (date) return dateToSerial(date);
|
|
1517
|
+
}
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
function findText(ctx, caseInsensitive) {
|
|
1521
|
+
const needle = ctx.text(0);
|
|
1522
|
+
if (failed(needle)) return needle;
|
|
1523
|
+
const haystack = ctx.text(1);
|
|
1524
|
+
if (failed(haystack)) return haystack;
|
|
1525
|
+
const start = ctx.integer(2, 1);
|
|
1526
|
+
if (failed(start)) return start;
|
|
1527
|
+
if (start < 1 || start > haystack.length + 1) {
|
|
1528
|
+
return ctx.argError(2, "#VALUE!", `must be between 1 and the length of the text (${haystack.length}).`);
|
|
1529
|
+
}
|
|
1530
|
+
let index;
|
|
1531
|
+
if (caseInsensitive) {
|
|
1532
|
+
const pattern = wildcardToRegExp(needle).source.slice(1, -1);
|
|
1533
|
+
const regex = new RegExp(pattern, "i");
|
|
1534
|
+
const match = regex.exec(haystack.slice(start - 1));
|
|
1535
|
+
index = match ? match.index + start - 1 : -1;
|
|
1536
|
+
} else {
|
|
1537
|
+
index = haystack.indexOf(needle, start - 1);
|
|
1538
|
+
}
|
|
1539
|
+
if (index === -1) return ctx.error("#VALUE!", `${ctx.name} couldn't find "${needle}" in "${haystack}".`);
|
|
1540
|
+
return index + 1;
|
|
1541
|
+
}
|
|
1542
|
+
var TEXT_FUNCTIONS = [
|
|
1543
|
+
{
|
|
1544
|
+
name: "CONCAT",
|
|
1545
|
+
category: "text",
|
|
1546
|
+
description: "Joins text, numbers and the values of ranges into one piece of text.",
|
|
1547
|
+
args: [repeating("text", "A value, cell or range to join.", "text")],
|
|
1548
|
+
evaluate: (ctx) => {
|
|
1549
|
+
const items = ctx.flatValues();
|
|
1550
|
+
const error = items.find(isFormulaError);
|
|
1551
|
+
if (error) return error;
|
|
1552
|
+
return items.map(toText).join("");
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
name: "TEXTJOIN",
|
|
1557
|
+
category: "text",
|
|
1558
|
+
description: "Joins values with a delimiter, optionally skipping empty values.",
|
|
1559
|
+
args: [
|
|
1560
|
+
arg("delimiter", "Text to put between values.", "text"),
|
|
1561
|
+
arg("ignore_empty", "TRUE to skip empty values.", "logical"),
|
|
1562
|
+
repeating("text", "A value, cell or range to join.", "text")
|
|
1563
|
+
],
|
|
1564
|
+
evaluate: (ctx) => {
|
|
1565
|
+
const delimiter = ctx.text(0);
|
|
1566
|
+
if (failed(delimiter)) return delimiter;
|
|
1567
|
+
const ignoreEmpty = ctx.boolean(1);
|
|
1568
|
+
if (failed(ignoreEmpty)) return ignoreEmpty;
|
|
1569
|
+
const items = ctx.flatValues(2);
|
|
1570
|
+
const error = items.find(isFormulaError);
|
|
1571
|
+
if (error) return error;
|
|
1572
|
+
return items.map(toText).filter((text) => !ignoreEmpty || text !== "").join(delimiter);
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
name: "UPPER",
|
|
1577
|
+
category: "text",
|
|
1578
|
+
description: "Converts text to upper case.",
|
|
1579
|
+
args: [TEXT],
|
|
1580
|
+
evaluate: (ctx) => {
|
|
1581
|
+
const text = ctx.text(0);
|
|
1582
|
+
return failed(text) ? text : text.toUpperCase();
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
name: "LOWER",
|
|
1587
|
+
category: "text",
|
|
1588
|
+
description: "Converts text to lower case.",
|
|
1589
|
+
args: [TEXT],
|
|
1590
|
+
evaluate: (ctx) => {
|
|
1591
|
+
const text = ctx.text(0);
|
|
1592
|
+
return failed(text) ? text : text.toLowerCase();
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
name: "PROPER",
|
|
1597
|
+
category: "text",
|
|
1598
|
+
description: "Capitalizes the first letter of each word and lower-cases the rest.",
|
|
1599
|
+
args: [TEXT],
|
|
1600
|
+
evaluate: (ctx) => {
|
|
1601
|
+
const text = ctx.text(0);
|
|
1602
|
+
if (failed(text)) return text;
|
|
1603
|
+
return text.toLowerCase().replace(/(^|[^\p{L}])(\p{L})/gu, (_, before, letter) => before + letter.toUpperCase());
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
name: "TRIM",
|
|
1608
|
+
category: "text",
|
|
1609
|
+
description: "Removes leading and trailing spaces.",
|
|
1610
|
+
args: [TEXT],
|
|
1611
|
+
evaluate: (ctx) => {
|
|
1612
|
+
const text = ctx.text(0);
|
|
1613
|
+
return failed(text) ? text : text.trim();
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
name: "LEN",
|
|
1618
|
+
category: "text",
|
|
1619
|
+
description: "Returns the number of characters in text.",
|
|
1620
|
+
args: [TEXT],
|
|
1621
|
+
evaluate: (ctx) => {
|
|
1622
|
+
const text = ctx.text(0);
|
|
1623
|
+
return failed(text) ? text : text.length;
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "LEFT",
|
|
1628
|
+
category: "text",
|
|
1629
|
+
description: "Returns the first characters of text.",
|
|
1630
|
+
args: [TEXT, optional("num_chars", "How many characters to return (default 1).", "number")],
|
|
1631
|
+
evaluate: (ctx) => {
|
|
1632
|
+
const parts = textAndCount(ctx, 1, 1);
|
|
1633
|
+
return typeof parts === "string" ? parts : parts.text.slice(0, parts.count);
|
|
1634
|
+
}
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
name: "RIGHT",
|
|
1638
|
+
category: "text",
|
|
1639
|
+
description: "Returns the last characters of text.",
|
|
1640
|
+
args: [TEXT, optional("num_chars", "How many characters to return (default 1).", "number")],
|
|
1641
|
+
evaluate: (ctx) => {
|
|
1642
|
+
const parts = textAndCount(ctx, 1, 1);
|
|
1643
|
+
if (typeof parts === "string") return parts;
|
|
1644
|
+
return parts.count === 0 ? "" : parts.text.slice(-parts.count);
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
name: "MID",
|
|
1649
|
+
category: "text",
|
|
1650
|
+
description: "Returns characters from the middle of text, starting at a position.",
|
|
1651
|
+
args: [
|
|
1652
|
+
TEXT,
|
|
1653
|
+
arg("start_num", "Position of the first character (1 = first).", "number"),
|
|
1654
|
+
arg("num_chars", "How many characters to return.", "number")
|
|
1655
|
+
],
|
|
1656
|
+
evaluate: (ctx) => {
|
|
1657
|
+
const parts = textAndCount(ctx, 2);
|
|
1658
|
+
if (typeof parts === "string") return parts;
|
|
1659
|
+
const start = ctx.integer(1);
|
|
1660
|
+
if (failed(start)) return start;
|
|
1661
|
+
if (start < 1) return ctx.argError(1, "#VALUE!", "must be 1 or more.");
|
|
1662
|
+
return parts.text.slice(start - 1, start - 1 + parts.count);
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
name: "FIND",
|
|
1667
|
+
category: "text",
|
|
1668
|
+
description: "Returns the position of text within other text (case-sensitive, no wildcards).",
|
|
1669
|
+
args: [
|
|
1670
|
+
arg("find_text", "The text to find.", "text"),
|
|
1671
|
+
arg("within_text", "The text to search.", "text"),
|
|
1672
|
+
optional("start_num", "Position to start searching from (default 1).", "number")
|
|
1673
|
+
],
|
|
1674
|
+
evaluate: (ctx) => findText(ctx, false)
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
name: "SEARCH",
|
|
1678
|
+
category: "text",
|
|
1679
|
+
description: "Returns the position of text within other text (case-insensitive, supports * and ? wildcards).",
|
|
1680
|
+
args: [
|
|
1681
|
+
arg("find_text", "The text to find.", "text"),
|
|
1682
|
+
arg("within_text", "The text to search.", "text"),
|
|
1683
|
+
optional("start_num", "Position to start searching from (default 1).", "number")
|
|
1684
|
+
],
|
|
1685
|
+
evaluate: (ctx) => findText(ctx, true)
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: "SUBSTITUTE",
|
|
1689
|
+
category: "text",
|
|
1690
|
+
description: "Replaces occurrences of old text with new text (all of them, or only the given instance).",
|
|
1691
|
+
args: [
|
|
1692
|
+
TEXT,
|
|
1693
|
+
arg("old_text", "The text to replace.", "text"),
|
|
1694
|
+
arg("new_text", "The replacement text.", "text"),
|
|
1695
|
+
optional("instance_num", "Which occurrence to replace (default: all).", "number")
|
|
1696
|
+
],
|
|
1697
|
+
evaluate: (ctx) => {
|
|
1698
|
+
const text = ctx.text(0);
|
|
1699
|
+
if (failed(text)) return text;
|
|
1700
|
+
const oldText = ctx.text(1);
|
|
1701
|
+
if (failed(oldText)) return oldText;
|
|
1702
|
+
const newText = ctx.text(2);
|
|
1703
|
+
if (failed(newText)) return newText;
|
|
1704
|
+
if (oldText === "") return text;
|
|
1705
|
+
if (!ctx.has(3)) return text.split(oldText).join(newText);
|
|
1706
|
+
const instance = ctx.integer(3);
|
|
1707
|
+
if (failed(instance)) return instance;
|
|
1708
|
+
if (instance < 1) return ctx.argError(3, "#VALUE!", "must be 1 or more.");
|
|
1709
|
+
let index = -1;
|
|
1710
|
+
for (let n = 0; n < instance; n++) {
|
|
1711
|
+
index = text.indexOf(oldText, index + 1);
|
|
1712
|
+
if (index === -1) return text;
|
|
1713
|
+
}
|
|
1714
|
+
return text.slice(0, index) + newText + text.slice(index + oldText.length);
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
name: "REPLACE",
|
|
1719
|
+
category: "text",
|
|
1720
|
+
description: "Replaces part of text, by position, with new text.",
|
|
1721
|
+
args: [
|
|
1722
|
+
arg("old_text", "The text to change.", "text"),
|
|
1723
|
+
arg("start_num", "Position of the first character to replace (1 = first).", "number"),
|
|
1724
|
+
arg("num_chars", "How many characters to replace.", "number"),
|
|
1725
|
+
arg("new_text", "The replacement text.", "text")
|
|
1726
|
+
],
|
|
1727
|
+
evaluate: (ctx) => {
|
|
1728
|
+
const text = ctx.text(0);
|
|
1729
|
+
if (failed(text)) return text;
|
|
1730
|
+
const start = ctx.integer(1);
|
|
1731
|
+
if (failed(start)) return start;
|
|
1732
|
+
if (start < 1) return ctx.argError(1, "#VALUE!", "must be 1 or more.");
|
|
1733
|
+
const count = ctx.integer(2);
|
|
1734
|
+
if (failed(count)) return count;
|
|
1735
|
+
if (count < 0) return ctx.argError(2, "#VALUE!", "can't be negative.");
|
|
1736
|
+
const newText = ctx.text(3);
|
|
1737
|
+
if (failed(newText)) return newText;
|
|
1738
|
+
return text.slice(0, start - 1) + newText + text.slice(start - 1 + count);
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
name: "REPT",
|
|
1743
|
+
category: "text",
|
|
1744
|
+
description: "Repeats text a number of times.",
|
|
1745
|
+
args: [TEXT, arg("number_times", "How many times to repeat the text.", "number")],
|
|
1746
|
+
evaluate: (ctx) => {
|
|
1747
|
+
const parts = textAndCount(ctx, 1);
|
|
1748
|
+
if (typeof parts === "string") return parts;
|
|
1749
|
+
if (parts.text.length * parts.count > 32767) {
|
|
1750
|
+
return ctx.error("#VALUE!", "REPT would produce text longer than 32,767 characters.");
|
|
1751
|
+
}
|
|
1752
|
+
return parts.text.repeat(parts.count);
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
name: "EXACT",
|
|
1757
|
+
category: "text",
|
|
1758
|
+
description: "Returns TRUE if two pieces of text are identical, including case.",
|
|
1759
|
+
args: [arg("text1", "The first text.", "text"), arg("text2", "The second text.", "text")],
|
|
1760
|
+
evaluate: (ctx) => {
|
|
1761
|
+
const a = ctx.text(0);
|
|
1762
|
+
if (failed(a)) return a;
|
|
1763
|
+
const b = ctx.text(1);
|
|
1764
|
+
if (failed(b)) return b;
|
|
1765
|
+
return a === b;
|
|
1766
|
+
}
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
name: "VALUE",
|
|
1770
|
+
category: "text",
|
|
1771
|
+
description: 'Converts text that looks like a number (e.g. "1,234.5", "50%", "$12") or an ISO date to a number.',
|
|
1772
|
+
args: [TEXT],
|
|
1773
|
+
evaluate: (ctx) => {
|
|
1774
|
+
const value = ctx.scalar(0);
|
|
1775
|
+
if (isFormulaError(value)) return value;
|
|
1776
|
+
if (typeof value === "number") return value;
|
|
1777
|
+
if (isBlank(value)) return 0;
|
|
1778
|
+
const num = parseNumericText(toText(value));
|
|
1779
|
+
if (num === null) return ctx.argError(0, "#VALUE!", `("${toText(value)}") can't be converted to a number.`);
|
|
1780
|
+
return num;
|
|
1781
|
+
}
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
name: "TEXT",
|
|
1785
|
+
category: "text",
|
|
1786
|
+
description: 'Formats a number or date as text using a format code such as "0.00", "#,##0", "0%" or "yyyy-mm-dd".',
|
|
1787
|
+
args: [arg("value", "The number or date to format."), arg("format_text", "The format code.", "text")],
|
|
1788
|
+
evaluate: (ctx) => {
|
|
1789
|
+
const value = ctx.scalar(0);
|
|
1790
|
+
if (isFormulaError(value)) return value;
|
|
1791
|
+
const format = ctx.text(1);
|
|
1792
|
+
if (failed(format)) return format;
|
|
1793
|
+
const result = formatValueWithCode(value, format);
|
|
1794
|
+
return _nullishCoalesce(result, () => ( ctx.argError(0, "#VALUE!", `isn't a date, so it can't use the format "${format}".`)));
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
];
|
|
1798
|
+
|
|
1799
|
+
// src/spreadsheet/formula/functions/lookup.ts
|
|
1800
|
+
var sameType = (a, b) => typeof a === "number" === (typeof b === "number") && typeof a === "boolean" === (typeof b === "boolean");
|
|
1801
|
+
function lookupIndex(values, target, mode, reverse = false) {
|
|
1802
|
+
const wanted = normalizeValue(target);
|
|
1803
|
+
const order = values.map((_, i) => i);
|
|
1804
|
+
if (reverse) order.reverse();
|
|
1805
|
+
if (mode === "exact" || mode === "wildcard") {
|
|
1806
|
+
const regex = mode === "wildcard" && typeof wanted === "string" && hasWildcards(wanted) ? wildcardToRegExp(wanted) : null;
|
|
1807
|
+
for (const i of order) {
|
|
1808
|
+
const value = normalizeValue(values[i]);
|
|
1809
|
+
if (isFormulaError(value) || isBlank(value)) continue;
|
|
1810
|
+
if (regex ? typeof value === "string" && regex.test(value) : sameType(value, wanted) && compareValues(value, wanted) === 0) {
|
|
1811
|
+
return i;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return -1;
|
|
1815
|
+
}
|
|
1816
|
+
if (mode === "sorted-asc" || mode === "sorted-desc") {
|
|
1817
|
+
let found = -1;
|
|
1818
|
+
for (let i = 0; i < values.length; i++) {
|
|
1819
|
+
const value = normalizeValue(values[i]);
|
|
1820
|
+
if (isFormulaError(value) || isBlank(value) || !sameType(value, wanted)) continue;
|
|
1821
|
+
const cmp = compareValues(value, wanted);
|
|
1822
|
+
if (mode === "sorted-asc" ? cmp <= 0 : cmp >= 0) found = i;
|
|
1823
|
+
else break;
|
|
1824
|
+
}
|
|
1825
|
+
return found;
|
|
1826
|
+
}
|
|
1827
|
+
let best = -1;
|
|
1828
|
+
let bestValue;
|
|
1829
|
+
for (const i of order) {
|
|
1830
|
+
const value = normalizeValue(values[i]);
|
|
1831
|
+
if (isFormulaError(value) || isBlank(value) || !sameType(value, wanted)) continue;
|
|
1832
|
+
const cmp = compareValues(value, wanted);
|
|
1833
|
+
if (cmp === 0) return i;
|
|
1834
|
+
const candidate = mode === "next-smaller" ? cmp < 0 : cmp > 0;
|
|
1835
|
+
const closer = best === -1 || (mode === "next-smaller" ? compareValues(value, bestValue) > 0 : compareValues(value, bestValue) < 0);
|
|
1836
|
+
if (candidate && closer) {
|
|
1837
|
+
best = i;
|
|
1838
|
+
bestValue = value;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return best;
|
|
1842
|
+
}
|
|
1843
|
+
function vector(ctx, index) {
|
|
1844
|
+
const range = ctx.range(index);
|
|
1845
|
+
if (failed(range)) return range;
|
|
1846
|
+
if (range.rows !== 1 && range.cols !== 1) {
|
|
1847
|
+
return ctx.argError(index, "#N/A", "must be a single row or column.");
|
|
1848
|
+
}
|
|
1849
|
+
return { range, values: range.values };
|
|
1850
|
+
}
|
|
1851
|
+
var notFound = (ctx, value) => ctx.error("#N/A", `${ctx.name} couldn't find ${typeof value === "string" ? `"${value}"` : String(value)}.`);
|
|
1852
|
+
function tableLookup(ctx, vertical) {
|
|
1853
|
+
const target = ctx.scalar(0);
|
|
1854
|
+
if (isFormulaError(target)) return target;
|
|
1855
|
+
const table = ctx.range(1);
|
|
1856
|
+
if (failed(table)) return table;
|
|
1857
|
+
const index = ctx.integer(2);
|
|
1858
|
+
if (failed(index)) return index;
|
|
1859
|
+
const approximate = ctx.boolean(3, true);
|
|
1860
|
+
if (failed(approximate)) return approximate;
|
|
1861
|
+
const span = vertical ? table.cols : table.rows;
|
|
1862
|
+
if (index < 1) return ctx.argError(2, "#VALUE!", "must be 1 or more.");
|
|
1863
|
+
if (index > span) {
|
|
1864
|
+
return ctx.argError(2, "#REF!", `is larger than the number of ${vertical ? "columns" : "rows"} in the table (${span}).`);
|
|
1865
|
+
}
|
|
1866
|
+
const keys = vertical ? sliceRange(table, 0, table.rows, 0, 1) : sliceRange(table, 0, 1, 0, table.cols);
|
|
1867
|
+
const position = lookupIndex(keys, target, approximate ? "sorted-asc" : "wildcard");
|
|
1868
|
+
if (position === -1) return notFound(ctx, target);
|
|
1869
|
+
return vertical ? rangeGet(table, position, index - 1) : rangeGet(table, index - 1, position);
|
|
1870
|
+
}
|
|
1871
|
+
var LOOKUP_FUNCTIONS = [
|
|
1872
|
+
{
|
|
1873
|
+
name: "VLOOKUP",
|
|
1874
|
+
category: "lookup",
|
|
1875
|
+
description: "Looks for a value in the first column of a table and returns the value in the same row of another column.",
|
|
1876
|
+
args: [
|
|
1877
|
+
arg("lookup_value", "The value to find in the first column."),
|
|
1878
|
+
arg("table_array", "The table to search.", "range"),
|
|
1879
|
+
arg("col_index_num", "Which column to return (1 = first column).", "number"),
|
|
1880
|
+
optional("range_lookup", "FALSE for an exact match; TRUE or omitted for the closest match in a sorted first column.", "logical")
|
|
1881
|
+
],
|
|
1882
|
+
evaluate: (ctx) => tableLookup(ctx, true)
|
|
1883
|
+
},
|
|
1884
|
+
{
|
|
1885
|
+
name: "HLOOKUP",
|
|
1886
|
+
category: "lookup",
|
|
1887
|
+
description: "Looks for a value in the first row of a table and returns the value in the same column of another row.",
|
|
1888
|
+
args: [
|
|
1889
|
+
arg("lookup_value", "The value to find in the first row."),
|
|
1890
|
+
arg("table_array", "The table to search.", "range"),
|
|
1891
|
+
arg("row_index_num", "Which row to return (1 = first row).", "number"),
|
|
1892
|
+
optional("range_lookup", "FALSE for an exact match; TRUE or omitted for the closest match in a sorted first row.", "logical")
|
|
1893
|
+
],
|
|
1894
|
+
evaluate: (ctx) => tableLookup(ctx, false)
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
name: "MATCH",
|
|
1898
|
+
category: "lookup",
|
|
1899
|
+
description: "Returns the position of a value in a row or column.",
|
|
1900
|
+
args: [
|
|
1901
|
+
arg("lookup_value", "The value to find."),
|
|
1902
|
+
arg("lookup_array", "A single row or column to search.", "range"),
|
|
1903
|
+
optional("match_type", "0 = exact; 1 (default) = largest value <= lookup_value in ascending data; -1 = smallest value >= lookup_value in descending data.", "number")
|
|
1904
|
+
],
|
|
1905
|
+
evaluate: (ctx) => {
|
|
1906
|
+
const target = ctx.scalar(0);
|
|
1907
|
+
if (isFormulaError(target)) return target;
|
|
1908
|
+
const list = vector(ctx, 1);
|
|
1909
|
+
if (failed(list)) return list;
|
|
1910
|
+
const type = ctx.number(2, 1);
|
|
1911
|
+
if (failed(type)) return type;
|
|
1912
|
+
const mode = type === 0 ? "wildcard" : type > 0 ? "sorted-asc" : "sorted-desc";
|
|
1913
|
+
const position = lookupIndex(list.values, target, mode);
|
|
1914
|
+
return position === -1 ? notFound(ctx, target) : position + 1;
|
|
1915
|
+
}
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
name: "INDEX",
|
|
1919
|
+
category: "lookup",
|
|
1920
|
+
description: "Returns the value at a row and column of a range. Row or column 0 returns the whole column or row.",
|
|
1921
|
+
args: [
|
|
1922
|
+
arg("array", "The range to read from.", "range"),
|
|
1923
|
+
arg("row_num", "The row (1 = first row; 0 = every row).", "number"),
|
|
1924
|
+
optional("column_num", "The column (1 = first column; 0 = every column).", "number")
|
|
1925
|
+
],
|
|
1926
|
+
evaluate: (ctx) => {
|
|
1927
|
+
const range = ctx.range(0);
|
|
1928
|
+
if (failed(range)) return range;
|
|
1929
|
+
let row = ctx.integer(1);
|
|
1930
|
+
if (failed(row)) return row;
|
|
1931
|
+
let col = ctx.integer(2, ctx.has(2) ? void 0 : 1);
|
|
1932
|
+
if (failed(col)) return col;
|
|
1933
|
+
if (!ctx.has(2) && range.rows === 1 && range.cols > 1) {
|
|
1934
|
+
col = row;
|
|
1935
|
+
row = 1;
|
|
1936
|
+
}
|
|
1937
|
+
if (row < 0 || row > range.rows) {
|
|
1938
|
+
return ctx.argError(1, "#REF!", `is outside the range (1 to ${range.rows}).`);
|
|
1939
|
+
}
|
|
1940
|
+
if (col < 0 || col > range.cols) {
|
|
1941
|
+
return ctx.argError(2, "#REF!", `is outside the range (1 to ${range.cols}).`);
|
|
1942
|
+
}
|
|
1943
|
+
if (row === 0 && col === 0) return range.values;
|
|
1944
|
+
if (row === 0) return sliceRange(range, 0, range.rows, col - 1, 1);
|
|
1945
|
+
if (col === 0) return sliceRange(range, row - 1, 1, 0, range.cols);
|
|
1946
|
+
return rangeGet(range, row - 1, col - 1);
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
name: "XLOOKUP",
|
|
1951
|
+
category: "lookup",
|
|
1952
|
+
description: "Finds a value in one range and returns the value at the same position in another range.",
|
|
1953
|
+
args: [
|
|
1954
|
+
arg("lookup_value", "The value to find."),
|
|
1955
|
+
arg("lookup_array", "A single row or column to search.", "range"),
|
|
1956
|
+
arg("return_array", "The range to return a value from; same height (or width) as lookup_array.", "range"),
|
|
1957
|
+
optional("if_not_found", "Returned when nothing matches (default #N/A)."),
|
|
1958
|
+
optional("match_mode", "0 (default) = exact; -1 = exact or next smaller; 1 = exact or next larger; 2 = wildcard match.", "number"),
|
|
1959
|
+
optional("search_mode", "1 (default) = first to last; -1 = last to first.", "number")
|
|
1960
|
+
],
|
|
1961
|
+
evaluate: (ctx) => {
|
|
1962
|
+
const target = ctx.scalar(0);
|
|
1963
|
+
if (isFormulaError(target)) return target;
|
|
1964
|
+
const list = vector(ctx, 1);
|
|
1965
|
+
if (failed(list)) return list;
|
|
1966
|
+
const result = ctx.range(2);
|
|
1967
|
+
if (failed(result)) return result;
|
|
1968
|
+
const matchMode = ctx.integer(4, 0);
|
|
1969
|
+
if (failed(matchMode)) return matchMode;
|
|
1970
|
+
const searchMode = ctx.integer(5, 1);
|
|
1971
|
+
if (failed(searchMode)) return searchMode;
|
|
1972
|
+
const vertical = list.range.cols === 1 && list.range.rows > 1;
|
|
1973
|
+
const length = list.values.length;
|
|
1974
|
+
if ((vertical ? result.rows : result.cols) !== length && !(length === 1 && (result.rows === 1 || result.cols === 1))) {
|
|
1975
|
+
return ctx.argError(2, "#VALUE!", "must be the same height (or width) as argument 2.");
|
|
1976
|
+
}
|
|
1977
|
+
const modes = { 0: "exact", [-1]: "next-smaller", 1: "next-larger", 2: "wildcard" };
|
|
1978
|
+
const mode = modes[matchMode];
|
|
1979
|
+
if (!mode) return ctx.argError(4, "#VALUE!", "must be 0, -1, 1 or 2.");
|
|
1980
|
+
if (![1, -1, 2, -2].includes(searchMode)) return ctx.argError(5, "#VALUE!", "must be 1, -1, 2 or -2.");
|
|
1981
|
+
const position = lookupIndex(list.values, target, mode, searchMode < 0);
|
|
1982
|
+
if (position === -1) return ctx.has(3) ? ctx.value(3) : notFound(ctx, target);
|
|
1983
|
+
if (vertical) {
|
|
1984
|
+
return result.cols === 1 ? rangeGet(result, position, 0) : sliceRange(result, position, 1, 0, result.cols);
|
|
1985
|
+
}
|
|
1986
|
+
return result.rows === 1 ? rangeGet(result, 0, position) : sliceRange(result, 0, result.rows, position, 1);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
];
|
|
1990
|
+
|
|
1991
|
+
// src/spreadsheet/formula/functions/date.ts
|
|
1992
|
+
var START_DATE = arg("start_date", "The date to start from.", "date");
|
|
1993
|
+
var DATE = arg("date", 'A date (e.g. "2026-01-31" or a cell containing one).', "date");
|
|
1994
|
+
function addMonths(date, months) {
|
|
1995
|
+
const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
|
|
1996
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
1997
|
+
target.setDate(Math.min(date.getDate(), lastDay));
|
|
1998
|
+
return target;
|
|
1999
|
+
}
|
|
2000
|
+
function datePart(ctx, part) {
|
|
2001
|
+
const date = ctx.date(0);
|
|
2002
|
+
return failed(date) ? date : part(date);
|
|
2003
|
+
}
|
|
2004
|
+
var isWorkday = (date) => date.getDay() !== 0 && date.getDay() !== 6;
|
|
2005
|
+
var DATE_FUNCTIONS = [
|
|
2006
|
+
{
|
|
2007
|
+
name: "TODAY",
|
|
2008
|
+
category: "date",
|
|
2009
|
+
description: "Returns today's date (YYYY-MM-DD). Changes on every recalculation.",
|
|
2010
|
+
args: [],
|
|
2011
|
+
volatile: true,
|
|
2012
|
+
// Local calendar date (toISOString would give the UTC date).
|
|
2013
|
+
evaluate: () => formatIsoDate(/* @__PURE__ */ new Date())
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
name: "NOW",
|
|
2017
|
+
category: "date",
|
|
2018
|
+
description: "Returns the current date and time (YYYY-MM-DD HH:MM:SS). Changes on every recalculation.",
|
|
2019
|
+
args: [],
|
|
2020
|
+
volatile: true,
|
|
2021
|
+
evaluate: () => formatIsoDateTime(/* @__PURE__ */ new Date())
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
name: "DATE",
|
|
2025
|
+
category: "date",
|
|
2026
|
+
description: "Builds a date from a year, month and day. Months and days outside their range roll over.",
|
|
2027
|
+
args: [
|
|
2028
|
+
arg("year", "The year (0-1899 are added to 1900).", "number"),
|
|
2029
|
+
arg("month", "The month (1-12).", "number"),
|
|
2030
|
+
arg("day", "The day of the month.", "number")
|
|
2031
|
+
],
|
|
2032
|
+
evaluate: (ctx) => {
|
|
2033
|
+
let year = ctx.integer(0);
|
|
2034
|
+
if (failed(year)) return year;
|
|
2035
|
+
const month = ctx.integer(1);
|
|
2036
|
+
if (failed(month)) return month;
|
|
2037
|
+
const day = ctx.integer(2);
|
|
2038
|
+
if (failed(day)) return day;
|
|
2039
|
+
if (year < 0 || year > 9999) return ctx.argError(0, "#NUM!", "must be between 0 and 9999.");
|
|
2040
|
+
if (year < 1900) year += 1900;
|
|
2041
|
+
const date = new Date(year, month - 1, 1);
|
|
2042
|
+
date.setDate(day);
|
|
2043
|
+
if (date.getFullYear() < 1900 || date.getFullYear() > 9999) {
|
|
2044
|
+
return ctx.error("#NUM!", "DATE produced a date outside the years 1900-9999.");
|
|
2045
|
+
}
|
|
2046
|
+
return formatIsoDate(date);
|
|
2047
|
+
}
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
name: "YEAR",
|
|
2051
|
+
category: "date",
|
|
2052
|
+
description: "Returns the year of a date.",
|
|
2053
|
+
args: [DATE],
|
|
2054
|
+
evaluate: (ctx) => datePart(ctx, (d) => d.getFullYear())
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
name: "MONTH",
|
|
2058
|
+
category: "date",
|
|
2059
|
+
description: "Returns the month of a date (1-12).",
|
|
2060
|
+
args: [DATE],
|
|
2061
|
+
evaluate: (ctx) => datePart(ctx, (d) => d.getMonth() + 1)
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
name: "DAY",
|
|
2065
|
+
category: "date",
|
|
2066
|
+
description: "Returns the day of the month of a date (1-31).",
|
|
2067
|
+
args: [DATE],
|
|
2068
|
+
evaluate: (ctx) => datePart(ctx, (d) => d.getDate())
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
name: "WEEKDAY",
|
|
2072
|
+
category: "date",
|
|
2073
|
+
description: "Returns the day of the week of a date.",
|
|
2074
|
+
args: [DATE, optional("return_type", "1 (default) = Sunday 1 to Saturday 7; 2 = Monday 1 to Sunday 7; 3 = Monday 0 to Sunday 6.", "number")],
|
|
2075
|
+
evaluate: (ctx) => {
|
|
2076
|
+
const date = ctx.date(0);
|
|
2077
|
+
if (failed(date)) return date;
|
|
2078
|
+
const type = ctx.integer(1, 1);
|
|
2079
|
+
if (failed(type)) return type;
|
|
2080
|
+
const day = date.getDay();
|
|
2081
|
+
if (type === 1) return day + 1;
|
|
2082
|
+
if (type === 2) return day === 0 ? 7 : day;
|
|
2083
|
+
if (type === 3) return day === 0 ? 6 : day - 1;
|
|
2084
|
+
return ctx.argError(1, "#NUM!", "must be 1, 2 or 3.");
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
name: "DAYS",
|
|
2089
|
+
category: "date",
|
|
2090
|
+
description: "Returns the number of days between two dates.",
|
|
2091
|
+
args: [arg("end_date", "The later date.", "date"), START_DATE],
|
|
2092
|
+
evaluate: (ctx) => {
|
|
2093
|
+
const end = ctx.date(0);
|
|
2094
|
+
if (failed(end)) return end;
|
|
2095
|
+
const start = ctx.date(1);
|
|
2096
|
+
if (failed(start)) return start;
|
|
2097
|
+
return daysBetween(start, end);
|
|
2098
|
+
}
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
name: "EDATE",
|
|
2102
|
+
category: "date",
|
|
2103
|
+
description: "Returns the date a number of months before or after a date.",
|
|
2104
|
+
args: [START_DATE, arg("months", "Months to add (negative for earlier dates).", "number")],
|
|
2105
|
+
evaluate: (ctx) => {
|
|
2106
|
+
const start = ctx.date(0);
|
|
2107
|
+
if (failed(start)) return start;
|
|
2108
|
+
const months = ctx.integer(1);
|
|
2109
|
+
if (failed(months)) return months;
|
|
2110
|
+
return formatIsoDate(addMonths(start, months));
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
name: "EOMONTH",
|
|
2115
|
+
category: "date",
|
|
2116
|
+
description: "Returns the last day of the month a number of months before or after a date.",
|
|
2117
|
+
args: [START_DATE, arg("months", "Months to add (0 = same month).", "number")],
|
|
2118
|
+
evaluate: (ctx) => {
|
|
2119
|
+
const start = ctx.date(0);
|
|
2120
|
+
if (failed(start)) return start;
|
|
2121
|
+
const months = ctx.integer(1);
|
|
2122
|
+
if (failed(months)) return months;
|
|
2123
|
+
return formatIsoDate(new Date(start.getFullYear(), start.getMonth() + months + 1, 0));
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
name: "NETWORKDAYS",
|
|
2128
|
+
category: "date",
|
|
2129
|
+
description: "Counts the working days (Monday to Friday) between two dates, inclusive, minus holidays.",
|
|
2130
|
+
args: [START_DATE, arg("end_date", "The last date.", "date"), optional("holidays", "Dates to exclude.", "range")],
|
|
2131
|
+
evaluate: (ctx) => {
|
|
2132
|
+
const start = ctx.date(0);
|
|
2133
|
+
if (failed(start)) return start;
|
|
2134
|
+
const end = ctx.date(1);
|
|
2135
|
+
if (failed(end)) return end;
|
|
2136
|
+
const holidays = /* @__PURE__ */ new Set();
|
|
2137
|
+
if (ctx.has(2)) {
|
|
2138
|
+
const raw = ctx.value(2);
|
|
2139
|
+
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
2140
|
+
if (isFormulaError(value)) return value;
|
|
2141
|
+
if (isBlank(value)) continue;
|
|
2142
|
+
const date = toDate(value);
|
|
2143
|
+
if (!date) return ctx.argError(2, "#VALUE!", "must contain only dates.");
|
|
2144
|
+
holidays.add(formatIsoDate(date));
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
const sign = daysBetween(start, end) < 0 ? -1 : 1;
|
|
2148
|
+
const [from, to] = sign < 0 ? [end, start] : [start, end];
|
|
2149
|
+
let count = 0;
|
|
2150
|
+
const total = daysBetween(from, to);
|
|
2151
|
+
for (let i = 0; i <= total; i++) {
|
|
2152
|
+
const day = new Date(from.getFullYear(), from.getMonth(), from.getDate() + i);
|
|
2153
|
+
if (isWorkday(day) && !holidays.has(formatIsoDate(day))) count++;
|
|
2154
|
+
}
|
|
2155
|
+
return sign * count;
|
|
2156
|
+
}
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
name: "DATEDIF",
|
|
2160
|
+
category: "date",
|
|
2161
|
+
description: "Returns the difference between two dates in years, months or days.",
|
|
2162
|
+
args: [
|
|
2163
|
+
START_DATE,
|
|
2164
|
+
arg("end_date", "The later date.", "date"),
|
|
2165
|
+
arg("unit", '"Y" years, "M" months, "D" days, "MD" days ignoring months and years, "YM" months ignoring years, "YD" days ignoring years.', "text")
|
|
2166
|
+
],
|
|
2167
|
+
evaluate: (ctx) => {
|
|
2168
|
+
const start = ctx.date(0);
|
|
2169
|
+
if (failed(start)) return start;
|
|
2170
|
+
const end = ctx.date(1);
|
|
2171
|
+
if (failed(end)) return end;
|
|
2172
|
+
const unit = ctx.text(2);
|
|
2173
|
+
if (failed(unit)) return unit;
|
|
2174
|
+
if (daysBetween(start, end) < 0) return ctx.error("#NUM!", "DATEDIF needs the start date to be on or before the end date.");
|
|
2175
|
+
let months = (end.getFullYear() - start.getFullYear()) * 12 + end.getMonth() - start.getMonth();
|
|
2176
|
+
if (end.getDate() < start.getDate()) months--;
|
|
2177
|
+
switch (unit.trim().toUpperCase()) {
|
|
2178
|
+
case "Y":
|
|
2179
|
+
return Math.floor(months / 12);
|
|
2180
|
+
case "M":
|
|
2181
|
+
return months;
|
|
2182
|
+
case "D":
|
|
2183
|
+
return daysBetween(start, end);
|
|
2184
|
+
case "YM":
|
|
2185
|
+
return months % 12;
|
|
2186
|
+
case "MD": {
|
|
2187
|
+
const anchor = addMonths(start, months);
|
|
2188
|
+
return daysBetween(anchor, end);
|
|
2189
|
+
}
|
|
2190
|
+
case "YD": {
|
|
2191
|
+
const anchor = addMonths(start, Math.floor(months / 12) * 12);
|
|
2192
|
+
return daysBetween(anchor, end);
|
|
2193
|
+
}
|
|
2194
|
+
default:
|
|
2195
|
+
return ctx.argError(2, "#NUM!", 'must be "Y", "M", "D", "MD", "YM" or "YD".');
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
];
|
|
2200
|
+
|
|
2201
|
+
// src/spreadsheet/formula/functions/information.ts
|
|
2202
|
+
var VALUE = arg("value", "The value or cell to check.");
|
|
2203
|
+
var check = (test) => (ctx) => test(ctx.scalar(0));
|
|
2204
|
+
var INFORMATION_FUNCTIONS = [
|
|
2205
|
+
{
|
|
2206
|
+
name: "ISBLANK",
|
|
2207
|
+
category: "information",
|
|
2208
|
+
description: "Returns TRUE if the value is empty.",
|
|
2209
|
+
args: [VALUE],
|
|
2210
|
+
evaluate: check(isBlank)
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
name: "ISNUMBER",
|
|
2214
|
+
category: "information",
|
|
2215
|
+
description: "Returns TRUE if the value is a number (typed cell values that look like numbers count).",
|
|
2216
|
+
args: [VALUE],
|
|
2217
|
+
evaluate: check((value) => numericValue(value) !== null)
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
name: "ISTEXT",
|
|
2221
|
+
category: "information",
|
|
2222
|
+
description: "Returns TRUE if the value is text that is not a number.",
|
|
2223
|
+
args: [VALUE],
|
|
2224
|
+
evaluate: check(
|
|
2225
|
+
(value) => typeof value === "string" && !isBlank(value) && !isFormulaError(value) && numericValue(value) === null
|
|
2226
|
+
)
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
name: "ISERROR",
|
|
2230
|
+
category: "information",
|
|
2231
|
+
description: "Returns TRUE if the value is any error.",
|
|
2232
|
+
args: [VALUE],
|
|
2233
|
+
evaluate: check(isFormulaError)
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
name: "ISNA",
|
|
2237
|
+
category: "information",
|
|
2238
|
+
description: "Returns TRUE if the value is the #N/A error.",
|
|
2239
|
+
args: [VALUE],
|
|
2240
|
+
evaluate: check((value) => value === "#N/A")
|
|
2241
|
+
}
|
|
2242
|
+
];
|
|
2243
|
+
|
|
2244
|
+
// src/spreadsheet/formula/functions/index.ts
|
|
2245
|
+
var BUILTIN_FORMULA_FUNCTIONS = [
|
|
2246
|
+
...MATH_FUNCTIONS,
|
|
2247
|
+
...STATISTICAL_FUNCTIONS,
|
|
2248
|
+
...CONDITIONAL_FUNCTIONS,
|
|
2249
|
+
...LOGICAL_FUNCTIONS,
|
|
2250
|
+
...TEXT_FUNCTIONS,
|
|
2251
|
+
...LOOKUP_FUNCTIONS,
|
|
2252
|
+
...DATE_FUNCTIONS,
|
|
2253
|
+
...INFORMATION_FUNCTIONS
|
|
2254
|
+
];
|
|
2255
|
+
|
|
2256
|
+
// src/spreadsheet/formula/registry.ts
|
|
2257
|
+
var registry = /* @__PURE__ */ new Map();
|
|
2258
|
+
var builtinsLoaded = false;
|
|
2259
|
+
function ensureBuiltins() {
|
|
2260
|
+
if (builtinsLoaded) return;
|
|
2261
|
+
builtinsLoaded = true;
|
|
2262
|
+
for (const def of BUILTIN_FORMULA_FUNCTIONS) {
|
|
2263
|
+
if (!registry.has(def.name)) registry.set(def.name, def);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
function registerFormulaFunction(definition) {
|
|
2267
|
+
ensureBuiltins();
|
|
2268
|
+
const name = definition.name.trim().toUpperCase();
|
|
2269
|
+
if (!/^[A-Z_][A-Z0-9_.]*$/.test(name)) {
|
|
2270
|
+
throw new Error(`Invalid formula function name "${definition.name}"`);
|
|
2271
|
+
}
|
|
2272
|
+
registry.set(name, { ...definition, name });
|
|
2273
|
+
}
|
|
2274
|
+
function unregisterFormulaFunction(name) {
|
|
2275
|
+
ensureBuiltins();
|
|
2276
|
+
return registry.delete(name.toUpperCase());
|
|
2277
|
+
}
|
|
2278
|
+
function getFormulaFunction(name) {
|
|
2279
|
+
ensureBuiltins();
|
|
2280
|
+
return registry.get(name.toUpperCase());
|
|
2281
|
+
}
|
|
2282
|
+
function listFormulaFunctions(category) {
|
|
2283
|
+
ensureBuiltins();
|
|
2284
|
+
return Array.from(registry.values()).filter((def) => !category || def.category === category).sort((a, b) => a.name.localeCompare(b.name));
|
|
2285
|
+
}
|
|
2286
|
+
function searchFormulaFunctions(query, options = {}) {
|
|
2287
|
+
const q = query.trim().toUpperCase();
|
|
2288
|
+
const all = listFormulaFunctions(options.category);
|
|
2289
|
+
const prefix = all.filter((def) => def.name.startsWith(q));
|
|
2290
|
+
const contains = q ? all.filter((def) => !def.name.startsWith(q) && def.name.includes(q)) : [];
|
|
2291
|
+
const results = [...prefix, ...contains];
|
|
2292
|
+
return options.limit !== void 0 ? results.slice(0, options.limit) : results;
|
|
2293
|
+
}
|
|
2294
|
+
function layoutOf(def) {
|
|
2295
|
+
const groupStart = def.args.findIndex((arg2) => arg2.repeating);
|
|
2296
|
+
if (groupStart === -1) return { fixed: def.args, group: [], groupStart: def.args.length };
|
|
2297
|
+
return { fixed: def.args.slice(0, groupStart), group: def.args.slice(groupStart), groupStart };
|
|
2298
|
+
}
|
|
2299
|
+
function getFormulaArgumentSpecIndex(def, argIndex) {
|
|
2300
|
+
const { fixed, group, groupStart } = layoutOf(def);
|
|
2301
|
+
if (argIndex < fixed.length) return argIndex;
|
|
2302
|
+
if (group.length === 0) return -1;
|
|
2303
|
+
return groupStart + (argIndex - groupStart) % group.length;
|
|
2304
|
+
}
|
|
2305
|
+
function checkFormulaArgumentCount(def, count) {
|
|
2306
|
+
const { fixed, group } = layoutOf(def);
|
|
2307
|
+
const requiredFixed = fixed.filter((arg2) => !arg2.optional).length;
|
|
2308
|
+
const groupRequired = group.length > 0 && !group[0].optional;
|
|
2309
|
+
const min2 = requiredFixed + (groupRequired ? group.length : 0);
|
|
2310
|
+
const max2 = group.length > 0 ? Infinity : fixed.length;
|
|
2311
|
+
const given = `${count} ${count === 1 ? "was" : "were"} given`;
|
|
2312
|
+
const plural = (n) => `${n} argument${n === 1 ? "" : "s"}`;
|
|
2313
|
+
if (count < min2) {
|
|
2314
|
+
const expected = min2 === max2 ? plural(min2) : `at least ${plural(min2)}`;
|
|
2315
|
+
return `${def.name} needs ${expected}, but ${given}.`;
|
|
2316
|
+
}
|
|
2317
|
+
if (count > max2) {
|
|
2318
|
+
const expected = min2 === max2 ? plural(max2) : `at most ${plural(max2)}`;
|
|
2319
|
+
return max2 === 0 ? `${def.name} takes no arguments, but ${given}.` : `${def.name} takes ${expected}, but ${given}.`;
|
|
2320
|
+
}
|
|
2321
|
+
if (group.length > 1 && count > fixed.length) {
|
|
2322
|
+
const extra = (count - fixed.length) % group.length;
|
|
2323
|
+
if (extra !== 0 && !(def.allowTrailingArgument && extra === 1)) {
|
|
2324
|
+
const names = group.map((arg2) => arg2.name).join(", ");
|
|
2325
|
+
return `${def.name} expects its repeating arguments (${names}) in complete groups.`;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
function getFormulaSignatureParts(nameOrDef, activeArgIndex) {
|
|
2331
|
+
const def = typeof nameOrDef === "string" ? getFormulaFunction(nameOrDef) : nameOrDef;
|
|
2332
|
+
if (!def) return [];
|
|
2333
|
+
const { fixed, group, groupStart } = layoutOf(def);
|
|
2334
|
+
let activeSpec = -1;
|
|
2335
|
+
let activeRepeat = 0;
|
|
2336
|
+
if (activeArgIndex !== void 0 && activeArgIndex !== null && activeArgIndex >= 0) {
|
|
2337
|
+
activeSpec = getFormulaArgumentSpecIndex(def, activeArgIndex);
|
|
2338
|
+
if (activeSpec >= groupStart && group.length > 0) {
|
|
2339
|
+
activeRepeat = Math.floor((activeArgIndex - groupStart) / group.length) > 0 ? 1 : 0;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const parts = [{ text: `${def.name}(`, specIndex: null, active: false }];
|
|
2343
|
+
const pushArg = (text, specIndex, repeat = 0) => {
|
|
2344
|
+
if (parts.length > 1) parts.push({ text: ", ", specIndex: null, active: false });
|
|
2345
|
+
parts.push({ text, specIndex, active: specIndex === activeSpec && repeat === activeRepeat });
|
|
2346
|
+
};
|
|
2347
|
+
fixed.forEach((arg2, i) => pushArg(arg2.optional ? `[${arg2.name}]` : arg2.name, i));
|
|
2348
|
+
if (group.length > 0) {
|
|
2349
|
+
const groupOptional = !!group[0].optional;
|
|
2350
|
+
group.forEach((arg2, i) => {
|
|
2351
|
+
const open = groupOptional && i === 0 ? "[" : "";
|
|
2352
|
+
pushArg(`${open}${arg2.name}1`, groupStart + i, 0);
|
|
2353
|
+
});
|
|
2354
|
+
group.forEach((arg2, i) => {
|
|
2355
|
+
const open = !groupOptional && i === 0 ? "[" : "";
|
|
2356
|
+
pushArg(`${open}${arg2.name}2`, groupStart + i, 1);
|
|
2357
|
+
});
|
|
2358
|
+
parts.push({ text: ", ...]", specIndex: null, active: false });
|
|
2359
|
+
}
|
|
2360
|
+
parts.push({ text: ")", specIndex: null, active: false });
|
|
2361
|
+
return parts;
|
|
2362
|
+
}
|
|
2363
|
+
function getFormulaSignature(nameOrDef) {
|
|
2364
|
+
return getFormulaSignatureParts(nameOrDef).map((part) => part.text).join("");
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/spreadsheet/formula/evaluator.ts
|
|
2368
|
+
var DEFAULT_MESSAGES = {
|
|
2369
|
+
"#DIV/0!": "The formula divides by zero.",
|
|
2370
|
+
"#VALUE!": "A value in the formula has the wrong type.",
|
|
2371
|
+
"#REF!": "The formula refers to a cell or range that was deleted.",
|
|
2372
|
+
"#NAME?": "The formula contains an unknown name.",
|
|
2373
|
+
"#N/A": "A value is not available.",
|
|
2374
|
+
"#NUM!": "The formula produced an invalid number.",
|
|
2375
|
+
"#CYCLE!": "This formula is part of a circular reference.",
|
|
2376
|
+
"#ERROR!": "The formula could not be evaluated."
|
|
2377
|
+
};
|
|
2378
|
+
function describeFormulaError(code) {
|
|
2379
|
+
return _nullishCoalesce(DEFAULT_MESSAGES[code], () => ( "The formula could not be evaluated."));
|
|
2380
|
+
}
|
|
2381
|
+
var quote = (value) => typeof value === "string" ? `"${value}"` : String(value);
|
|
2382
|
+
var FormulaEvaluator = (_class2 = class {
|
|
2383
|
+
|
|
2384
|
+
__init3() {this.diagnostics = []}
|
|
2385
|
+
constructor(context = {}) {;_class2.prototype.__init3.call(this);
|
|
2386
|
+
this.context = context;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Evaluates a formula. Errors in the formula are returned as error values
|
|
2390
|
+
* (`#VALUE!` etc.); malformed formulas and missing resolvers throw.
|
|
2391
|
+
*/
|
|
2392
|
+
evaluate(astOrFormula) {
|
|
2393
|
+
const ast = typeof astOrFormula === "string" ? parseFormula(astOrFormula) : astOrFormula;
|
|
2394
|
+
return this.evaluateNode(ast);
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Evaluates a formula and explains errors. Never throws: malformed formulas
|
|
2398
|
+
* return `#ERROR!` with the parser's message. A result that is a whole range
|
|
2399
|
+
* (no dynamic-array spilling) is reduced to its first value.
|
|
2400
|
+
*/
|
|
2401
|
+
evaluateWithDiagnostics(astOrFormula) {
|
|
2402
|
+
const saved = this.diagnostics;
|
|
2403
|
+
this.diagnostics = [];
|
|
2404
|
+
try {
|
|
2405
|
+
let value = this.evaluate(astOrFormula);
|
|
2406
|
+
if (Array.isArray(value)) value = value.length > 0 ? value[0] : "";
|
|
2407
|
+
if (isFormulaError(value)) {
|
|
2408
|
+
const code = value;
|
|
2409
|
+
const diagnostic = this.diagnostics.find((d) => d.code === code);
|
|
2410
|
+
return { value, error: code, message: _nullishCoalesce(_optionalChain([diagnostic, 'optionalAccess', _10 => _10.message]), () => ( describeFormulaError(code))) };
|
|
2411
|
+
}
|
|
2412
|
+
return { value };
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
return { value: "#ERROR!", error: "#ERROR!", message: _optionalChain([err, 'optionalAccess', _11 => _11.message]) || "Formula error" };
|
|
2415
|
+
} finally {
|
|
2416
|
+
this.diagnostics = saved;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/** Records why an error happened and returns the error code. */
|
|
2420
|
+
fail(code, message) {
|
|
2421
|
+
this.diagnostics.push({ code, message });
|
|
2422
|
+
return code;
|
|
2423
|
+
}
|
|
2424
|
+
evaluateNode(node) {
|
|
2425
|
+
switch (node.type) {
|
|
2426
|
+
case "Literal":
|
|
2427
|
+
if (node.value === "#REF!") return this.fail("#REF!", "This formula refers to a cell or range that was deleted.");
|
|
2428
|
+
if (node.value === "#NAME?") return this.fail("#NAME?", "This formula contains an unknown name.");
|
|
2429
|
+
return node.value;
|
|
2430
|
+
case "CellRef": {
|
|
2431
|
+
if (!this.context.resolveCell) {
|
|
2432
|
+
throw new Error(`Cannot resolve cell reference ${node.ref}: no cell resolver provided`);
|
|
2433
|
+
}
|
|
2434
|
+
const value = this.context.resolveCell(node.col, node.row);
|
|
2435
|
+
if (isFormulaError(value)) {
|
|
2436
|
+
this.fail(
|
|
2437
|
+
value,
|
|
2438
|
+
value === "#CYCLE!" ? `Cell ${node.ref} is part of a circular reference.` : `Cell ${node.ref} contains an error (${value}).`
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
return value;
|
|
2442
|
+
}
|
|
2443
|
+
case "Range":
|
|
2444
|
+
return this.evaluateRange(node);
|
|
2445
|
+
case "PropertyRef": {
|
|
2446
|
+
if (!this.context.resolveProperty) {
|
|
2447
|
+
throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);
|
|
2448
|
+
}
|
|
2449
|
+
return this.context.resolveProperty(node.property);
|
|
2450
|
+
}
|
|
2451
|
+
case "UnaryOp": {
|
|
2452
|
+
const raw = this.evaluateNode(node.argument);
|
|
2453
|
+
const val = toNumber(raw);
|
|
2454
|
+
if (isFormulaError(val)) {
|
|
2455
|
+
return isFormulaError(raw) ? raw : this.fail("#VALUE!", `The ${node.op} operator can't use text (${quote(raw)}).`);
|
|
2456
|
+
}
|
|
2457
|
+
if (node.op === "-") return -val;
|
|
2458
|
+
if (node.op === "+") return val;
|
|
2459
|
+
throw new Error(`Unsupported unary operator: ${node.op}`);
|
|
2460
|
+
}
|
|
2461
|
+
case "BinaryOp": {
|
|
2462
|
+
const left = this.evaluateNode(node.left);
|
|
2463
|
+
if (isFormulaError(left)) return left;
|
|
2464
|
+
const right = this.evaluateNode(node.right);
|
|
2465
|
+
if (isFormulaError(right)) return right;
|
|
2466
|
+
if (["+", "-", "*", "/", "^"].includes(node.op)) {
|
|
2467
|
+
const a = toNumber(left);
|
|
2468
|
+
if (isFormulaError(a)) return this.fail("#VALUE!", `The ${node.op} operator can't use text (${quote(left)}).`);
|
|
2469
|
+
const b = toNumber(right);
|
|
2470
|
+
if (isFormulaError(b)) return this.fail("#VALUE!", `The ${node.op} operator can't use text (${quote(right)}).`);
|
|
2471
|
+
const x = a;
|
|
2472
|
+
const y = b;
|
|
2473
|
+
let result;
|
|
2474
|
+
switch (node.op) {
|
|
2475
|
+
case "+":
|
|
2476
|
+
result = x + y;
|
|
2477
|
+
break;
|
|
2478
|
+
case "-":
|
|
2479
|
+
result = x - y;
|
|
2480
|
+
break;
|
|
2481
|
+
case "*":
|
|
2482
|
+
result = x * y;
|
|
2483
|
+
break;
|
|
2484
|
+
case "/":
|
|
2485
|
+
if (y === 0) return this.fail("#DIV/0!", "The formula divides by zero.");
|
|
2486
|
+
result = x / y;
|
|
2487
|
+
break;
|
|
2488
|
+
default:
|
|
2489
|
+
result = Math.pow(x, y);
|
|
2490
|
+
}
|
|
2491
|
+
return Number.isFinite(result) ? result : this.fail("#NUM!", "The result is too large or not a number.");
|
|
2492
|
+
}
|
|
2493
|
+
switch (node.op) {
|
|
2494
|
+
case "&":
|
|
2495
|
+
return toText(left) + toText(right);
|
|
2496
|
+
case "=":
|
|
2497
|
+
return compareValues(left, right) === 0;
|
|
2498
|
+
case "<>":
|
|
2499
|
+
return compareValues(left, right) !== 0;
|
|
2500
|
+
case "<":
|
|
2501
|
+
return compareValues(left, right) < 0;
|
|
2502
|
+
case "<=":
|
|
2503
|
+
return compareValues(left, right) <= 0;
|
|
2504
|
+
case ">":
|
|
2505
|
+
return compareValues(left, right) > 0;
|
|
2506
|
+
case ">=":
|
|
2507
|
+
return compareValues(left, right) >= 0;
|
|
2508
|
+
default:
|
|
2509
|
+
throw new Error(`Unsupported binary operator: ${node.op}`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
case "FunctionCall":
|
|
2513
|
+
return this.callFunction(node);
|
|
2514
|
+
default:
|
|
2515
|
+
throw new Error(`Unknown AST node: ${node.type}`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
evaluateRange(node) {
|
|
2519
|
+
if (!this.context.resolveRange) {
|
|
2520
|
+
throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);
|
|
2521
|
+
}
|
|
2522
|
+
const values = this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow);
|
|
2523
|
+
if (!Array.isArray(values)) return values;
|
|
2524
|
+
const cols = Math.abs(colNameToIndex(node.endCol) - colNameToIndex(node.startCol)) + 1;
|
|
2525
|
+
if (values.length % cols === 0) withRangeShape(values, values.length / cols, cols);
|
|
2526
|
+
const error = values.find(isFormulaError);
|
|
2527
|
+
if (error) this.fail(error, `The range ${node.raw} contains an error (${error}).`);
|
|
2528
|
+
return values;
|
|
2529
|
+
}
|
|
2530
|
+
callFunction(node) {
|
|
2531
|
+
const def = getFormulaFunction(node.name);
|
|
2532
|
+
if (!def) return this.fail("#NAME?", `Unknown function "${node.name.toUpperCase()}".`);
|
|
2533
|
+
const countError = checkFormulaArgumentCount(def, node.args.length);
|
|
2534
|
+
if (countError) return this.fail("#ERROR!", countError);
|
|
2535
|
+
const result = def.evaluate(this.createContext(def, node.args));
|
|
2536
|
+
if (typeof result === "number" && !Number.isFinite(result)) {
|
|
2537
|
+
return this.fail("#NUM!", `${def.name} produced an invalid number.`);
|
|
2538
|
+
}
|
|
2539
|
+
return result === void 0 || result === null ? "" : result;
|
|
2540
|
+
}
|
|
2541
|
+
createContext(def, nodes) {
|
|
2542
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2543
|
+
const args = nodes.map((node, index) => ({
|
|
2544
|
+
index,
|
|
2545
|
+
node,
|
|
2546
|
+
value: () => {
|
|
2547
|
+
if (!cache.has(index)) cache.set(index, this.evaluateNode(node));
|
|
2548
|
+
return cache.get(index);
|
|
2549
|
+
}
|
|
2550
|
+
}));
|
|
2551
|
+
const has = (index) => index >= 0 && index < args.length;
|
|
2552
|
+
const value = (index) => has(index) ? args[index].value() : "";
|
|
2553
|
+
const argError = (index, code, reason) => this.fail(code, `Argument ${index + 1} of ${def.name} ${reason}`);
|
|
2554
|
+
const scalar = (index) => {
|
|
2555
|
+
const v = value(index);
|
|
2556
|
+
if (!Array.isArray(v)) return v;
|
|
2557
|
+
if (v.length <= 1) return v.length === 1 ? v[0] : "";
|
|
2558
|
+
return argError(index, "#VALUE!", "must be a single value, not a range.");
|
|
2559
|
+
};
|
|
2560
|
+
const number = (index, fallback) => {
|
|
2561
|
+
if (!has(index) && fallback !== void 0) return fallback;
|
|
2562
|
+
const v = scalar(index);
|
|
2563
|
+
if (isFormulaError(v)) return v;
|
|
2564
|
+
const num = toNumber(v);
|
|
2565
|
+
return isFormulaError(num) ? argError(index, "#VALUE!", `can't be text (${quote(v)}).`) : num;
|
|
2566
|
+
};
|
|
2567
|
+
const flatValues = (from = 0, to = args.length) => {
|
|
2568
|
+
const items = [];
|
|
2569
|
+
for (let i = from; i < Math.min(to, args.length); i++) {
|
|
2570
|
+
const v = args[i].value();
|
|
2571
|
+
if (Array.isArray(v)) {
|
|
2572
|
+
for (const item of v) items.push(item);
|
|
2573
|
+
} else {
|
|
2574
|
+
items.push(v);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return items;
|
|
2578
|
+
};
|
|
2579
|
+
return {
|
|
2580
|
+
name: def.name,
|
|
2581
|
+
args,
|
|
2582
|
+
error: (code, message) => this.fail(code, _nullishCoalesce(message, () => ( describeFormulaError(code)))),
|
|
2583
|
+
argError,
|
|
2584
|
+
has,
|
|
2585
|
+
value,
|
|
2586
|
+
scalar,
|
|
2587
|
+
number,
|
|
2588
|
+
integer: (index, fallback) => {
|
|
2589
|
+
const num = number(index, fallback);
|
|
2590
|
+
return isFormulaError(num) ? num : Math.trunc(num);
|
|
2591
|
+
},
|
|
2592
|
+
text: (index, fallback) => {
|
|
2593
|
+
if (!has(index) && fallback !== void 0) return fallback;
|
|
2594
|
+
const v = scalar(index);
|
|
2595
|
+
return isFormulaError(v) ? v : toText(v);
|
|
2596
|
+
},
|
|
2597
|
+
boolean: (index, fallback) => {
|
|
2598
|
+
if (!has(index) && fallback !== void 0) return fallback;
|
|
2599
|
+
const v = scalar(index);
|
|
2600
|
+
if (isFormulaError(v)) return v;
|
|
2601
|
+
const bool = toBoolean(v);
|
|
2602
|
+
return isFormulaError(bool) ? argError(index, "#VALUE!", `must be TRUE or FALSE, not text (${quote(v)}).`) : bool;
|
|
2603
|
+
},
|
|
2604
|
+
date: (index) => {
|
|
2605
|
+
const v = scalar(index);
|
|
2606
|
+
if (isFormulaError(v)) return v;
|
|
2607
|
+
if (isBlank(v)) return argError(index, "#VALUE!", "is empty, but a date is needed.");
|
|
2608
|
+
return _nullishCoalesce(toDate(v), () => ( argError(index, "#VALUE!", `(${quote(v)}) isn't a valid date.`)));
|
|
2609
|
+
},
|
|
2610
|
+
range: (index) => {
|
|
2611
|
+
const v = value(index);
|
|
2612
|
+
if (Array.isArray(v)) return toFormulaRange(v);
|
|
2613
|
+
if (isFormulaError(v)) return v;
|
|
2614
|
+
return argError(index, "#VALUE!", "must be a range.");
|
|
2615
|
+
},
|
|
2616
|
+
flatValues,
|
|
2617
|
+
numbers: (from, to) => {
|
|
2618
|
+
const nums = [];
|
|
2619
|
+
for (const item of flatValues(from, to)) {
|
|
2620
|
+
if (isFormulaError(item)) return item;
|
|
2621
|
+
if (isBlank(item) || typeof item === "boolean") continue;
|
|
2622
|
+
const num = typeof item === "number" ? item : numericValue(item);
|
|
2623
|
+
if (num !== null) nums.push(num);
|
|
2624
|
+
}
|
|
2625
|
+
return nums;
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
}, _class2);
|
|
2630
|
+
|
|
2631
|
+
|
|
2632
|
+
|
|
2633
|
+
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
|
|
2637
|
+
|
|
2638
|
+
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
|
|
2642
|
+
|
|
2643
|
+
|
|
2644
|
+
|
|
2645
|
+
|
|
2646
|
+
|
|
2647
|
+
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
|
|
2656
|
+
exports.tokenizeFormula = tokenizeFormula; exports.WHOLE_COLUMN_END_ROW = WHOLE_COLUMN_END_ROW; exports.parseCellReference = parseCellReference; exports.parseRange = parseRange; exports.FormulaParser = FormulaParser; exports.parseFormula = parseFormula; exports.FORMULA_ERRORS = FORMULA_ERRORS; exports.isFormulaError = isFormulaError; exports.colNameToIndex = colNameToIndex; exports.indexToColName = indexToColName; exports.wildcardToRegExp = wildcardToRegExp; exports.parseCriteria = parseCriteria; exports.formatValueWithCode = formatValueWithCode; exports.registerFormulaFunction = registerFormulaFunction; exports.unregisterFormulaFunction = unregisterFormulaFunction; exports.getFormulaFunction = getFormulaFunction; exports.listFormulaFunctions = listFormulaFunctions; exports.searchFormulaFunctions = searchFormulaFunctions; exports.getFormulaArgumentSpecIndex = getFormulaArgumentSpecIndex; exports.checkFormulaArgumentCount = checkFormulaArgumentCount; exports.getFormulaSignatureParts = getFormulaSignatureParts; exports.getFormulaSignature = getFormulaSignature; exports.describeFormulaError = describeFormulaError; exports.FormulaEvaluator = FormulaEvaluator;
|
|
2657
|
+
//# sourceMappingURL=chunk-JYRIOFEN.cjs.map
|