@molecule/api-formula-engine 1.0.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/LICENSE +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/dependencies.d.ts +44 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +110 -0
- package/dist/evaluator.d.ts +29 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +159 -0
- package/dist/functions.d.ts +22 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +774 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +255 -0
- package/dist/references.d.ts +57 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +116 -0
- package/dist/sheet.d.ts +130 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/sheet.js +291 -0
- package/dist/tokenizer.d.ts +32 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +187 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/values.d.ts +80 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +190 -0
- package/package.json +33 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in function registry.
|
|
3
|
+
*
|
|
4
|
+
* Each entry is a pure function: given a list of evaluated argument
|
|
5
|
+
* values (scalars or expanded ranges), returns a `CellValue`.
|
|
6
|
+
*
|
|
7
|
+
* Implements: SUM, AVERAGE/AVG, MIN, MAX, COUNT, COUNTA, COUNTIF,
|
|
8
|
+
* SUMIF, IF, AND, OR, NOT, IFS, SWITCH, IFERROR, ISERROR, ISNUMBER,
|
|
9
|
+
* ISBLANK, CONCAT/CONCATENATE, LEFT, RIGHT, MID, LEN, TRIM, UPPER,
|
|
10
|
+
* LOWER, SUBSTITUTE, ROUND, ABS, MOD, POWER, SQRT, INT, CEILING,
|
|
11
|
+
* FLOOR, DATE, TODAY, NOW, YEAR, MONTH, DAY, DATEDIF / DATEDIFF.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { compareValues, dateToSerial, firstError, isError, makeError, serialToDate, toBoolean, toNumber, toStringValue, } from './values.js';
|
|
16
|
+
/**
|
|
17
|
+
* Flatten an argument value: ranges arrive as arrays, scalars stay as
|
|
18
|
+
* scalars. Always returns a flat `CellValue[]`.
|
|
19
|
+
*
|
|
20
|
+
* @param arg - Either a scalar `CellValue` or an array (range).
|
|
21
|
+
* @returns Flat list of cell values.
|
|
22
|
+
*/
|
|
23
|
+
function flatten(arg) {
|
|
24
|
+
if (Array.isArray(arg))
|
|
25
|
+
return [...arg];
|
|
26
|
+
return [arg];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Flatten all argument values (scalars and ranges) into a single `CellValue[]`.
|
|
30
|
+
*/
|
|
31
|
+
function flattenAll(args) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const a of args) {
|
|
34
|
+
if (Array.isArray(a)) {
|
|
35
|
+
for (const v of a)
|
|
36
|
+
out.push(v);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
out.push(a);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Coerce one argument value (scalar) to a number, propagating errors.
|
|
46
|
+
*
|
|
47
|
+
* @param arg - Argument value.
|
|
48
|
+
* @returns Number or error.
|
|
49
|
+
*/
|
|
50
|
+
function asScalarNumber(arg) {
|
|
51
|
+
if (Array.isArray(arg)) {
|
|
52
|
+
if (arg.length === 0)
|
|
53
|
+
return 0;
|
|
54
|
+
return asScalarNumber(arg[0]);
|
|
55
|
+
}
|
|
56
|
+
const v = arg;
|
|
57
|
+
const n = toNumber(v);
|
|
58
|
+
if (isError(n))
|
|
59
|
+
return { error: n };
|
|
60
|
+
return n;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extract the first element of a range argument, or return the scalar as-is.
|
|
64
|
+
*/
|
|
65
|
+
function asScalar(arg) {
|
|
66
|
+
if (Array.isArray(arg))
|
|
67
|
+
return arg.length === 0 ? null : arg[0];
|
|
68
|
+
return arg;
|
|
69
|
+
}
|
|
70
|
+
/** SUM(...): numeric sum, ignoring text and booleans Excel-style. */
|
|
71
|
+
const SUM = (args) => {
|
|
72
|
+
const flat = flattenAll(args);
|
|
73
|
+
let total = 0;
|
|
74
|
+
for (const v of flat) {
|
|
75
|
+
if (isError(v))
|
|
76
|
+
return v;
|
|
77
|
+
if (typeof v === 'number')
|
|
78
|
+
total += v;
|
|
79
|
+
else if (v instanceof Date)
|
|
80
|
+
total += dateToSerial(v);
|
|
81
|
+
else if (typeof v === 'boolean')
|
|
82
|
+
total += v ? 1 : 0;
|
|
83
|
+
// strings, null → ignored (Excel SUM behavior)
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
};
|
|
87
|
+
/** AVERAGE / AVG: numeric mean, error on empty. */
|
|
88
|
+
const AVERAGE = (args) => {
|
|
89
|
+
const flat = flattenAll(args);
|
|
90
|
+
let total = 0;
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (const v of flat) {
|
|
93
|
+
if (isError(v))
|
|
94
|
+
return v;
|
|
95
|
+
if (typeof v === 'number') {
|
|
96
|
+
total += v;
|
|
97
|
+
count++;
|
|
98
|
+
}
|
|
99
|
+
else if (typeof v === 'boolean') {
|
|
100
|
+
total += v ? 1 : 0;
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
else if (v instanceof Date) {
|
|
104
|
+
total += dateToSerial(v);
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (count === 0)
|
|
109
|
+
return makeError('#DIV/0!');
|
|
110
|
+
return total / count;
|
|
111
|
+
};
|
|
112
|
+
/** MIN: numeric minimum. */
|
|
113
|
+
const MIN = (args) => {
|
|
114
|
+
const flat = flattenAll(args);
|
|
115
|
+
let min = Number.POSITIVE_INFINITY;
|
|
116
|
+
let seen = false;
|
|
117
|
+
for (const v of flat) {
|
|
118
|
+
if (isError(v))
|
|
119
|
+
return v;
|
|
120
|
+
if (typeof v === 'number') {
|
|
121
|
+
if (v < min)
|
|
122
|
+
min = v;
|
|
123
|
+
seen = true;
|
|
124
|
+
}
|
|
125
|
+
else if (v instanceof Date) {
|
|
126
|
+
const n = dateToSerial(v);
|
|
127
|
+
if (n < min)
|
|
128
|
+
min = n;
|
|
129
|
+
seen = true;
|
|
130
|
+
}
|
|
131
|
+
else if (typeof v === 'boolean') {
|
|
132
|
+
const n = v ? 1 : 0;
|
|
133
|
+
if (n < min)
|
|
134
|
+
min = n;
|
|
135
|
+
seen = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return seen ? min : 0;
|
|
139
|
+
};
|
|
140
|
+
/** MAX: numeric maximum. */
|
|
141
|
+
const MAX = (args) => {
|
|
142
|
+
const flat = flattenAll(args);
|
|
143
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
144
|
+
let seen = false;
|
|
145
|
+
for (const v of flat) {
|
|
146
|
+
if (isError(v))
|
|
147
|
+
return v;
|
|
148
|
+
if (typeof v === 'number') {
|
|
149
|
+
if (v > max)
|
|
150
|
+
max = v;
|
|
151
|
+
seen = true;
|
|
152
|
+
}
|
|
153
|
+
else if (v instanceof Date) {
|
|
154
|
+
const n = dateToSerial(v);
|
|
155
|
+
if (n > max)
|
|
156
|
+
max = n;
|
|
157
|
+
seen = true;
|
|
158
|
+
}
|
|
159
|
+
else if (typeof v === 'boolean') {
|
|
160
|
+
const n = v ? 1 : 0;
|
|
161
|
+
if (n > max)
|
|
162
|
+
max = n;
|
|
163
|
+
seen = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return seen ? max : 0;
|
|
167
|
+
};
|
|
168
|
+
/** COUNT: numeric count only. */
|
|
169
|
+
const COUNT = (args) => {
|
|
170
|
+
const flat = flattenAll(args);
|
|
171
|
+
let n = 0;
|
|
172
|
+
for (const v of flat) {
|
|
173
|
+
if (typeof v === 'number' || v instanceof Date)
|
|
174
|
+
n++;
|
|
175
|
+
}
|
|
176
|
+
return n;
|
|
177
|
+
};
|
|
178
|
+
/** COUNTA: non-empty count (any type except null/empty-string). */
|
|
179
|
+
const COUNTA = (args) => {
|
|
180
|
+
const flat = flattenAll(args);
|
|
181
|
+
let n = 0;
|
|
182
|
+
for (const v of flat) {
|
|
183
|
+
if (v !== null && !(typeof v === 'string' && v === ''))
|
|
184
|
+
n++;
|
|
185
|
+
}
|
|
186
|
+
return n;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Test if a value matches a COUNTIF/SUMIF criterion. Criteria can be:
|
|
190
|
+
* - a number/string for equality (with `*` and `?` wildcards in
|
|
191
|
+
* strings)
|
|
192
|
+
* - `">5"`, `"<>foo"`, `"<=10"`, etc.
|
|
193
|
+
*/
|
|
194
|
+
function matchesCriteria(value, criteria) {
|
|
195
|
+
if (isError(value) || isError(criteria))
|
|
196
|
+
return false;
|
|
197
|
+
// Convert criteria to a string for operator parsing
|
|
198
|
+
const critStr = (() => {
|
|
199
|
+
if (typeof criteria === 'string')
|
|
200
|
+
return criteria;
|
|
201
|
+
if (typeof criteria === 'number')
|
|
202
|
+
return String(criteria);
|
|
203
|
+
if (typeof criteria === 'boolean')
|
|
204
|
+
return criteria ? 'TRUE' : 'FALSE';
|
|
205
|
+
if (criteria === null)
|
|
206
|
+
return '';
|
|
207
|
+
return '';
|
|
208
|
+
})();
|
|
209
|
+
const m = /^(<=|>=|<>|<|>|=)?(.*)$/.exec(critStr);
|
|
210
|
+
const op = m?.[1] ?? '=';
|
|
211
|
+
const rhsRaw = m?.[2] ?? '';
|
|
212
|
+
// Parse rhs as number if possible, else string
|
|
213
|
+
let rhs = rhsRaw;
|
|
214
|
+
const asNum = Number(rhsRaw);
|
|
215
|
+
if (rhsRaw.trim() !== '' && Number.isFinite(asNum))
|
|
216
|
+
rhs = asNum;
|
|
217
|
+
if (rhsRaw.toUpperCase() === 'TRUE')
|
|
218
|
+
rhs = true;
|
|
219
|
+
if (rhsRaw.toUpperCase() === 'FALSE')
|
|
220
|
+
rhs = false;
|
|
221
|
+
// Wildcards only apply on plain equality with string criteria.
|
|
222
|
+
if ((op === '=' || op === undefined) && typeof rhs === 'string' && /[*?]/.test(rhs)) {
|
|
223
|
+
if (typeof value !== 'string')
|
|
224
|
+
return false;
|
|
225
|
+
const pattern = rhs
|
|
226
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
227
|
+
.replace(/\*/g, '.*')
|
|
228
|
+
.replace(/\?/g, '.');
|
|
229
|
+
return new RegExp(`^${pattern}$`, 'i').test(value);
|
|
230
|
+
}
|
|
231
|
+
const cmp = compareValues(value, rhs);
|
|
232
|
+
if (isError(cmp))
|
|
233
|
+
return false;
|
|
234
|
+
switch (op) {
|
|
235
|
+
case '=':
|
|
236
|
+
case undefined:
|
|
237
|
+
return cmp === 0;
|
|
238
|
+
case '<>':
|
|
239
|
+
return cmp !== 0;
|
|
240
|
+
case '<':
|
|
241
|
+
return cmp < 0;
|
|
242
|
+
case '<=':
|
|
243
|
+
return cmp <= 0;
|
|
244
|
+
case '>':
|
|
245
|
+
return cmp > 0;
|
|
246
|
+
case '>=':
|
|
247
|
+
return cmp >= 0;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
/** COUNTIF(range, criteria). */
|
|
252
|
+
const COUNTIF = (args) => {
|
|
253
|
+
if (args.length !== 2)
|
|
254
|
+
return makeError('#VALUE!', 'COUNTIF requires exactly 2 arguments');
|
|
255
|
+
const range = flatten(args[0]);
|
|
256
|
+
const crit = asScalar(args[1]);
|
|
257
|
+
if (isError(crit))
|
|
258
|
+
return crit;
|
|
259
|
+
let n = 0;
|
|
260
|
+
for (const v of range) {
|
|
261
|
+
if (isError(v))
|
|
262
|
+
return v;
|
|
263
|
+
if (matchesCriteria(v, crit))
|
|
264
|
+
n++;
|
|
265
|
+
}
|
|
266
|
+
return n;
|
|
267
|
+
};
|
|
268
|
+
/** SUMIF(range, criteria, [sumRange]). */
|
|
269
|
+
const SUMIF = (args) => {
|
|
270
|
+
if (args.length < 2 || args.length > 3) {
|
|
271
|
+
return makeError('#VALUE!', 'SUMIF requires 2 or 3 arguments');
|
|
272
|
+
}
|
|
273
|
+
const matchRange = flatten(args[0]);
|
|
274
|
+
const crit = asScalar(args[1]);
|
|
275
|
+
if (isError(crit))
|
|
276
|
+
return crit;
|
|
277
|
+
const sumRange = args.length === 3 ? flatten(args[2]) : matchRange;
|
|
278
|
+
let total = 0;
|
|
279
|
+
for (let i = 0; i < matchRange.length; i++) {
|
|
280
|
+
const v = matchRange[i];
|
|
281
|
+
if (isError(v))
|
|
282
|
+
return v;
|
|
283
|
+
if (!matchesCriteria(v, crit))
|
|
284
|
+
continue;
|
|
285
|
+
const target = sumRange[i] ?? null;
|
|
286
|
+
if (isError(target))
|
|
287
|
+
return target;
|
|
288
|
+
if (typeof target === 'number')
|
|
289
|
+
total += target;
|
|
290
|
+
else if (target instanceof Date)
|
|
291
|
+
total += dateToSerial(target);
|
|
292
|
+
else if (typeof target === 'boolean')
|
|
293
|
+
total += target ? 1 : 0;
|
|
294
|
+
}
|
|
295
|
+
return total;
|
|
296
|
+
};
|
|
297
|
+
/** IF(condition, thenValue, [elseValue]). */
|
|
298
|
+
const IF = (args) => {
|
|
299
|
+
if (args.length < 2 || args.length > 3) {
|
|
300
|
+
return makeError('#VALUE!', 'IF requires 2 or 3 arguments');
|
|
301
|
+
}
|
|
302
|
+
const cond = asScalar(args[0]);
|
|
303
|
+
if (isError(cond))
|
|
304
|
+
return cond;
|
|
305
|
+
const b = toBoolean(cond);
|
|
306
|
+
if (isError(b))
|
|
307
|
+
return b;
|
|
308
|
+
if (b)
|
|
309
|
+
return asScalar(args[1]);
|
|
310
|
+
return args.length === 3 ? asScalar(args[2]) : false;
|
|
311
|
+
};
|
|
312
|
+
/** AND(...): true if all truthy. */
|
|
313
|
+
const AND = (args) => {
|
|
314
|
+
const flat = flattenAll(args);
|
|
315
|
+
if (flat.length === 0)
|
|
316
|
+
return makeError('#VALUE!', 'AND requires at least one argument');
|
|
317
|
+
for (const v of flat) {
|
|
318
|
+
if (isError(v))
|
|
319
|
+
return v;
|
|
320
|
+
const b = toBoolean(v);
|
|
321
|
+
if (isError(b))
|
|
322
|
+
return b;
|
|
323
|
+
if (!b)
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
};
|
|
328
|
+
/** OR(...): true if any truthy. */
|
|
329
|
+
const OR = (args) => {
|
|
330
|
+
const flat = flattenAll(args);
|
|
331
|
+
if (flat.length === 0)
|
|
332
|
+
return makeError('#VALUE!', 'OR requires at least one argument');
|
|
333
|
+
for (const v of flat) {
|
|
334
|
+
if (isError(v))
|
|
335
|
+
return v;
|
|
336
|
+
const b = toBoolean(v);
|
|
337
|
+
if (isError(b))
|
|
338
|
+
return b;
|
|
339
|
+
if (b)
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
};
|
|
344
|
+
/** NOT(value). */
|
|
345
|
+
const NOT = (args) => {
|
|
346
|
+
if (args.length !== 1)
|
|
347
|
+
return makeError('#VALUE!', 'NOT requires exactly 1 argument');
|
|
348
|
+
const v = asScalar(args[0]);
|
|
349
|
+
if (isError(v))
|
|
350
|
+
return v;
|
|
351
|
+
const b = toBoolean(v);
|
|
352
|
+
if (isError(b))
|
|
353
|
+
return b;
|
|
354
|
+
return !b;
|
|
355
|
+
};
|
|
356
|
+
/** IFS(cond1, val1, [cond2, val2, ...]). Excel-compatible. */
|
|
357
|
+
const IFS = (args) => {
|
|
358
|
+
if (args.length < 2 || args.length % 2 !== 0) {
|
|
359
|
+
return makeError('#VALUE!', 'IFS requires pairs of (condition, value)');
|
|
360
|
+
}
|
|
361
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
362
|
+
const c = asScalar(args[i]);
|
|
363
|
+
if (isError(c))
|
|
364
|
+
return c;
|
|
365
|
+
const b = toBoolean(c);
|
|
366
|
+
if (isError(b))
|
|
367
|
+
return b;
|
|
368
|
+
if (b)
|
|
369
|
+
return asScalar(args[i + 1]);
|
|
370
|
+
}
|
|
371
|
+
return makeError('#N/A', 'IFS: no condition matched');
|
|
372
|
+
};
|
|
373
|
+
/** SWITCH(expr, c1, v1, [c2, v2, ...], [default]). */
|
|
374
|
+
const SWITCH = (args) => {
|
|
375
|
+
if (args.length < 3) {
|
|
376
|
+
return makeError('#VALUE!', 'SWITCH requires at least 3 arguments');
|
|
377
|
+
}
|
|
378
|
+
const subject = asScalar(args[0]);
|
|
379
|
+
if (isError(subject))
|
|
380
|
+
return subject;
|
|
381
|
+
let i = 1;
|
|
382
|
+
while (i + 1 < args.length) {
|
|
383
|
+
const candidate = asScalar(args[i]);
|
|
384
|
+
if (isError(candidate))
|
|
385
|
+
return candidate;
|
|
386
|
+
const cmp = compareValues(subject, candidate);
|
|
387
|
+
if (!isError(cmp) && cmp === 0)
|
|
388
|
+
return asScalar(args[i + 1]);
|
|
389
|
+
i += 2;
|
|
390
|
+
}
|
|
391
|
+
// Trailing odd argument = default.
|
|
392
|
+
if (i < args.length)
|
|
393
|
+
return asScalar(args[i]);
|
|
394
|
+
return makeError('#N/A', 'SWITCH: no case matched');
|
|
395
|
+
};
|
|
396
|
+
/** IFERROR(value, fallback). */
|
|
397
|
+
const IFERROR = (args) => {
|
|
398
|
+
if (args.length !== 2)
|
|
399
|
+
return makeError('#VALUE!', 'IFERROR requires 2 arguments');
|
|
400
|
+
const v = asScalar(args[0]);
|
|
401
|
+
if (isError(v))
|
|
402
|
+
return asScalar(args[1]);
|
|
403
|
+
return v;
|
|
404
|
+
};
|
|
405
|
+
const ISERROR = (args) => isError(asScalar(args[0] ?? null));
|
|
406
|
+
const ISNUMBER = (args) => typeof asScalar(args[0] ?? null) === 'number';
|
|
407
|
+
const ISBLANK = (args) => asScalar(args[0] ?? null) === null;
|
|
408
|
+
// --- String functions ---
|
|
409
|
+
const CONCAT = (args) => {
|
|
410
|
+
const flat = flattenAll(args);
|
|
411
|
+
const err = firstError(flat);
|
|
412
|
+
if (err)
|
|
413
|
+
return err;
|
|
414
|
+
let out = '';
|
|
415
|
+
for (const v of flat) {
|
|
416
|
+
const s = toStringValue(v);
|
|
417
|
+
if (isError(s))
|
|
418
|
+
return s;
|
|
419
|
+
out += s;
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
};
|
|
423
|
+
const LEFT = (args) => {
|
|
424
|
+
const text = toStringValue(asScalar(args[0] ?? null));
|
|
425
|
+
if (isError(text))
|
|
426
|
+
return text;
|
|
427
|
+
const nArg = args.length >= 2 ? asScalarNumber(args[1]) : 1;
|
|
428
|
+
if (typeof nArg === 'object')
|
|
429
|
+
return nArg.error;
|
|
430
|
+
if (nArg < 0)
|
|
431
|
+
return makeError('#VALUE!');
|
|
432
|
+
return text.slice(0, Math.floor(nArg));
|
|
433
|
+
};
|
|
434
|
+
const RIGHT = (args) => {
|
|
435
|
+
const text = toStringValue(asScalar(args[0] ?? null));
|
|
436
|
+
if (isError(text))
|
|
437
|
+
return text;
|
|
438
|
+
const nArg = args.length >= 2 ? asScalarNumber(args[1]) : 1;
|
|
439
|
+
if (typeof nArg === 'object')
|
|
440
|
+
return nArg.error;
|
|
441
|
+
if (nArg < 0)
|
|
442
|
+
return makeError('#VALUE!');
|
|
443
|
+
return nArg === 0 ? '' : text.slice(-Math.floor(nArg));
|
|
444
|
+
};
|
|
445
|
+
const MID = (args) => {
|
|
446
|
+
if (args.length !== 3)
|
|
447
|
+
return makeError('#VALUE!', 'MID requires 3 arguments');
|
|
448
|
+
const text = toStringValue(asScalar(args[0]));
|
|
449
|
+
if (isError(text))
|
|
450
|
+
return text;
|
|
451
|
+
const startN = asScalarNumber(args[1]);
|
|
452
|
+
if (typeof startN === 'object')
|
|
453
|
+
return startN.error;
|
|
454
|
+
const lengthN = asScalarNumber(args[2]);
|
|
455
|
+
if (typeof lengthN === 'object')
|
|
456
|
+
return lengthN.error;
|
|
457
|
+
if (startN < 1 || lengthN < 0)
|
|
458
|
+
return makeError('#VALUE!');
|
|
459
|
+
return text.slice(Math.floor(startN) - 1, Math.floor(startN) - 1 + Math.floor(lengthN));
|
|
460
|
+
};
|
|
461
|
+
const LEN = (args) => {
|
|
462
|
+
const text = toStringValue(asScalar(args[0] ?? null));
|
|
463
|
+
if (isError(text))
|
|
464
|
+
return text;
|
|
465
|
+
return text.length;
|
|
466
|
+
};
|
|
467
|
+
const TRIM = (args) => {
|
|
468
|
+
const text = toStringValue(asScalar(args[0] ?? null));
|
|
469
|
+
if (isError(text))
|
|
470
|
+
return text;
|
|
471
|
+
// Excel TRIM collapses multiple internal spaces to one and strips ends.
|
|
472
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
473
|
+
};
|
|
474
|
+
const UPPER = (args) => {
|
|
475
|
+
const text = toStringValue(asScalar(args[0] ?? null));
|
|
476
|
+
if (isError(text))
|
|
477
|
+
return text;
|
|
478
|
+
return text.toUpperCase();
|
|
479
|
+
};
|
|
480
|
+
const LOWER = (args) => {
|
|
481
|
+
const text = toStringValue(asScalar(args[0] ?? null));
|
|
482
|
+
if (isError(text))
|
|
483
|
+
return text;
|
|
484
|
+
return text.toLowerCase();
|
|
485
|
+
};
|
|
486
|
+
const SUBSTITUTE = (args) => {
|
|
487
|
+
if (args.length < 3 || args.length > 4) {
|
|
488
|
+
return makeError('#VALUE!', 'SUBSTITUTE requires 3 or 4 arguments');
|
|
489
|
+
}
|
|
490
|
+
const text = toStringValue(asScalar(args[0]));
|
|
491
|
+
if (isError(text))
|
|
492
|
+
return text;
|
|
493
|
+
const oldText = toStringValue(asScalar(args[1]));
|
|
494
|
+
if (isError(oldText))
|
|
495
|
+
return oldText;
|
|
496
|
+
const newText = toStringValue(asScalar(args[2]));
|
|
497
|
+
if (isError(newText))
|
|
498
|
+
return newText;
|
|
499
|
+
if (oldText === '')
|
|
500
|
+
return text;
|
|
501
|
+
if (args.length === 3) {
|
|
502
|
+
return text.split(oldText).join(newText);
|
|
503
|
+
}
|
|
504
|
+
const instanceN = asScalarNumber(args[3]);
|
|
505
|
+
if (typeof instanceN === 'object')
|
|
506
|
+
return instanceN.error;
|
|
507
|
+
if (instanceN < 1)
|
|
508
|
+
return makeError('#VALUE!');
|
|
509
|
+
let count = 0;
|
|
510
|
+
let idx = 0;
|
|
511
|
+
while (idx <= text.length) {
|
|
512
|
+
const found = text.indexOf(oldText, idx);
|
|
513
|
+
if (found < 0)
|
|
514
|
+
break;
|
|
515
|
+
count++;
|
|
516
|
+
if (count === Math.floor(instanceN)) {
|
|
517
|
+
return text.slice(0, found) + newText + text.slice(found + oldText.length);
|
|
518
|
+
}
|
|
519
|
+
idx = found + oldText.length;
|
|
520
|
+
}
|
|
521
|
+
return text;
|
|
522
|
+
};
|
|
523
|
+
// --- Math functions ---
|
|
524
|
+
const ROUND = (args) => {
|
|
525
|
+
const n = asScalarNumber(args[0] ?? 0);
|
|
526
|
+
if (typeof n === 'object')
|
|
527
|
+
return n.error;
|
|
528
|
+
const digits = args.length >= 2 ? asScalarNumber(args[1]) : 0;
|
|
529
|
+
if (typeof digits === 'object')
|
|
530
|
+
return digits.error;
|
|
531
|
+
const f = 10 ** Math.floor(digits);
|
|
532
|
+
return Math.round(n * f) / f;
|
|
533
|
+
};
|
|
534
|
+
const ABS = (args) => {
|
|
535
|
+
const n = asScalarNumber(args[0] ?? 0);
|
|
536
|
+
if (typeof n === 'object')
|
|
537
|
+
return n.error;
|
|
538
|
+
return Math.abs(n);
|
|
539
|
+
};
|
|
540
|
+
const MOD = (args) => {
|
|
541
|
+
if (args.length !== 2)
|
|
542
|
+
return makeError('#VALUE!', 'MOD requires 2 arguments');
|
|
543
|
+
const a = asScalarNumber(args[0]);
|
|
544
|
+
if (typeof a === 'object')
|
|
545
|
+
return a.error;
|
|
546
|
+
const b = asScalarNumber(args[1]);
|
|
547
|
+
if (typeof b === 'object')
|
|
548
|
+
return b.error;
|
|
549
|
+
if (b === 0)
|
|
550
|
+
return makeError('#DIV/0!');
|
|
551
|
+
// Excel MOD: result has sign of divisor.
|
|
552
|
+
return a - b * Math.floor(a / b);
|
|
553
|
+
};
|
|
554
|
+
const POWER = (args) => {
|
|
555
|
+
if (args.length !== 2)
|
|
556
|
+
return makeError('#VALUE!', 'POWER requires 2 arguments');
|
|
557
|
+
const a = asScalarNumber(args[0]);
|
|
558
|
+
if (typeof a === 'object')
|
|
559
|
+
return a.error;
|
|
560
|
+
const b = asScalarNumber(args[1]);
|
|
561
|
+
if (typeof b === 'object')
|
|
562
|
+
return b.error;
|
|
563
|
+
const r = a ** b;
|
|
564
|
+
if (!Number.isFinite(r))
|
|
565
|
+
return makeError('#NUM!');
|
|
566
|
+
return r;
|
|
567
|
+
};
|
|
568
|
+
const SQRT = (args) => {
|
|
569
|
+
const n = asScalarNumber(args[0] ?? 0);
|
|
570
|
+
if (typeof n === 'object')
|
|
571
|
+
return n.error;
|
|
572
|
+
if (n < 0)
|
|
573
|
+
return makeError('#NUM!');
|
|
574
|
+
return Math.sqrt(n);
|
|
575
|
+
};
|
|
576
|
+
const INT = (args) => {
|
|
577
|
+
const n = asScalarNumber(args[0] ?? 0);
|
|
578
|
+
if (typeof n === 'object')
|
|
579
|
+
return n.error;
|
|
580
|
+
return Math.floor(n);
|
|
581
|
+
};
|
|
582
|
+
const CEILING = (args) => {
|
|
583
|
+
const n = asScalarNumber(args[0] ?? 0);
|
|
584
|
+
if (typeof n === 'object')
|
|
585
|
+
return n.error;
|
|
586
|
+
const sig = args.length >= 2 ? asScalarNumber(args[1]) : 1;
|
|
587
|
+
if (typeof sig === 'object')
|
|
588
|
+
return sig.error;
|
|
589
|
+
if (sig === 0)
|
|
590
|
+
return 0;
|
|
591
|
+
return Math.ceil(n / sig) * sig;
|
|
592
|
+
};
|
|
593
|
+
const FLOOR = (args) => {
|
|
594
|
+
const n = asScalarNumber(args[0] ?? 0);
|
|
595
|
+
if (typeof n === 'object')
|
|
596
|
+
return n.error;
|
|
597
|
+
const sig = args.length >= 2 ? asScalarNumber(args[1]) : 1;
|
|
598
|
+
if (typeof sig === 'object')
|
|
599
|
+
return sig.error;
|
|
600
|
+
if (sig === 0)
|
|
601
|
+
return makeError('#DIV/0!');
|
|
602
|
+
return Math.floor(n / sig) * sig;
|
|
603
|
+
};
|
|
604
|
+
// --- Date functions ---
|
|
605
|
+
const DATE = (args) => {
|
|
606
|
+
if (args.length !== 3)
|
|
607
|
+
return makeError('#VALUE!', 'DATE requires 3 arguments');
|
|
608
|
+
const y = asScalarNumber(args[0]);
|
|
609
|
+
if (typeof y === 'object')
|
|
610
|
+
return y.error;
|
|
611
|
+
const m = asScalarNumber(args[1]);
|
|
612
|
+
if (typeof m === 'object')
|
|
613
|
+
return m.error;
|
|
614
|
+
const d = asScalarNumber(args[2]);
|
|
615
|
+
if (typeof d === 'object')
|
|
616
|
+
return d.error;
|
|
617
|
+
// Excel: years <100 add 1900.
|
|
618
|
+
const year = y < 100 ? y + 1900 : y;
|
|
619
|
+
return new Date(Date.UTC(year, Math.floor(m) - 1, Math.floor(d)));
|
|
620
|
+
};
|
|
621
|
+
const TODAY = (_args, ctx) => {
|
|
622
|
+
const now = ctx.now();
|
|
623
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
624
|
+
};
|
|
625
|
+
const NOW = (_args, ctx) => ctx.now();
|
|
626
|
+
const YEAR = (args) => {
|
|
627
|
+
const v = asScalar(args[0] ?? null);
|
|
628
|
+
if (isError(v))
|
|
629
|
+
return v;
|
|
630
|
+
if (v instanceof Date)
|
|
631
|
+
return v.getUTCFullYear();
|
|
632
|
+
const n = toNumber(v);
|
|
633
|
+
if (isError(n))
|
|
634
|
+
return n;
|
|
635
|
+
return serialToDate(n).getUTCFullYear();
|
|
636
|
+
};
|
|
637
|
+
const MONTH = (args) => {
|
|
638
|
+
const v = asScalar(args[0] ?? null);
|
|
639
|
+
if (isError(v))
|
|
640
|
+
return v;
|
|
641
|
+
if (v instanceof Date)
|
|
642
|
+
return v.getUTCMonth() + 1;
|
|
643
|
+
const n = toNumber(v);
|
|
644
|
+
if (isError(n))
|
|
645
|
+
return n;
|
|
646
|
+
return serialToDate(n).getUTCMonth() + 1;
|
|
647
|
+
};
|
|
648
|
+
const DAY = (args) => {
|
|
649
|
+
const v = asScalar(args[0] ?? null);
|
|
650
|
+
if (isError(v))
|
|
651
|
+
return v;
|
|
652
|
+
if (v instanceof Date)
|
|
653
|
+
return v.getUTCDate();
|
|
654
|
+
const n = toNumber(v);
|
|
655
|
+
if (isError(n))
|
|
656
|
+
return n;
|
|
657
|
+
return serialToDate(n).getUTCDate();
|
|
658
|
+
};
|
|
659
|
+
/**
|
|
660
|
+
* DATEDIFF(start, end, unit) — computes whole-unit difference.
|
|
661
|
+
* Units: "D" days, "M" months, "Y" years, "H" hours, "MIN" minutes,
|
|
662
|
+
* "S" seconds.
|
|
663
|
+
*/
|
|
664
|
+
const DATEDIFF = (args) => {
|
|
665
|
+
if (args.length !== 3)
|
|
666
|
+
return makeError('#VALUE!', 'DATEDIFF requires 3 arguments');
|
|
667
|
+
const startV = asScalar(args[0]);
|
|
668
|
+
if (isError(startV))
|
|
669
|
+
return startV;
|
|
670
|
+
const endV = asScalar(args[1]);
|
|
671
|
+
if (isError(endV))
|
|
672
|
+
return endV;
|
|
673
|
+
const unitV = asScalar(args[2]);
|
|
674
|
+
if (isError(unitV))
|
|
675
|
+
return unitV;
|
|
676
|
+
const start = startV instanceof Date
|
|
677
|
+
? startV
|
|
678
|
+
: (() => {
|
|
679
|
+
const n = toNumber(startV);
|
|
680
|
+
if (isError(n))
|
|
681
|
+
return n;
|
|
682
|
+
return serialToDate(n);
|
|
683
|
+
})();
|
|
684
|
+
if (isError(start))
|
|
685
|
+
return start;
|
|
686
|
+
const end = endV instanceof Date
|
|
687
|
+
? endV
|
|
688
|
+
: (() => {
|
|
689
|
+
const n = toNumber(endV);
|
|
690
|
+
if (isError(n))
|
|
691
|
+
return n;
|
|
692
|
+
return serialToDate(n);
|
|
693
|
+
})();
|
|
694
|
+
if (isError(end))
|
|
695
|
+
return end;
|
|
696
|
+
const unit = String(unitV).toUpperCase();
|
|
697
|
+
const ms = end.getTime() - start.getTime();
|
|
698
|
+
switch (unit) {
|
|
699
|
+
case 'D':
|
|
700
|
+
return Math.trunc(ms / 86_400_000);
|
|
701
|
+
case 'H':
|
|
702
|
+
return Math.trunc(ms / 3_600_000);
|
|
703
|
+
case 'MIN':
|
|
704
|
+
return Math.trunc(ms / 60_000);
|
|
705
|
+
case 'S':
|
|
706
|
+
return Math.trunc(ms / 1000);
|
|
707
|
+
case 'M':
|
|
708
|
+
return ((end.getUTCFullYear() - start.getUTCFullYear()) * 12 +
|
|
709
|
+
(end.getUTCMonth() - start.getUTCMonth()) -
|
|
710
|
+
(end.getUTCDate() < start.getUTCDate() ? 1 : 0));
|
|
711
|
+
case 'Y': {
|
|
712
|
+
let years = end.getUTCFullYear() - start.getUTCFullYear();
|
|
713
|
+
if (end.getUTCMonth() < start.getUTCMonth() ||
|
|
714
|
+
(end.getUTCMonth() === start.getUTCMonth() && end.getUTCDate() < start.getUTCDate())) {
|
|
715
|
+
years--;
|
|
716
|
+
}
|
|
717
|
+
return years;
|
|
718
|
+
}
|
|
719
|
+
default:
|
|
720
|
+
return makeError('#NUM!', `Unknown unit: ${unit}`);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
/**
|
|
724
|
+
* Map of function names (uppercase) to implementations. Aliases like
|
|
725
|
+
* `AVG → AVERAGE` and `DATEDIF → DATEDIFF` map to the same function
|
|
726
|
+
* reference.
|
|
727
|
+
*/
|
|
728
|
+
export const BUILTIN_FUNCTIONS = Object.freeze({
|
|
729
|
+
SUM,
|
|
730
|
+
AVERAGE,
|
|
731
|
+
AVG: AVERAGE,
|
|
732
|
+
MIN,
|
|
733
|
+
MAX,
|
|
734
|
+
COUNT,
|
|
735
|
+
COUNTA,
|
|
736
|
+
COUNTIF,
|
|
737
|
+
SUMIF,
|
|
738
|
+
IF,
|
|
739
|
+
AND,
|
|
740
|
+
OR,
|
|
741
|
+
NOT,
|
|
742
|
+
IFS,
|
|
743
|
+
SWITCH,
|
|
744
|
+
IFERROR,
|
|
745
|
+
ISERROR,
|
|
746
|
+
ISNUMBER,
|
|
747
|
+
ISBLANK,
|
|
748
|
+
CONCAT,
|
|
749
|
+
CONCATENATE: CONCAT,
|
|
750
|
+
LEFT,
|
|
751
|
+
RIGHT,
|
|
752
|
+
MID,
|
|
753
|
+
LEN,
|
|
754
|
+
TRIM,
|
|
755
|
+
UPPER,
|
|
756
|
+
LOWER,
|
|
757
|
+
SUBSTITUTE,
|
|
758
|
+
ROUND,
|
|
759
|
+
ABS,
|
|
760
|
+
MOD,
|
|
761
|
+
POWER,
|
|
762
|
+
SQRT,
|
|
763
|
+
INT,
|
|
764
|
+
CEILING,
|
|
765
|
+
FLOOR,
|
|
766
|
+
DATE,
|
|
767
|
+
TODAY,
|
|
768
|
+
NOW,
|
|
769
|
+
YEAR,
|
|
770
|
+
MONTH,
|
|
771
|
+
DAY,
|
|
772
|
+
DATEDIFF,
|
|
773
|
+
DATEDIF: DATEDIFF,
|
|
774
|
+
});
|