@pixerate/editor 0.21.0 → 0.22.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 +20 -11
- package/dist/agent/index.cjs +8 -8
- package/dist/agent/index.d.cts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +2 -2
- package/dist/canvas/index.cjs +1269 -13
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +432 -6
- package/dist/canvas/index.d.ts +432 -6
- package/dist/canvas/index.js +1264 -8
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-U6NDQFOK.cjs → chunk-6M3BCTBD.cjs} +427 -138
- package/dist/chunk-6M3BCTBD.cjs.map +1 -0
- package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
- package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
- package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
- package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
- package/dist/{chunk-A7DKIA72.js → chunk-DENSCKFC.js} +642 -69
- package/dist/chunk-DENSCKFC.js.map +1 -0
- package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
- package/dist/chunk-GVSMRPHB.cjs.map +1 -0
- package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
- package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
- package/dist/chunk-MJZBNWG7.js.map +1 -0
- package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
- package/dist/chunk-QGHGJ56O.js.map +1 -0
- package/dist/{chunk-GVW4P7BG.js → chunk-RU52I7QO.js} +305 -16
- package/dist/chunk-RU52I7QO.js.map +1 -0
- package/dist/{chunk-JYRIOFEN.cjs → chunk-SAZRENEZ.cjs} +647 -74
- package/dist/chunk-SAZRENEZ.cjs.map +1 -0
- package/dist/{controller-OUdxPY-X.d.ts → controller-B7GIybc4.d.cts} +139 -7
- package/dist/{controller-OUdxPY-X.d.cts → controller-B7GIybc4.d.ts} +139 -7
- package/dist/extensions/index.cjs +14 -4
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +3 -3
- package/dist/extensions/index.d.ts +3 -3
- package/dist/extensions/index.js +13 -3
- package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
- package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
- package/dist/index.cjs +250 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -8
- package/dist/index.d.ts +294 -8
- package/dist/index.js +241 -7
- package/dist/index.js.map +1 -1
- package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
- package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
- package/dist/serializers/index.cjs +3 -3
- package/dist/serializers/index.d.cts +1 -1
- package/dist/serializers/index.d.ts +1 -1
- package/dist/serializers/index.js +2 -2
- package/dist/spreadsheet/index.cjs +7 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +33 -4
- package/dist/spreadsheet/index.d.ts +33 -4
- package/dist/spreadsheet/index.js +6 -2
- package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
- package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
- package/dist/{types-DMe0vFB1.d.cts → types-CJ4U-HHI.d.cts} +1 -1
- package/dist/{types-DMe0vFB1.d.ts → types-CJ4U-HHI.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3TYMNULT.js.map +0 -1
- package/dist/chunk-4QPLEUL2.cjs.map +0 -1
- package/dist/chunk-A7DKIA72.js.map +0 -1
- package/dist/chunk-CYFJCKC4.js.map +0 -1
- package/dist/chunk-GVW4P7BG.js.map +0 -1
- package/dist/chunk-JYRIOFEN.cjs.map +0 -1
- package/dist/chunk-TVRC4KVT.cjs.map +0 -1
- package/dist/chunk-U6NDQFOK.cjs.map +0 -1
- /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
|
@@ -53,7 +53,7 @@ function tokenizeFormula(formula) {
|
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
55
|
if (char === "#") {
|
|
56
|
-
const match = /^#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|CYCLE!|ERROR!)/.exec(input.slice(i));
|
|
56
|
+
const match = /^#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|CYCLE!|ERROR!|SPILL!|CALC!)/.exec(input.slice(i));
|
|
57
57
|
if (!match) {
|
|
58
58
|
throw new Error(`Unexpected character '#' at position ${i + offset}`);
|
|
59
59
|
}
|
|
@@ -104,7 +104,7 @@ function tokenizeFormula(formula) {
|
|
|
104
104
|
i += 2;
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
107
|
-
if (["+", "-", "*", "/", "^", "&", "=", "<", ">"].includes(char)) {
|
|
107
|
+
if (["+", "-", "*", "/", "^", "&", "=", "<", ">", "@"].includes(char)) {
|
|
108
108
|
tokens.push({ type: "OPERATOR", value: char, position: i + offset });
|
|
109
109
|
i++;
|
|
110
110
|
continue;
|
|
@@ -144,6 +144,11 @@ function tokenizeFormula(formula) {
|
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
146
|
if (/^\$?[A-Za-z]+\$?[0-9]+$/.test(rawWord)) {
|
|
147
|
+
if (input[i] === "#") {
|
|
148
|
+
i++;
|
|
149
|
+
tokens.push({ type: "SPILL_REF", value: upper.replace(/\$/g, ""), position: start + offset });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
147
152
|
tokens.push({
|
|
148
153
|
type: "CELL_REF",
|
|
149
154
|
value: upper.replace(/\$/g, ""),
|
|
@@ -265,7 +270,7 @@ var FormulaParser = (_class = class {
|
|
|
265
270
|
return expr;
|
|
266
271
|
}
|
|
267
272
|
unary() {
|
|
268
|
-
if (this.matchOperator("-", "+")) {
|
|
273
|
+
if (this.matchOperator("-", "+", "@")) {
|
|
269
274
|
const op = this.previous().value;
|
|
270
275
|
const argument = this.unary();
|
|
271
276
|
return { type: "UnaryOp", op, argument };
|
|
@@ -304,6 +309,11 @@ var FormulaParser = (_class = class {
|
|
|
304
309
|
this.advance();
|
|
305
310
|
return parseRange(token.value);
|
|
306
311
|
}
|
|
312
|
+
if (token.type === "SPILL_REF") {
|
|
313
|
+
this.advance();
|
|
314
|
+
const parsed = parseCellReference(token.value);
|
|
315
|
+
return { type: "SpillRef", ref: token.value, col: parsed.col, row: parsed.row };
|
|
316
|
+
}
|
|
307
317
|
if (token.type === "PROPERTY_REF") {
|
|
308
318
|
this.advance();
|
|
309
319
|
return {
|
|
@@ -329,7 +339,7 @@ var FormulaParser = (_class = class {
|
|
|
329
339
|
args
|
|
330
340
|
};
|
|
331
341
|
}
|
|
332
|
-
return { type: "
|
|
342
|
+
return { type: "Name", name };
|
|
333
343
|
}
|
|
334
344
|
if (this.match("LPAREN")) {
|
|
335
345
|
const expr = this.expression();
|
|
@@ -391,7 +401,9 @@ var FORMULA_ERRORS = /* @__PURE__ */ new Set([
|
|
|
391
401
|
"#N/A",
|
|
392
402
|
"#NUM!",
|
|
393
403
|
"#CYCLE!",
|
|
394
|
-
"#ERROR!"
|
|
404
|
+
"#ERROR!",
|
|
405
|
+
"#SPILL!",
|
|
406
|
+
"#CALC!"
|
|
395
407
|
]);
|
|
396
408
|
function isFormulaError(value) {
|
|
397
409
|
return typeof value === "string" && FORMULA_ERRORS.has(value);
|
|
@@ -430,6 +442,11 @@ function normalizeValue(value) {
|
|
|
430
442
|
return num === null ? value : num;
|
|
431
443
|
}
|
|
432
444
|
function compareValues(left, right) {
|
|
445
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
446
|
+
const a = isoDateTime(left);
|
|
447
|
+
const b = a === null ? null : isoDateTime(right);
|
|
448
|
+
if (a !== null && b !== null) return a - b;
|
|
449
|
+
}
|
|
433
450
|
const rank = (v) => typeof v === "boolean" ? 2 : typeof v === "string" ? 1 : 0;
|
|
434
451
|
const l = isBlank(left) ? typeof right === "string" ? "" : 0 : left;
|
|
435
452
|
const r = isBlank(right) ? typeof l === "string" ? "" : 0 : right;
|
|
@@ -505,6 +522,20 @@ function toDate(value) {
|
|
|
505
522
|
}
|
|
506
523
|
return null;
|
|
507
524
|
}
|
|
525
|
+
var ISO_DATE = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
526
|
+
function parseIsoDate(value) {
|
|
527
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 19) return null;
|
|
528
|
+
const iso = ISO_DATE.exec(value.trim());
|
|
529
|
+
if (!iso) return null;
|
|
530
|
+
const [, y, m, d, hh = "0", mm = "0", ss = "0"] = iso;
|
|
531
|
+
const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));
|
|
532
|
+
return date.getMonth() === Number(m) - 1 && date.getDate() === Number(d) ? date : null;
|
|
533
|
+
}
|
|
534
|
+
var isoDateHasTime = (value) => /[ T]\d/.test(value.trim());
|
|
535
|
+
function isoDateTime(value) {
|
|
536
|
+
if (value.length < 8 || value.charCodeAt(4) !== 45) return null;
|
|
537
|
+
return _nullishCoalesce(_optionalChain([parseIsoDate, 'call', _2 => _2(value), 'optionalAccess', _3 => _3.getTime, 'call', _4 => _4()]), () => ( null));
|
|
538
|
+
}
|
|
508
539
|
function dateToSerial(date) {
|
|
509
540
|
const utc = Date.UTC(
|
|
510
541
|
date.getFullYear(),
|
|
@@ -762,8 +793,8 @@ function formatDate(date, format) {
|
|
|
762
793
|
return lower.length >= 2 ? pad2(date.getSeconds()) : String(date.getSeconds());
|
|
763
794
|
}
|
|
764
795
|
const position = codes.findIndex((c) => c.i === index);
|
|
765
|
-
const prev = _nullishCoalesce(_optionalChain([codes, 'access',
|
|
766
|
-
const next = _nullishCoalesce(_optionalChain([codes, 'access',
|
|
796
|
+
const prev = _nullishCoalesce(_optionalChain([codes, 'access', _5 => _5[position - 1], 'optionalAccess', _6 => _6.text, 'access', _7 => _7.toLowerCase, 'call', _8 => _8()]), () => ( ""));
|
|
797
|
+
const next = _nullishCoalesce(_optionalChain([codes, 'access', _9 => _9[position + 1], 'optionalAccess', _10 => _10.text, 'access', _11 => _11.toLowerCase, 'call', _12 => _12()]), () => ( ""));
|
|
767
798
|
if (lower.length <= 2 && (prev.startsWith("h") || next.startsWith("s"))) {
|
|
768
799
|
return lower.length === 2 ? pad2(date.getMinutes()) : String(date.getMinutes());
|
|
769
800
|
}
|
|
@@ -1606,11 +1637,11 @@ var TEXT_FUNCTIONS = [
|
|
|
1606
1637
|
{
|
|
1607
1638
|
name: "TRIM",
|
|
1608
1639
|
category: "text",
|
|
1609
|
-
description: "Removes leading and trailing spaces.",
|
|
1640
|
+
description: "Removes leading and trailing spaces and collapses runs of inner spaces to one, like Excel.",
|
|
1610
1641
|
args: [TEXT],
|
|
1611
1642
|
evaluate: (ctx) => {
|
|
1612
1643
|
const text = ctx.text(0);
|
|
1613
|
-
return failed(text) ? text : text.trim();
|
|
1644
|
+
return failed(text) ? text : text.trim().replace(/ {2,}/g, " ");
|
|
1614
1645
|
}
|
|
1615
1646
|
},
|
|
1616
1647
|
{
|
|
@@ -2241,6 +2272,335 @@ var INFORMATION_FUNCTIONS = [
|
|
|
2241
2272
|
}
|
|
2242
2273
|
];
|
|
2243
2274
|
|
|
2275
|
+
// src/spreadsheet/formula/functions/array.ts
|
|
2276
|
+
var MAX_ARRAY_CELLS = 1e6;
|
|
2277
|
+
function rangeArg(ctx, index) {
|
|
2278
|
+
const value = ctx.value(index);
|
|
2279
|
+
if (Array.isArray(value)) return toFormulaRange(value);
|
|
2280
|
+
if (isFormulaError(value)) return value;
|
|
2281
|
+
return { rows: 1, cols: 1, values: [value] };
|
|
2282
|
+
}
|
|
2283
|
+
function fromRows(rows, cols) {
|
|
2284
|
+
const out = [];
|
|
2285
|
+
for (let c = 0; c < cols; c++) {
|
|
2286
|
+
for (const row of rows) out.push(row[c]);
|
|
2287
|
+
}
|
|
2288
|
+
return withRangeShape(out, rows.length, cols);
|
|
2289
|
+
}
|
|
2290
|
+
var rowOf = (range, r) => {
|
|
2291
|
+
const row = [];
|
|
2292
|
+
for (let c = 0; c < range.cols; c++) row.push(rangeGet(range, r, c));
|
|
2293
|
+
return row;
|
|
2294
|
+
};
|
|
2295
|
+
var colOf = (range, c) => {
|
|
2296
|
+
const col = [];
|
|
2297
|
+
for (let r = 0; r < range.rows; r++) col.push(rangeGet(range, r, c));
|
|
2298
|
+
return col;
|
|
2299
|
+
};
|
|
2300
|
+
var linesOf = (range, byCol) => byCol ? Array.from({ length: range.cols }, (_, c) => colOf(range, c)) : Array.from({ length: range.rows }, (_, r) => rowOf(range, r));
|
|
2301
|
+
function fromLines(lines, byCol, width) {
|
|
2302
|
+
if (!byCol) return fromRows(lines, width);
|
|
2303
|
+
const out = [];
|
|
2304
|
+
for (const line of lines) for (const v of line) out.push(v);
|
|
2305
|
+
return withRangeShape(out, width, lines.length);
|
|
2306
|
+
}
|
|
2307
|
+
function compareForSort(a, b, order) {
|
|
2308
|
+
const blankA = isBlank(a);
|
|
2309
|
+
const blankB = isBlank(b);
|
|
2310
|
+
if (blankA || blankB) return blankA === blankB ? 0 : blankA ? 1 : -1;
|
|
2311
|
+
return compareValues(normalizeValue(a), normalizeValue(b)) * order;
|
|
2312
|
+
}
|
|
2313
|
+
var sortOrder = (ctx, index) => {
|
|
2314
|
+
const order = ctx.integer(index, 1);
|
|
2315
|
+
if (failed(order)) return order;
|
|
2316
|
+
return order === 1 || order === -1 ? order : ctx.argError(index, "#VALUE!", "must be 1 (ascending) or -1 (descending).");
|
|
2317
|
+
};
|
|
2318
|
+
function includeFlag(value) {
|
|
2319
|
+
if (isFormulaError(value)) return value;
|
|
2320
|
+
const flag = toBoolean(value);
|
|
2321
|
+
return isFormulaError(flag) ? "#VALUE!" : flag;
|
|
2322
|
+
}
|
|
2323
|
+
var ARRAY_FUNCTIONS = [
|
|
2324
|
+
{
|
|
2325
|
+
name: "FILTER",
|
|
2326
|
+
category: "lookup",
|
|
2327
|
+
description: "Returns the rows (or columns) of an array whose include value is TRUE. The result spills.",
|
|
2328
|
+
args: [
|
|
2329
|
+
arg("array", "The range or array to filter.", "range"),
|
|
2330
|
+
arg("include", "TRUE/FALSE for each row (a column as tall as array) or each column (a row as wide as array)."),
|
|
2331
|
+
optional("if_empty", "Returned when nothing matches (default #CALC!).")
|
|
2332
|
+
],
|
|
2333
|
+
evaluate: (ctx) => {
|
|
2334
|
+
const range = rangeArg(ctx, 0);
|
|
2335
|
+
if (failed(range)) return range;
|
|
2336
|
+
const include = rangeArg(ctx, 1);
|
|
2337
|
+
if (failed(include)) return include;
|
|
2338
|
+
let byCol;
|
|
2339
|
+
if (include.cols === 1 && include.rows === range.rows) byCol = false;
|
|
2340
|
+
else if (include.rows === 1 && include.cols === range.cols) byCol = true;
|
|
2341
|
+
else {
|
|
2342
|
+
return ctx.argError(1, "#VALUE!", "must be a single column as tall as argument 1 (or a single row as wide).");
|
|
2343
|
+
}
|
|
2344
|
+
const lines = linesOf(range, byCol);
|
|
2345
|
+
const kept = [];
|
|
2346
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2347
|
+
const flag = includeFlag(include.values[i]);
|
|
2348
|
+
if (typeof flag === "string") {
|
|
2349
|
+
return flag === "#VALUE!" ? ctx.argError(1, "#VALUE!", `has a value that isn't TRUE or FALSE (${include.values[i]}).`) : flag;
|
|
2350
|
+
}
|
|
2351
|
+
if (flag) kept.push(lines[i]);
|
|
2352
|
+
}
|
|
2353
|
+
if (kept.length === 0) {
|
|
2354
|
+
return ctx.has(2) ? ctx.value(2) : ctx.error("#CALC!", "FILTER found no matching values.");
|
|
2355
|
+
}
|
|
2356
|
+
return fromLines(kept, byCol, byCol ? range.rows : range.cols);
|
|
2357
|
+
}
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
name: "SORT",
|
|
2361
|
+
category: "lookup",
|
|
2362
|
+
description: "Sorts the rows (or columns) of an array by one of its columns (or rows). The result spills.",
|
|
2363
|
+
args: [
|
|
2364
|
+
arg("array", "The range or array to sort.", "range"),
|
|
2365
|
+
optional("sort_index", "The column (or row, with by_col) to sort by; 1 = first (default).", "number"),
|
|
2366
|
+
optional("sort_order", "1 = ascending (default), -1 = descending.", "number"),
|
|
2367
|
+
optional("by_col", "TRUE sorts columns by a row; FALSE (default) sorts rows by a column.", "logical")
|
|
2368
|
+
],
|
|
2369
|
+
evaluate: (ctx) => {
|
|
2370
|
+
const range = rangeArg(ctx, 0);
|
|
2371
|
+
if (failed(range)) return range;
|
|
2372
|
+
const index = ctx.integer(1, 1);
|
|
2373
|
+
if (failed(index)) return index;
|
|
2374
|
+
const order = sortOrder(ctx, 2);
|
|
2375
|
+
if (failed(order)) return order;
|
|
2376
|
+
const byCol = ctx.boolean(3, false);
|
|
2377
|
+
if (failed(byCol)) return byCol;
|
|
2378
|
+
const width = byCol ? range.rows : range.cols;
|
|
2379
|
+
if (index < 1 || index > width) {
|
|
2380
|
+
return ctx.argError(1, "#VALUE!", `must be between 1 and ${width}.`);
|
|
2381
|
+
}
|
|
2382
|
+
const lines = linesOf(range, byCol).map((line, position) => ({ line, position })).sort((a, b) => compareForSort(a.line[index - 1], b.line[index - 1], order) || a.position - b.position).map((entry) => entry.line);
|
|
2383
|
+
return fromLines(lines, byCol, width);
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
name: "SORTBY",
|
|
2388
|
+
category: "lookup",
|
|
2389
|
+
description: "Sorts an array by the values in other ranges (one value per row or column). The result spills.",
|
|
2390
|
+
args: [
|
|
2391
|
+
arg("array", "The range or array to sort.", "range"),
|
|
2392
|
+
arg("by_array", "A column as tall as array (or a row as wide) to sort by.", "range"),
|
|
2393
|
+
optional("sort_order", "1 = ascending (default), -1 = descending.", "number"),
|
|
2394
|
+
arg("by_array", "Another range to sort by, for ties.", "range", { repeating: true, optional: true }),
|
|
2395
|
+
arg("sort_order", "Its order: 1 = ascending (default), -1 = descending.", "number", { repeating: true, optional: true })
|
|
2396
|
+
],
|
|
2397
|
+
allowTrailingArgument: true,
|
|
2398
|
+
evaluate: (ctx) => {
|
|
2399
|
+
const range = rangeArg(ctx, 0);
|
|
2400
|
+
if (failed(range)) return range;
|
|
2401
|
+
const keys = [];
|
|
2402
|
+
let byCol = null;
|
|
2403
|
+
for (let i = 1; i < ctx.args.length; i += 2) {
|
|
2404
|
+
const by = rangeArg(ctx, i);
|
|
2405
|
+
if (failed(by)) return by;
|
|
2406
|
+
let keyByCol;
|
|
2407
|
+
if (by.cols === 1 && by.rows === range.rows) keyByCol = false;
|
|
2408
|
+
else if (by.rows === 1 && by.cols === range.cols) keyByCol = true;
|
|
2409
|
+
else {
|
|
2410
|
+
return ctx.argError(i, "#VALUE!", "must be a single column as tall as argument 1 (or a single row as wide).");
|
|
2411
|
+
}
|
|
2412
|
+
if (byCol !== null && byCol !== keyByCol) {
|
|
2413
|
+
return ctx.argError(i, "#VALUE!", "must have the same orientation as the other sort ranges.");
|
|
2414
|
+
}
|
|
2415
|
+
byCol = keyByCol;
|
|
2416
|
+
const order = sortOrder(ctx, i + 1);
|
|
2417
|
+
if (failed(order)) return order;
|
|
2418
|
+
keys.push({ values: by.values, order });
|
|
2419
|
+
}
|
|
2420
|
+
const sortByCol = _nullishCoalesce(byCol, () => ( false));
|
|
2421
|
+
const lines = linesOf(range, sortByCol).map((line, position) => ({ line, position })).sort((a, b) => {
|
|
2422
|
+
for (const key of keys) {
|
|
2423
|
+
const result = compareForSort(key.values[a.position], key.values[b.position], key.order);
|
|
2424
|
+
if (result !== 0) return result;
|
|
2425
|
+
}
|
|
2426
|
+
return a.position - b.position;
|
|
2427
|
+
}).map((entry) => entry.line);
|
|
2428
|
+
return fromLines(lines, sortByCol, sortByCol ? range.rows : range.cols);
|
|
2429
|
+
}
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
name: "UNIQUE",
|
|
2433
|
+
category: "lookup",
|
|
2434
|
+
description: "Returns the distinct rows (or columns) of an array, in order of first appearance. The result spills.",
|
|
2435
|
+
args: [
|
|
2436
|
+
arg("array", "The range or array.", "range"),
|
|
2437
|
+
optional("by_col", "TRUE compares columns; FALSE (default) compares rows.", "logical"),
|
|
2438
|
+
optional("exactly_once", "TRUE returns only the rows that appear exactly once (default FALSE).", "logical")
|
|
2439
|
+
],
|
|
2440
|
+
evaluate: (ctx) => {
|
|
2441
|
+
const range = rangeArg(ctx, 0);
|
|
2442
|
+
if (failed(range)) return range;
|
|
2443
|
+
const byCol = ctx.boolean(1, false);
|
|
2444
|
+
if (failed(byCol)) return byCol;
|
|
2445
|
+
const exactlyOnce = ctx.boolean(2, false);
|
|
2446
|
+
if (failed(exactlyOnce)) return exactlyOnce;
|
|
2447
|
+
const keyOf = (line) => JSON.stringify(
|
|
2448
|
+
line.map((v) => {
|
|
2449
|
+
const n = normalizeValue(v);
|
|
2450
|
+
return typeof n === "string" ? `s:${n.toLowerCase()}` : isBlank(n) ? "blank" : `${typeof n}:${String(n)}`;
|
|
2451
|
+
})
|
|
2452
|
+
);
|
|
2453
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2454
|
+
for (const line of linesOf(range, byCol)) {
|
|
2455
|
+
const key = keyOf(line);
|
|
2456
|
+
const entry = counts.get(key);
|
|
2457
|
+
if (entry) entry.count++;
|
|
2458
|
+
else counts.set(key, { line, count: 1 });
|
|
2459
|
+
}
|
|
2460
|
+
const lines = Array.from(counts.values()).filter((entry) => !exactlyOnce || entry.count === 1).map((entry) => entry.line);
|
|
2461
|
+
if (lines.length === 0) return ctx.error("#CALC!", "UNIQUE found no values that appear exactly once.");
|
|
2462
|
+
return fromLines(lines, byCol, byCol ? range.rows : range.cols);
|
|
2463
|
+
}
|
|
2464
|
+
},
|
|
2465
|
+
{
|
|
2466
|
+
name: "SEQUENCE",
|
|
2467
|
+
category: "math",
|
|
2468
|
+
description: "Returns a grid of sequential numbers, filled row by row. The result spills.",
|
|
2469
|
+
args: [
|
|
2470
|
+
arg("rows", "The number of rows.", "number"),
|
|
2471
|
+
optional("columns", "The number of columns (default 1).", "number"),
|
|
2472
|
+
optional("start", "The first number (default 1).", "number"),
|
|
2473
|
+
optional("step", "The increment (default 1).", "number")
|
|
2474
|
+
],
|
|
2475
|
+
evaluate: (ctx) => {
|
|
2476
|
+
const rows = ctx.integer(0);
|
|
2477
|
+
if (failed(rows)) return rows;
|
|
2478
|
+
const cols = ctx.integer(1, 1);
|
|
2479
|
+
if (failed(cols)) return cols;
|
|
2480
|
+
const start = ctx.number(2, 1);
|
|
2481
|
+
if (failed(start)) return start;
|
|
2482
|
+
const step = ctx.number(3, 1);
|
|
2483
|
+
if (failed(step)) return step;
|
|
2484
|
+
if (rows < 1 || cols < 1) return ctx.error("#CALC!", "SEQUENCE needs at least one row and one column.");
|
|
2485
|
+
if (rows * cols > MAX_ARRAY_CELLS) return ctx.error("#NUM!", `SEQUENCE can return at most ${MAX_ARRAY_CELLS} values.`);
|
|
2486
|
+
const out = [];
|
|
2487
|
+
for (let c = 0; c < cols; c++) {
|
|
2488
|
+
for (let r = 0; r < rows; r++) out.push(start + step * (r * cols + c));
|
|
2489
|
+
}
|
|
2490
|
+
return rows * cols === 1 ? out[0] : withRangeShape(out, rows, cols);
|
|
2491
|
+
}
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
name: "TRANSPOSE",
|
|
2495
|
+
category: "lookup",
|
|
2496
|
+
description: "Swaps the rows and columns of an array. The result spills.",
|
|
2497
|
+
args: [arg("array", "The range or array to transpose.", "range")],
|
|
2498
|
+
evaluate: (ctx) => {
|
|
2499
|
+
const range = rangeArg(ctx, 0);
|
|
2500
|
+
if (failed(range)) return range;
|
|
2501
|
+
if (range.rows * range.cols === 1) return range.values[0];
|
|
2502
|
+
const out = [];
|
|
2503
|
+
for (let r = 0; r < range.rows; r++) {
|
|
2504
|
+
for (let c = 0; c < range.cols; c++) out.push(rangeGet(range, r, c));
|
|
2505
|
+
}
|
|
2506
|
+
return withRangeShape(out, range.cols, range.rows);
|
|
2507
|
+
}
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
name: "RANDARRAY",
|
|
2511
|
+
category: "math",
|
|
2512
|
+
description: "Returns a grid of random numbers. Changes on every recalculation. The result spills.",
|
|
2513
|
+
args: [
|
|
2514
|
+
optional("rows", "The number of rows (default 1).", "number"),
|
|
2515
|
+
optional("columns", "The number of columns (default 1).", "number"),
|
|
2516
|
+
optional("min", "The smallest number (default 0).", "number"),
|
|
2517
|
+
optional("max", "The largest number (default 1).", "number"),
|
|
2518
|
+
optional("integer", "TRUE returns whole numbers; FALSE (default) decimals.", "logical")
|
|
2519
|
+
],
|
|
2520
|
+
volatile: true,
|
|
2521
|
+
evaluate: (ctx) => {
|
|
2522
|
+
const rows = ctx.integer(0, 1);
|
|
2523
|
+
if (failed(rows)) return rows;
|
|
2524
|
+
const cols = ctx.integer(1, 1);
|
|
2525
|
+
if (failed(cols)) return cols;
|
|
2526
|
+
const min2 = ctx.number(2, 0);
|
|
2527
|
+
if (failed(min2)) return min2;
|
|
2528
|
+
const max2 = ctx.number(3, 1);
|
|
2529
|
+
if (failed(max2)) return max2;
|
|
2530
|
+
const integer = ctx.boolean(4, false);
|
|
2531
|
+
if (failed(integer)) return integer;
|
|
2532
|
+
if (rows < 1 || cols < 1) return ctx.error("#CALC!", "RANDARRAY needs at least one row and one column.");
|
|
2533
|
+
if (rows * cols > MAX_ARRAY_CELLS) return ctx.error("#NUM!", `RANDARRAY can return at most ${MAX_ARRAY_CELLS} values.`);
|
|
2534
|
+
if (min2 > max2) return ctx.argError(2, "#VALUE!", "can't be larger than max.");
|
|
2535
|
+
if (integer && (!Number.isInteger(min2) || !Number.isInteger(max2))) {
|
|
2536
|
+
return ctx.error("#VALUE!", "RANDARRAY needs whole min and max values when integer is TRUE.");
|
|
2537
|
+
}
|
|
2538
|
+
const next = () => integer ? min2 + Math.floor(Math.random() * (max2 - min2 + 1)) : min2 + Math.random() * (max2 - min2);
|
|
2539
|
+
const out = [];
|
|
2540
|
+
for (let i = 0; i < rows * cols; i++) out.push(next());
|
|
2541
|
+
return rows * cols === 1 ? out[0] : withRangeShape(out, rows, cols);
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
// Evaluated by the evaluator itself (it defines names); listed here for
|
|
2546
|
+
// autocomplete, signatures and argument-count checks.
|
|
2547
|
+
name: "LET",
|
|
2548
|
+
category: "logical",
|
|
2549
|
+
description: "Names the results of calculations so a formula can reuse them: LET(x, A1*2, x+x). The last argument is the calculation that uses the names.",
|
|
2550
|
+
args: [
|
|
2551
|
+
repeating("name", "A name to define (letters, not a cell reference)."),
|
|
2552
|
+
repeating("name_value", "The value of the name.")
|
|
2553
|
+
],
|
|
2554
|
+
allowTrailingArgument: true,
|
|
2555
|
+
evaluate: (ctx) => ctx.error("#ERROR!", "LET must be evaluated by the formula evaluator.")
|
|
2556
|
+
}
|
|
2557
|
+
];
|
|
2558
|
+
var ELEMENTWISE_BUILTINS = {
|
|
2559
|
+
ABS: true,
|
|
2560
|
+
ROUND: true,
|
|
2561
|
+
ROUNDUP: true,
|
|
2562
|
+
ROUNDDOWN: true,
|
|
2563
|
+
INT: true,
|
|
2564
|
+
MOD: true,
|
|
2565
|
+
POWER: true,
|
|
2566
|
+
SQRT: true,
|
|
2567
|
+
SIGN: true,
|
|
2568
|
+
CEILING: true,
|
|
2569
|
+
FLOOR: true,
|
|
2570
|
+
LEN: true,
|
|
2571
|
+
UPPER: true,
|
|
2572
|
+
LOWER: true,
|
|
2573
|
+
TRIM: true,
|
|
2574
|
+
PROPER: true,
|
|
2575
|
+
LEFT: true,
|
|
2576
|
+
RIGHT: true,
|
|
2577
|
+
MID: true,
|
|
2578
|
+
FIND: true,
|
|
2579
|
+
SEARCH: true,
|
|
2580
|
+
SUBSTITUTE: true,
|
|
2581
|
+
REPLACE: true,
|
|
2582
|
+
REPT: true,
|
|
2583
|
+
EXACT: true,
|
|
2584
|
+
VALUE: true,
|
|
2585
|
+
TEXT: true,
|
|
2586
|
+
YEAR: true,
|
|
2587
|
+
MONTH: true,
|
|
2588
|
+
DAY: true,
|
|
2589
|
+
WEEKDAY: true,
|
|
2590
|
+
EDATE: true,
|
|
2591
|
+
EOMONTH: true,
|
|
2592
|
+
DATE: true,
|
|
2593
|
+
NOT: true,
|
|
2594
|
+
ISBLANK: true,
|
|
2595
|
+
ISNUMBER: true,
|
|
2596
|
+
ISTEXT: true,
|
|
2597
|
+
ISERROR: true,
|
|
2598
|
+
ISNA: true,
|
|
2599
|
+
IF: [0],
|
|
2600
|
+
IFERROR: [0],
|
|
2601
|
+
IFNA: [0]
|
|
2602
|
+
};
|
|
2603
|
+
|
|
2244
2604
|
// src/spreadsheet/formula/functions/index.ts
|
|
2245
2605
|
var BUILTIN_FORMULA_FUNCTIONS = [
|
|
2246
2606
|
...MATH_FUNCTIONS,
|
|
@@ -2250,8 +2610,12 @@ var BUILTIN_FORMULA_FUNCTIONS = [
|
|
|
2250
2610
|
...TEXT_FUNCTIONS,
|
|
2251
2611
|
...LOOKUP_FUNCTIONS,
|
|
2252
2612
|
...DATE_FUNCTIONS,
|
|
2253
|
-
...INFORMATION_FUNCTIONS
|
|
2254
|
-
|
|
2613
|
+
...INFORMATION_FUNCTIONS,
|
|
2614
|
+
...ARRAY_FUNCTIONS
|
|
2615
|
+
].map((def) => {
|
|
2616
|
+
const elementwise = ELEMENTWISE_BUILTINS[def.name];
|
|
2617
|
+
return elementwise && def.elementwise === void 0 ? { ...def, elementwise } : def;
|
|
2618
|
+
});
|
|
2255
2619
|
|
|
2256
2620
|
// src/spreadsheet/formula/registry.ts
|
|
2257
2621
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -2373,16 +2737,43 @@ var DEFAULT_MESSAGES = {
|
|
|
2373
2737
|
"#N/A": "A value is not available.",
|
|
2374
2738
|
"#NUM!": "The formula produced an invalid number.",
|
|
2375
2739
|
"#CYCLE!": "This formula is part of a circular reference.",
|
|
2376
|
-
"#ERROR!": "The formula could not be evaluated."
|
|
2740
|
+
"#ERROR!": "The formula could not be evaluated.",
|
|
2741
|
+
"#SPILL!": "The formula's array result can't spill into the cells it needs.",
|
|
2742
|
+
"#CALC!": "The formula produced an empty array."
|
|
2377
2743
|
};
|
|
2378
2744
|
function describeFormulaError(code) {
|
|
2379
2745
|
return _nullishCoalesce(DEFAULT_MESSAGES[code], () => ( "The formula could not be evaluated."));
|
|
2380
2746
|
}
|
|
2381
2747
|
var quote = (value) => typeof value === "string" ? `"${value}"` : String(value);
|
|
2748
|
+
var MAX_FORMULA_ARRAY_SIZE = 1e6;
|
|
2749
|
+
function shapeOf(value) {
|
|
2750
|
+
if (!Array.isArray(value)) return { rows: 1, cols: 1 };
|
|
2751
|
+
const range = toFormulaRange(value);
|
|
2752
|
+
return { rows: range.rows, cols: range.cols };
|
|
2753
|
+
}
|
|
2754
|
+
function broadcastGet(value, row, col) {
|
|
2755
|
+
if (!Array.isArray(value)) return value;
|
|
2756
|
+
const range = toFormulaRange(value);
|
|
2757
|
+
const r = range.rows === 1 ? 0 : row;
|
|
2758
|
+
const c = range.cols === 1 ? 0 : col;
|
|
2759
|
+
if (r >= range.rows || c >= range.cols) return "#N/A";
|
|
2760
|
+
return range.values[c * range.rows + r];
|
|
2761
|
+
}
|
|
2762
|
+
function buildArray(rows, cols, fn) {
|
|
2763
|
+
const out = [];
|
|
2764
|
+
for (let c = 0; c < cols; c++) {
|
|
2765
|
+
for (let r = 0; r < rows; r++) out.push(fn(r, c));
|
|
2766
|
+
}
|
|
2767
|
+
return withRangeShape(out, rows, cols);
|
|
2768
|
+
}
|
|
2769
|
+
var isMultiArray = (value) => Array.isArray(value) && value.length > 1;
|
|
2770
|
+
var firstValue = (value) => Array.isArray(value) ? value.length ? value[0] : "" : value;
|
|
2382
2771
|
var FormulaEvaluator = (_class2 = class {
|
|
2383
2772
|
|
|
2384
2773
|
__init3() {this.diagnostics = []}
|
|
2385
|
-
|
|
2774
|
+
// LET variables, innermost scope last.
|
|
2775
|
+
__init4() {this.scopes = []}
|
|
2776
|
+
constructor(context = {}) {;_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);
|
|
2386
2777
|
this.context = context;
|
|
2387
2778
|
}
|
|
2388
2779
|
/**
|
|
@@ -2395,25 +2786,39 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2395
2786
|
}
|
|
2396
2787
|
/**
|
|
2397
2788
|
* Evaluates a formula and explains errors. Never throws: malformed formulas
|
|
2398
|
-
* return `#ERROR!` with the parser's message. A result
|
|
2399
|
-
* (
|
|
2789
|
+
* return `#ERROR!` with the parser's message. A result with several values
|
|
2790
|
+
* (a range or a dynamic array) is returned in `array`, with its top-left
|
|
2791
|
+
* value as `value`; a one-value array is just that value.
|
|
2400
2792
|
*/
|
|
2401
2793
|
evaluateWithDiagnostics(astOrFormula) {
|
|
2402
2794
|
const saved = this.diagnostics;
|
|
2795
|
+
const savedScopes = this.scopes;
|
|
2403
2796
|
this.diagnostics = [];
|
|
2797
|
+
this.scopes = [];
|
|
2404
2798
|
try {
|
|
2405
2799
|
let value = this.evaluate(astOrFormula);
|
|
2406
|
-
|
|
2800
|
+
let array;
|
|
2801
|
+
if (Array.isArray(value)) {
|
|
2802
|
+
if (value.length > 1) array = toFormulaRange(value);
|
|
2803
|
+
value = firstValue(value);
|
|
2804
|
+
}
|
|
2407
2805
|
if (isFormulaError(value)) {
|
|
2408
2806
|
const code = value;
|
|
2409
2807
|
const diagnostic = this.diagnostics.find((d) => d.code === code);
|
|
2410
|
-
|
|
2808
|
+
const result = {
|
|
2809
|
+
value,
|
|
2810
|
+
error: code,
|
|
2811
|
+
message: _nullishCoalesce(_optionalChain([diagnostic, 'optionalAccess', _13 => _13.message]), () => ( describeFormulaError(code)))
|
|
2812
|
+
};
|
|
2813
|
+
if (array) result.array = array;
|
|
2814
|
+
return result;
|
|
2411
2815
|
}
|
|
2412
|
-
return { value };
|
|
2816
|
+
return array ? { value, array } : { value };
|
|
2413
2817
|
} catch (err) {
|
|
2414
|
-
return { value: "#ERROR!", error: "#ERROR!", message: _optionalChain([err, 'optionalAccess',
|
|
2818
|
+
return { value: "#ERROR!", error: "#ERROR!", message: _optionalChain([err, 'optionalAccess', _14 => _14.message]) || "Formula error" };
|
|
2415
2819
|
} finally {
|
|
2416
2820
|
this.diagnostics = saved;
|
|
2821
|
+
this.scopes = savedScopes;
|
|
2417
2822
|
}
|
|
2418
2823
|
}
|
|
2419
2824
|
/** Records why an error happened and returns the error code. */
|
|
@@ -2442,6 +2847,14 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2442
2847
|
}
|
|
2443
2848
|
case "Range":
|
|
2444
2849
|
return this.evaluateRange(node);
|
|
2850
|
+
case "SpillRef":
|
|
2851
|
+
return this.evaluateSpillRef(node);
|
|
2852
|
+
case "Name": {
|
|
2853
|
+
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
2854
|
+
if (this.scopes[i].has(node.name)) return this.scopes[i].get(node.name);
|
|
2855
|
+
}
|
|
2856
|
+
return this.fail("#NAME?", "This formula contains an unknown name.");
|
|
2857
|
+
}
|
|
2445
2858
|
case "PropertyRef": {
|
|
2446
2859
|
if (!this.context.resolveProperty) {
|
|
2447
2860
|
throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);
|
|
@@ -2449,65 +2862,38 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2449
2862
|
return this.context.resolveProperty(node.property);
|
|
2450
2863
|
}
|
|
2451
2864
|
case "UnaryOp": {
|
|
2865
|
+
if (node.op === "@") return this.implicitIntersection(node.argument);
|
|
2452
2866
|
const raw = this.evaluateNode(node.argument);
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
return
|
|
2867
|
+
if (isMultiArray(raw)) {
|
|
2868
|
+
const { rows, cols } = shapeOf(raw);
|
|
2869
|
+
return buildArray(rows, cols, (r, c) => this.unary(node.op, broadcastGet(raw, r, c)));
|
|
2456
2870
|
}
|
|
2457
|
-
|
|
2458
|
-
if (node.op === "+") return val;
|
|
2459
|
-
throw new Error(`Unsupported unary operator: ${node.op}`);
|
|
2871
|
+
return this.unary(node.op, firstValue(raw));
|
|
2460
2872
|
}
|
|
2461
2873
|
case "BinaryOp": {
|
|
2462
2874
|
const left = this.evaluateNode(node.left);
|
|
2463
2875
|
if (isFormulaError(left)) return left;
|
|
2464
2876
|
const right = this.evaluateNode(node.right);
|
|
2465
2877
|
if (isFormulaError(right)) return right;
|
|
2466
|
-
if (
|
|
2467
|
-
const a =
|
|
2468
|
-
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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}`);
|
|
2878
|
+
if (isMultiArray(left) || isMultiArray(right)) {
|
|
2879
|
+
const a = shapeOf(left);
|
|
2880
|
+
const b = shapeOf(right);
|
|
2881
|
+
const rows = Math.max(a.rows, b.rows);
|
|
2882
|
+
const cols = Math.max(a.cols, b.cols);
|
|
2883
|
+
if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail("#NUM!", "The array result is too large.");
|
|
2884
|
+
return buildArray(rows, cols, (r2, c) => {
|
|
2885
|
+
const l2 = broadcastGet(left, r2, c);
|
|
2886
|
+
if (isFormulaError(l2)) return l2;
|
|
2887
|
+
const rv = broadcastGet(right, r2, c);
|
|
2888
|
+
if (isFormulaError(rv)) return rv;
|
|
2889
|
+
return this.binary(node.op, l2, rv);
|
|
2890
|
+
});
|
|
2510
2891
|
}
|
|
2892
|
+
const l = firstValue(left);
|
|
2893
|
+
if (isFormulaError(l)) return l;
|
|
2894
|
+
const r = firstValue(right);
|
|
2895
|
+
if (isFormulaError(r)) return r;
|
|
2896
|
+
return this.binary(node.op, l, r);
|
|
2511
2897
|
}
|
|
2512
2898
|
case "FunctionCall":
|
|
2513
2899
|
return this.callFunction(node);
|
|
@@ -2515,6 +2901,134 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2515
2901
|
throw new Error(`Unknown AST node: ${node.type}`);
|
|
2516
2902
|
}
|
|
2517
2903
|
}
|
|
2904
|
+
unary(op, raw) {
|
|
2905
|
+
const val = toNumber(raw);
|
|
2906
|
+
if (isFormulaError(val)) {
|
|
2907
|
+
return isFormulaError(raw) ? raw : this.fail("#VALUE!", `The ${op} operator can't use text (${quote(raw)}).`);
|
|
2908
|
+
}
|
|
2909
|
+
if (op === "-") return -val;
|
|
2910
|
+
if (op === "+") return val;
|
|
2911
|
+
throw new Error(`Unsupported unary operator: ${op}`);
|
|
2912
|
+
}
|
|
2913
|
+
/** A binary operator on two single values. */
|
|
2914
|
+
binary(op, left, right) {
|
|
2915
|
+
if (["+", "-", "*", "/", "^"].includes(op)) {
|
|
2916
|
+
if (op === "+" || op === "-") {
|
|
2917
|
+
const leftDate = parseIsoDate(left);
|
|
2918
|
+
const rightDate = parseIsoDate(right);
|
|
2919
|
+
if (leftDate || rightDate) return this.dateArithmetic(op, left, right, leftDate, rightDate);
|
|
2920
|
+
}
|
|
2921
|
+
const a = toNumber(left);
|
|
2922
|
+
if (isFormulaError(a)) return this.fail("#VALUE!", `The ${op} operator can't use text (${quote(left)}).`);
|
|
2923
|
+
const b = toNumber(right);
|
|
2924
|
+
if (isFormulaError(b)) return this.fail("#VALUE!", `The ${op} operator can't use text (${quote(right)}).`);
|
|
2925
|
+
const x = a;
|
|
2926
|
+
const y = b;
|
|
2927
|
+
let result;
|
|
2928
|
+
switch (op) {
|
|
2929
|
+
case "+":
|
|
2930
|
+
result = x + y;
|
|
2931
|
+
break;
|
|
2932
|
+
case "-":
|
|
2933
|
+
result = x - y;
|
|
2934
|
+
break;
|
|
2935
|
+
case "*":
|
|
2936
|
+
result = x * y;
|
|
2937
|
+
break;
|
|
2938
|
+
case "/":
|
|
2939
|
+
if (y === 0) return this.fail("#DIV/0!", "The formula divides by zero.");
|
|
2940
|
+
result = x / y;
|
|
2941
|
+
break;
|
|
2942
|
+
default:
|
|
2943
|
+
result = Math.pow(x, y);
|
|
2944
|
+
}
|
|
2945
|
+
return Number.isFinite(result) ? result : this.fail("#NUM!", "The result is too large or not a number.");
|
|
2946
|
+
}
|
|
2947
|
+
switch (op) {
|
|
2948
|
+
case "&":
|
|
2949
|
+
return toText(left) + toText(right);
|
|
2950
|
+
case "=":
|
|
2951
|
+
return compareValues(left, right) === 0;
|
|
2952
|
+
case "<>":
|
|
2953
|
+
return compareValues(left, right) !== 0;
|
|
2954
|
+
case "<":
|
|
2955
|
+
return compareValues(left, right) < 0;
|
|
2956
|
+
case "<=":
|
|
2957
|
+
return compareValues(left, right) <= 0;
|
|
2958
|
+
case ">":
|
|
2959
|
+
return compareValues(left, right) > 0;
|
|
2960
|
+
case ">=":
|
|
2961
|
+
return compareValues(left, right) >= 0;
|
|
2962
|
+
default:
|
|
2963
|
+
throw new Error(`Unsupported binary operator: ${op}`);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Dates are ISO text: date +/- days is a date, date - date is a number of
|
|
2968
|
+
* days (fractional when times are involved).
|
|
2969
|
+
*/
|
|
2970
|
+
dateArithmetic(op, left, right, leftDate, rightDate) {
|
|
2971
|
+
if (leftDate && rightDate) {
|
|
2972
|
+
if (op === "+") return this.fail("#VALUE!", "Two dates can't be added; subtract them to count the days between.");
|
|
2973
|
+
return cleanFloat(dateToSerial(leftDate) - dateToSerial(rightDate));
|
|
2974
|
+
}
|
|
2975
|
+
if (!leftDate && op === "-") {
|
|
2976
|
+
return this.fail("#VALUE!", `A date (${quote(right)}) can't be subtracted from a number.`);
|
|
2977
|
+
}
|
|
2978
|
+
const date = _nullishCoalesce(leftDate, () => ( rightDate));
|
|
2979
|
+
const dateText = String(leftDate ? left : right);
|
|
2980
|
+
const other = leftDate ? right : left;
|
|
2981
|
+
const days = toNumber(other);
|
|
2982
|
+
if (isFormulaError(days)) return this.fail("#VALUE!", `The ${op} operator can't use text (${quote(other)}).`);
|
|
2983
|
+
const n = days;
|
|
2984
|
+
const result = serialToDate(dateToSerial(date) + (op === "+" ? n : -n));
|
|
2985
|
+
if (!result || result.getFullYear() < 1 || result.getFullYear() > 9999) {
|
|
2986
|
+
return this.fail("#NUM!", "The resulting date is out of range.");
|
|
2987
|
+
}
|
|
2988
|
+
return isoDateHasTime(dateText) || !Number.isInteger(n) ? formatIsoDateTime(result) : formatIsoDate(result);
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* `@` (implicit intersection): a range reference reads the cell in the
|
|
2992
|
+
* formula's row (one column) or column (one row); arrays give their
|
|
2993
|
+
* top-left value.
|
|
2994
|
+
*/
|
|
2995
|
+
implicitIntersection(argument) {
|
|
2996
|
+
const value = this.evaluateNode(argument);
|
|
2997
|
+
if (!Array.isArray(value)) return value;
|
|
2998
|
+
if (value.length <= 1) return firstValue(value);
|
|
2999
|
+
const cell = _optionalChain([this, 'access', _15 => _15.context, 'access', _16 => _16.getFormulaCell, 'optionalCall', _17 => _17()]);
|
|
3000
|
+
if (argument.type === "Range" && cell) {
|
|
3001
|
+
const range = toFormulaRange(value);
|
|
3002
|
+
const top = Math.min(argument.startRow, argument.endRow);
|
|
3003
|
+
const left = Math.min(colNameToIndex(argument.startCol), colNameToIndex(argument.endCol));
|
|
3004
|
+
if (range.cols === 1) {
|
|
3005
|
+
const r = cell.row - top;
|
|
3006
|
+
if (r >= 0 && r < range.rows) return range.values[r];
|
|
3007
|
+
} else if (range.rows === 1) {
|
|
3008
|
+
const c = colNameToIndex(cell.col) - left;
|
|
3009
|
+
if (c >= 0 && c < range.cols) return range.values[c];
|
|
3010
|
+
}
|
|
3011
|
+
return this.fail("#VALUE!", `The @ operator found no cell of ${argument.raw} in the formula's row or column.`);
|
|
3012
|
+
}
|
|
3013
|
+
return value[0];
|
|
3014
|
+
}
|
|
3015
|
+
evaluateSpillRef(node) {
|
|
3016
|
+
if (!this.context.resolveSpill) {
|
|
3017
|
+
throw new Error(`Cannot resolve spill range ${node.ref}#: no spill resolver provided`);
|
|
3018
|
+
}
|
|
3019
|
+
const spill = this.context.resolveSpill(node.col, node.row);
|
|
3020
|
+
if (typeof spill === "string" || !spill) {
|
|
3021
|
+
const code = isFormulaError(spill) ? spill : "#REF!";
|
|
3022
|
+
return this.fail(
|
|
3023
|
+
code,
|
|
3024
|
+
code === "#REF!" ? `${node.ref}# refers to ${node.ref}, which doesn't hold a spilled array.` : `The spill range ${node.ref}# isn't available (${code}).`
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
const values = withRangeShape(spill.values.slice(), spill.rows, spill.cols);
|
|
3028
|
+
const error = values.find(isFormulaError);
|
|
3029
|
+
if (error) this.fail(error, `The spill range ${node.ref}# contains an error (${error}).`);
|
|
3030
|
+
return values;
|
|
3031
|
+
}
|
|
2518
3032
|
evaluateRange(node) {
|
|
2519
3033
|
if (!this.context.resolveRange) {
|
|
2520
3034
|
throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);
|
|
@@ -2532,15 +3046,23 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2532
3046
|
if (!def) return this.fail("#NAME?", `Unknown function "${node.name.toUpperCase()}".`);
|
|
2533
3047
|
const countError = checkFormulaArgumentCount(def, node.args.length);
|
|
2534
3048
|
if (countError) return this.fail("#ERROR!", countError);
|
|
2535
|
-
|
|
3049
|
+
if (def.name === "LET") return this.evaluateLet(node);
|
|
3050
|
+
const args = this.lazyArguments(node.args);
|
|
3051
|
+
if (def.elementwise) {
|
|
3052
|
+
const lifted = this.liftOverArrays(def, args);
|
|
3053
|
+
if (lifted !== void 0) return lifted;
|
|
3054
|
+
}
|
|
3055
|
+
return this.finishCall(def, def.evaluate(this.createContext(def, args)));
|
|
3056
|
+
}
|
|
3057
|
+
finishCall(def, result) {
|
|
2536
3058
|
if (typeof result === "number" && !Number.isFinite(result)) {
|
|
2537
3059
|
return this.fail("#NUM!", `${def.name} produced an invalid number.`);
|
|
2538
3060
|
}
|
|
2539
3061
|
return result === void 0 || result === null ? "" : result;
|
|
2540
3062
|
}
|
|
2541
|
-
|
|
3063
|
+
lazyArguments(nodes) {
|
|
2542
3064
|
const cache = /* @__PURE__ */ new Map();
|
|
2543
|
-
|
|
3065
|
+
return nodes.map((node, index) => ({
|
|
2544
3066
|
index,
|
|
2545
3067
|
node,
|
|
2546
3068
|
value: () => {
|
|
@@ -2548,6 +3070,56 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2548
3070
|
return cache.get(index);
|
|
2549
3071
|
}
|
|
2550
3072
|
}));
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Applies a scalar function element by element when it receives arrays
|
|
3076
|
+
* (`=LEN(A1:A3)`, `=IF(A1:A3>1, "y", "n")`). Arguments typed `range` are
|
|
3077
|
+
* passed whole. Returns undefined when no trigger argument is a
|
|
3078
|
+
* multi-value array.
|
|
3079
|
+
*/
|
|
3080
|
+
liftOverArrays(def, args) {
|
|
3081
|
+
const liftable = (index) => _optionalChain([def, 'access', _18 => _18.args, 'access', _19 => _19[getFormulaArgumentSpecIndex(def, index)], 'optionalAccess', _20 => _20.type]) !== "range";
|
|
3082
|
+
const triggers = def.elementwise === true ? args.map((a) => a.index) : def.elementwise;
|
|
3083
|
+
if (!triggers.some((i) => i < args.length && liftable(i) && isMultiArray(args[i].value()))) return void 0;
|
|
3084
|
+
let rows = 1;
|
|
3085
|
+
let cols = 1;
|
|
3086
|
+
for (const a of args) {
|
|
3087
|
+
if (!liftable(a.index)) continue;
|
|
3088
|
+
const shape = shapeOf(a.value());
|
|
3089
|
+
rows = Math.max(rows, shape.rows);
|
|
3090
|
+
cols = Math.max(cols, shape.cols);
|
|
3091
|
+
}
|
|
3092
|
+
if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail("#NUM!", "The array result is too large.");
|
|
3093
|
+
return buildArray(rows, cols, (r, c) => {
|
|
3094
|
+
const elementArgs = args.map((a) => {
|
|
3095
|
+
if (!liftable(a.index)) return a;
|
|
3096
|
+
const element = broadcastGet(a.value(), r, c);
|
|
3097
|
+
return { index: a.index, node: a.node, value: () => element };
|
|
3098
|
+
});
|
|
3099
|
+
return firstValue(this.finishCall(def, def.evaluate(this.createContext(def, elementArgs))));
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
/** LET(name1, value1, [name2, value2, ...], calculation). */
|
|
3103
|
+
evaluateLet(node) {
|
|
3104
|
+
if (node.args.length % 2 === 0) {
|
|
3105
|
+
return this.fail("#ERROR!", "LET needs name/value pairs followed by a calculation.");
|
|
3106
|
+
}
|
|
3107
|
+
const scope = /* @__PURE__ */ new Map();
|
|
3108
|
+
this.scopes.push(scope);
|
|
3109
|
+
try {
|
|
3110
|
+
for (let i = 0; i < node.args.length - 1; i += 2) {
|
|
3111
|
+
const nameNode = node.args[i];
|
|
3112
|
+
if (nameNode.type !== "Name") {
|
|
3113
|
+
return this.fail("#ERROR!", `Argument ${i + 1} of LET must be a name (names can't look like cell references).`);
|
|
3114
|
+
}
|
|
3115
|
+
scope.set(nameNode.name, this.evaluateNode(node.args[i + 1]));
|
|
3116
|
+
}
|
|
3117
|
+
return this.evaluateNode(node.args[node.args.length - 1]);
|
|
3118
|
+
} finally {
|
|
3119
|
+
this.scopes.pop();
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
createContext(def, args) {
|
|
2551
3123
|
const has = (index) => index >= 0 && index < args.length;
|
|
2552
3124
|
const value = (index) => has(index) ? args[index].value() : "";
|
|
2553
3125
|
const argError = (index, code, reason) => this.fail(code, `Argument ${index + 1} of ${def.name} ${reason}`);
|
|
@@ -2653,5 +3225,6 @@ var FormulaEvaluator = (_class2 = class {
|
|
|
2653
3225
|
|
|
2654
3226
|
|
|
2655
3227
|
|
|
2656
|
-
|
|
2657
|
-
|
|
3228
|
+
|
|
3229
|
+
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.MAX_FORMULA_ARRAY_SIZE = MAX_FORMULA_ARRAY_SIZE; exports.FormulaEvaluator = FormulaEvaluator;
|
|
3230
|
+
//# sourceMappingURL=chunk-SAZRENEZ.cjs.map
|