@ai-matrx/design-system 0.22.0 → 0.25.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/CHANGELOG.md +151 -0
- package/dist/data-table/cell-undo.cjs +98 -0
- package/dist/data-table/cell-undo.cjs.map +1 -0
- package/dist/data-table/cell-undo.d.cts +85 -0
- package/dist/data-table/cell-undo.d.ts +85 -0
- package/dist/data-table/cell-undo.js +78 -0
- package/dist/data-table/cell-undo.js.map +1 -0
- package/dist/data-table/copy-helpers.d.cts +6 -2
- package/dist/data-table/copy-helpers.d.ts +6 -2
- package/dist/data-table/coverage.cjs +116 -0
- package/dist/data-table/coverage.cjs.map +1 -0
- package/dist/data-table/coverage.d.cts +65 -0
- package/dist/data-table/coverage.d.ts +65 -0
- package/dist/data-table/coverage.js +96 -0
- package/dist/data-table/coverage.js.map +1 -0
- package/dist/data-table/fill.cjs +299 -0
- package/dist/data-table/fill.cjs.map +1 -0
- package/dist/data-table/fill.d.cts +129 -0
- package/dist/data-table/fill.d.ts +129 -0
- package/dist/data-table/fill.js +277 -0
- package/dist/data-table/fill.js.map +1 -0
- package/dist/data-table/filter-engine.d.cts +6 -2
- package/dist/data-table/filter-engine.d.ts +6 -2
- package/dist/data-table/grid-clipboard.cjs +165 -0
- package/dist/data-table/grid-clipboard.cjs.map +1 -0
- package/dist/data-table/grid-clipboard.d.cts +85 -0
- package/dist/data-table/grid-clipboard.d.ts +85 -0
- package/dist/data-table/grid-clipboard.js +145 -0
- package/dist/data-table/grid-clipboard.js.map +1 -0
- package/dist/data-table/grid-selection.cjs +258 -0
- package/dist/data-table/grid-selection.cjs.map +1 -0
- package/dist/data-table/grid-selection.d.cts +166 -0
- package/dist/data-table/grid-selection.d.ts +166 -0
- package/dist/data-table/grid-selection.js +238 -0
- package/dist/data-table/grid-selection.js.map +1 -0
- package/dist/data-table/grouping.cjs +302 -0
- package/dist/data-table/grouping.cjs.map +1 -0
- package/dist/data-table/grouping.d.cts +119 -0
- package/dist/data-table/grouping.d.ts +119 -0
- package/dist/data-table/grouping.js +282 -0
- package/dist/data-table/grouping.js.map +1 -0
- package/dist/data-table/host.d.cts +6 -2
- package/dist/data-table/host.d.ts +6 -2
- package/dist/data-table/index.cjs +2433 -246
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +11 -3
- package/dist/data-table/index.d.ts +11 -3
- package/dist/data-table/index.js +2425 -238
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +6 -2
- package/dist/data-table/infer-filter.d.ts +6 -2
- package/dist/data-table/layered-filters.d.cts +6 -2
- package/dist/data-table/layered-filters.d.ts +6 -2
- package/dist/data-table/query-control.d.cts +6 -2
- package/dist/data-table/query-control.d.ts +6 -2
- package/dist/data-table/spreadsheet.cjs +1014 -0
- package/dist/data-table/spreadsheet.cjs.map +1 -0
- package/dist/data-table/spreadsheet.d.cts +118 -0
- package/dist/data-table/spreadsheet.d.ts +118 -0
- package/dist/data-table/spreadsheet.js +994 -0
- package/dist/data-table/spreadsheet.js.map +1 -0
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +6 -2
- package/dist/data-table/types.d.ts +6 -2
- package/dist/data-table/url-state.d.cts +6 -2
- package/dist/data-table/url-state.d.ts +6 -2
- package/dist/data-table/validation.cjs +229 -0
- package/dist/data-table/validation.cjs.map +1 -0
- package/dist/data-table/validation.d.cts +125 -0
- package/dist/data-table/validation.d.ts +125 -0
- package/dist/data-table/validation.js +209 -0
- package/dist/data-table/validation.js.map +1 -0
- package/dist/data-table/virtual-window.cjs +203 -0
- package/dist/data-table/virtual-window.cjs.map +1 -0
- package/dist/data-table/virtual-window.d.cts +84 -0
- package/dist/data-table/virtual-window.d.ts +84 -0
- package/dist/data-table/virtual-window.js +183 -0
- package/dist/data-table/virtual-window.js.map +1 -0
- package/dist/data-table/xlsx.d.cts +6 -2
- package/dist/data-table/xlsx.d.ts +6 -2
- package/dist/field-formats/index.d.cts +2 -2
- package/dist/field-formats/index.d.ts +2 -2
- package/dist/formulas/index.cjs +1041 -0
- package/dist/formulas/index.cjs.map +1 -0
- package/dist/formulas/index.d.cts +389 -0
- package/dist/formulas/index.d.ts +389 -0
- package/dist/formulas/index.js +1019 -0
- package/dist/formulas/index.js.map +1 -0
- package/dist/{types-BdbvlF9-.d.cts → types-CJWU1Gae.d.cts} +1 -1
- package/dist/{types-BdbvlF9-.d.ts → types-CJWU1Gae.d.ts} +1 -1
- package/dist/{types-Sn249eoy.d.cts → types-CmQrOz-T.d.cts} +154 -2
- package/dist/{types-D-flTpK0.d.ts → types-nlLCMiqd.d.ts} +154 -2
- package/package.json +109 -2
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/formulas/index.ts
|
|
21
|
+
var formulas_exports = {};
|
|
22
|
+
__export(formulas_exports, {
|
|
23
|
+
FORMULA_FUNCTIONS: () => FORMULA_FUNCTIONS,
|
|
24
|
+
evaluateFormula: () => evaluateFormula,
|
|
25
|
+
formulaColumnsOf: () => formulaColumnsOf,
|
|
26
|
+
formulaResultType: () => formulaResultType,
|
|
27
|
+
isFormulaColumn: () => isFormulaColumn,
|
|
28
|
+
parseFormula: () => parseFormula,
|
|
29
|
+
rewriteFormulaReferences: () => rewriteFormulaReferences,
|
|
30
|
+
withComputedColumns: () => withComputedColumns
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(formulas_exports);
|
|
33
|
+
|
|
34
|
+
// src/formulas/formulas.ts
|
|
35
|
+
var FUNCTION_SPECS = {
|
|
36
|
+
SUM: {
|
|
37
|
+
signature: "SUM(number, \u2026)",
|
|
38
|
+
description: "Adds every value, ignoring empty ones.",
|
|
39
|
+
min: 1,
|
|
40
|
+
max: Infinity,
|
|
41
|
+
result: "number"
|
|
42
|
+
},
|
|
43
|
+
MIN: {
|
|
44
|
+
signature: "MIN(number, \u2026)",
|
|
45
|
+
description: "The smallest value, ignoring empty ones.",
|
|
46
|
+
min: 1,
|
|
47
|
+
max: Infinity,
|
|
48
|
+
result: "number"
|
|
49
|
+
},
|
|
50
|
+
MAX: {
|
|
51
|
+
signature: "MAX(number, \u2026)",
|
|
52
|
+
description: "The largest value, ignoring empty ones.",
|
|
53
|
+
min: 1,
|
|
54
|
+
max: Infinity,
|
|
55
|
+
result: "number"
|
|
56
|
+
},
|
|
57
|
+
AVERAGE: {
|
|
58
|
+
signature: "AVERAGE(number, \u2026)",
|
|
59
|
+
description: "The average of the values that are filled in. Empty values are not counted.",
|
|
60
|
+
min: 1,
|
|
61
|
+
max: Infinity,
|
|
62
|
+
result: "number"
|
|
63
|
+
},
|
|
64
|
+
ROUND: {
|
|
65
|
+
signature: "ROUND(number, places?)",
|
|
66
|
+
description: "Rounds to the given number of decimal places (0 when omitted). Halves round away from zero.",
|
|
67
|
+
min: 1,
|
|
68
|
+
max: 2,
|
|
69
|
+
result: "number"
|
|
70
|
+
},
|
|
71
|
+
ABS: {
|
|
72
|
+
signature: "ABS(number)",
|
|
73
|
+
description: "The value without its minus sign.",
|
|
74
|
+
min: 1,
|
|
75
|
+
max: 1,
|
|
76
|
+
result: "number"
|
|
77
|
+
},
|
|
78
|
+
LEN: {
|
|
79
|
+
signature: "LEN(text)",
|
|
80
|
+
description: "How many characters the text has.",
|
|
81
|
+
min: 1,
|
|
82
|
+
max: 1,
|
|
83
|
+
result: "number"
|
|
84
|
+
},
|
|
85
|
+
UPPER: {
|
|
86
|
+
signature: "UPPER(text)",
|
|
87
|
+
description: "The text in capitals.",
|
|
88
|
+
min: 1,
|
|
89
|
+
max: 1,
|
|
90
|
+
result: "text"
|
|
91
|
+
},
|
|
92
|
+
LOWER: {
|
|
93
|
+
signature: "LOWER(text)",
|
|
94
|
+
description: "The text in lower case.",
|
|
95
|
+
min: 1,
|
|
96
|
+
max: 1,
|
|
97
|
+
result: "text"
|
|
98
|
+
},
|
|
99
|
+
TRIM: {
|
|
100
|
+
signature: "TRIM(text)",
|
|
101
|
+
description: "The text without leading or trailing spaces.",
|
|
102
|
+
min: 1,
|
|
103
|
+
max: 1,
|
|
104
|
+
result: "text"
|
|
105
|
+
},
|
|
106
|
+
CONCATENATE: {
|
|
107
|
+
signature: "CONCATENATE(text, \u2026)",
|
|
108
|
+
description: "Joins every value into one piece of text.",
|
|
109
|
+
min: 1,
|
|
110
|
+
max: Infinity,
|
|
111
|
+
result: "text"
|
|
112
|
+
},
|
|
113
|
+
LEFT: {
|
|
114
|
+
signature: "LEFT(text, count)",
|
|
115
|
+
description: "The first few characters of the text.",
|
|
116
|
+
min: 2,
|
|
117
|
+
max: 2,
|
|
118
|
+
result: "text"
|
|
119
|
+
},
|
|
120
|
+
RIGHT: {
|
|
121
|
+
signature: "RIGHT(text, count)",
|
|
122
|
+
description: "The last few characters of the text.",
|
|
123
|
+
min: 2,
|
|
124
|
+
max: 2,
|
|
125
|
+
result: "text"
|
|
126
|
+
},
|
|
127
|
+
CONTAINS: {
|
|
128
|
+
signature: "CONTAINS(text, part)",
|
|
129
|
+
description: "Yes when the text contains that part. Upper and lower case are treated the same.",
|
|
130
|
+
min: 2,
|
|
131
|
+
max: 2,
|
|
132
|
+
result: "boolean"
|
|
133
|
+
},
|
|
134
|
+
IF: {
|
|
135
|
+
signature: "IF(condition, then, otherwise?)",
|
|
136
|
+
description: "The second value when the condition holds, otherwise the third (empty when omitted).",
|
|
137
|
+
min: 2,
|
|
138
|
+
max: 3,
|
|
139
|
+
result: "unknown"
|
|
140
|
+
},
|
|
141
|
+
AND: {
|
|
142
|
+
signature: "AND(condition, \u2026)",
|
|
143
|
+
description: "Yes when every condition holds.",
|
|
144
|
+
min: 1,
|
|
145
|
+
max: Infinity,
|
|
146
|
+
result: "boolean"
|
|
147
|
+
},
|
|
148
|
+
OR: {
|
|
149
|
+
signature: "OR(condition, \u2026)",
|
|
150
|
+
description: "Yes when at least one condition holds.",
|
|
151
|
+
min: 1,
|
|
152
|
+
max: Infinity,
|
|
153
|
+
result: "boolean"
|
|
154
|
+
},
|
|
155
|
+
NOT: {
|
|
156
|
+
signature: "NOT(condition)",
|
|
157
|
+
description: "Turns yes into no and no into yes.",
|
|
158
|
+
min: 1,
|
|
159
|
+
max: 1,
|
|
160
|
+
result: "boolean"
|
|
161
|
+
},
|
|
162
|
+
BLANK: {
|
|
163
|
+
signature: "BLANK()",
|
|
164
|
+
description: "An empty value, for comparing against or returning.",
|
|
165
|
+
min: 0,
|
|
166
|
+
max: 0,
|
|
167
|
+
result: "unknown"
|
|
168
|
+
},
|
|
169
|
+
ISBLANK: {
|
|
170
|
+
signature: "ISBLANK(value)",
|
|
171
|
+
description: "Yes when the value is empty.",
|
|
172
|
+
min: 1,
|
|
173
|
+
max: 1,
|
|
174
|
+
result: "boolean"
|
|
175
|
+
},
|
|
176
|
+
TODAY: {
|
|
177
|
+
signature: "TODAY()",
|
|
178
|
+
description: "Today's date.",
|
|
179
|
+
min: 0,
|
|
180
|
+
max: 0,
|
|
181
|
+
result: "date"
|
|
182
|
+
},
|
|
183
|
+
NOW: {
|
|
184
|
+
signature: "NOW()",
|
|
185
|
+
description: "The current date and time.",
|
|
186
|
+
min: 0,
|
|
187
|
+
max: 0,
|
|
188
|
+
result: "date"
|
|
189
|
+
},
|
|
190
|
+
DATEDIFF: {
|
|
191
|
+
signature: "DATEDIFF(from, to, 'days' | 'hours' | 'minutes')",
|
|
192
|
+
description: "How far the second date is after the first. Negative when it is earlier.",
|
|
193
|
+
min: 3,
|
|
194
|
+
max: 3,
|
|
195
|
+
result: "number"
|
|
196
|
+
},
|
|
197
|
+
YEAR: {
|
|
198
|
+
signature: "YEAR(date)",
|
|
199
|
+
description: "The four-digit year of the date.",
|
|
200
|
+
min: 1,
|
|
201
|
+
max: 1,
|
|
202
|
+
result: "number"
|
|
203
|
+
},
|
|
204
|
+
MONTH: {
|
|
205
|
+
signature: "MONTH(date)",
|
|
206
|
+
description: "The month of the date, 1 to 12.",
|
|
207
|
+
min: 1,
|
|
208
|
+
max: 1,
|
|
209
|
+
result: "number"
|
|
210
|
+
},
|
|
211
|
+
DAY: {
|
|
212
|
+
signature: "DAY(date)",
|
|
213
|
+
description: "The day of the month, 1 to 31.",
|
|
214
|
+
min: 1,
|
|
215
|
+
max: 1,
|
|
216
|
+
result: "number"
|
|
217
|
+
},
|
|
218
|
+
DATEADD: {
|
|
219
|
+
signature: "DATEADD(date, count, 'days' | 'months' | 'years')",
|
|
220
|
+
description: "The date moved forward by that many units. Use a negative count to go back.",
|
|
221
|
+
min: 3,
|
|
222
|
+
max: 3,
|
|
223
|
+
result: "date"
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var FUNCTION_NAMES = Object.keys(FUNCTION_SPECS);
|
|
227
|
+
var FORMULA_FUNCTIONS = Object.freeze(
|
|
228
|
+
FUNCTION_NAMES.map((name) => ({
|
|
229
|
+
name,
|
|
230
|
+
signature: FUNCTION_SPECS[name].signature,
|
|
231
|
+
description: FUNCTION_SPECS[name].description
|
|
232
|
+
}))
|
|
233
|
+
);
|
|
234
|
+
var FormulaError = class extends Error {
|
|
235
|
+
position;
|
|
236
|
+
constructor(message, position = 0) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.name = "FormulaError";
|
|
239
|
+
this.position = position;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var TWO_CHAR_OPERATORS = ["!=", "<>", "<=", ">="];
|
|
243
|
+
var ONE_CHAR_OPERATORS = ["+", "-", "*", "/", "%", "&", "=", "<", ">"];
|
|
244
|
+
function isDigit(ch) {
|
|
245
|
+
return ch >= "0" && ch <= "9";
|
|
246
|
+
}
|
|
247
|
+
function isIdentifierStart(ch) {
|
|
248
|
+
return /[A-Za-z_]/.test(ch);
|
|
249
|
+
}
|
|
250
|
+
function isIdentifierPart(ch) {
|
|
251
|
+
return /[A-Za-z0-9_]/.test(ch);
|
|
252
|
+
}
|
|
253
|
+
function tokenize(source) {
|
|
254
|
+
const tokens = [];
|
|
255
|
+
let i = 0;
|
|
256
|
+
while (i < source.length) {
|
|
257
|
+
const ch = source[i];
|
|
258
|
+
if (ch === void 0) break;
|
|
259
|
+
if (/\s/.test(ch)) {
|
|
260
|
+
i += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (ch === "{") {
|
|
264
|
+
const close = source.indexOf("}", i + 1);
|
|
265
|
+
if (close === -1) {
|
|
266
|
+
throw new FormulaError(
|
|
267
|
+
"This column reference is missing its closing brace `}`.",
|
|
268
|
+
i
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const name = source.slice(i + 1, close).trim();
|
|
272
|
+
if (name === "") {
|
|
273
|
+
throw new FormulaError("This column reference has no name inside `{}`.", i);
|
|
274
|
+
}
|
|
275
|
+
tokens.push({ type: "reference", text: name, position: i });
|
|
276
|
+
i = close + 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (ch === '"' || ch === "'") {
|
|
280
|
+
const quote = ch;
|
|
281
|
+
let j = i + 1;
|
|
282
|
+
let text = "";
|
|
283
|
+
let closed = false;
|
|
284
|
+
while (j < source.length) {
|
|
285
|
+
if (source[j] === "\\" && j + 1 < source.length) {
|
|
286
|
+
text += source[j + 1];
|
|
287
|
+
j += 2;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (source[j] === quote) {
|
|
291
|
+
closed = true;
|
|
292
|
+
j += 1;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
text += source[j];
|
|
296
|
+
j += 1;
|
|
297
|
+
}
|
|
298
|
+
if (!closed) {
|
|
299
|
+
throw new FormulaError(
|
|
300
|
+
`This text is missing its closing ${quote === '"' ? "double" : "single"} quote.`,
|
|
301
|
+
i
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
tokens.push({ type: "string", text, position: i });
|
|
305
|
+
i = j;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (isDigit(ch) || ch === "." && isDigit(source[i + 1] ?? "")) {
|
|
309
|
+
let j = i;
|
|
310
|
+
let seenDot = false;
|
|
311
|
+
while (j < source.length) {
|
|
312
|
+
if (isDigit(source[j] ?? "")) {
|
|
313
|
+
j += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (source[j] === "." && !seenDot) {
|
|
317
|
+
seenDot = true;
|
|
318
|
+
j += 1;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
tokens.push({ type: "number", text: source.slice(i, j), position: i });
|
|
324
|
+
i = j;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (isIdentifierStart(ch)) {
|
|
328
|
+
let j = i;
|
|
329
|
+
while (j < source.length && isIdentifierPart(source[j] ?? "")) j += 1;
|
|
330
|
+
tokens.push({ type: "identifier", text: source.slice(i, j), position: i });
|
|
331
|
+
i = j;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (ch === "(") {
|
|
335
|
+
tokens.push({ type: "paren-open", text: ch, position: i });
|
|
336
|
+
i += 1;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ch === ")") {
|
|
340
|
+
tokens.push({ type: "paren-close", text: ch, position: i });
|
|
341
|
+
i += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (ch === ",") {
|
|
345
|
+
tokens.push({ type: "comma", text: ch, position: i });
|
|
346
|
+
i += 1;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const two = source.slice(i, i + 2);
|
|
350
|
+
if (TWO_CHAR_OPERATORS.includes(two)) {
|
|
351
|
+
tokens.push({ type: "operator", text: two, position: i });
|
|
352
|
+
i += 2;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (ONE_CHAR_OPERATORS.includes(ch)) {
|
|
356
|
+
tokens.push({ type: "operator", text: ch, position: i });
|
|
357
|
+
i += 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
throw new FormulaError(`\`${ch}\` does not mean anything in a formula.`, i);
|
|
361
|
+
}
|
|
362
|
+
tokens.push({ type: "end", text: "", position: source.length });
|
|
363
|
+
return tokens;
|
|
364
|
+
}
|
|
365
|
+
var COMPARISON_OPERATORS = ["=", "!=", "<>", "<", "<=", ">", ">="];
|
|
366
|
+
var Parser = class {
|
|
367
|
+
tokens;
|
|
368
|
+
index = 0;
|
|
369
|
+
references = [];
|
|
370
|
+
/**
|
|
371
|
+
* `tokenize` always terminates the list with an `end` token and the grammar
|
|
372
|
+
* never advances past it, so neither accessor below can actually run off the
|
|
373
|
+
* end. This sentinel is what they would return if one ever did, which keeps
|
|
374
|
+
* both accessors total without an assertion.
|
|
375
|
+
*/
|
|
376
|
+
endToken;
|
|
377
|
+
constructor(tokens) {
|
|
378
|
+
this.tokens = tokens;
|
|
379
|
+
this.endToken = tokens[tokens.length - 1] ?? {
|
|
380
|
+
type: "end",
|
|
381
|
+
text: "",
|
|
382
|
+
position: 0
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
peek() {
|
|
386
|
+
return this.tokens[this.index] ?? this.endToken;
|
|
387
|
+
}
|
|
388
|
+
next() {
|
|
389
|
+
return this.tokens[this.index++] ?? this.endToken;
|
|
390
|
+
}
|
|
391
|
+
matchOperator(candidates) {
|
|
392
|
+
const token = this.peek();
|
|
393
|
+
if (token.type === "operator" && candidates.includes(token.text)) {
|
|
394
|
+
this.index += 1;
|
|
395
|
+
return token;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
parseProgram() {
|
|
400
|
+
const expression = this.parseComparison();
|
|
401
|
+
const trailing = this.peek();
|
|
402
|
+
if (trailing.type !== "end") {
|
|
403
|
+
throw new FormulaError(
|
|
404
|
+
`Nothing should follow the formula here \u2014 remove \`${trailing.text}\`.`,
|
|
405
|
+
trailing.position
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return expression;
|
|
409
|
+
}
|
|
410
|
+
parseComparison() {
|
|
411
|
+
let left = this.parseConcat();
|
|
412
|
+
let operator = this.matchOperator(COMPARISON_OPERATORS);
|
|
413
|
+
while (operator) {
|
|
414
|
+
const right = this.parseConcat();
|
|
415
|
+
left = {
|
|
416
|
+
kind: "binary",
|
|
417
|
+
operator: operator.text,
|
|
418
|
+
left,
|
|
419
|
+
right,
|
|
420
|
+
position: operator.position
|
|
421
|
+
};
|
|
422
|
+
operator = this.matchOperator(COMPARISON_OPERATORS);
|
|
423
|
+
}
|
|
424
|
+
return left;
|
|
425
|
+
}
|
|
426
|
+
parseConcat() {
|
|
427
|
+
let left = this.parseAdditive();
|
|
428
|
+
let operator = this.matchOperator(["&"]);
|
|
429
|
+
while (operator) {
|
|
430
|
+
const right = this.parseAdditive();
|
|
431
|
+
left = {
|
|
432
|
+
kind: "binary",
|
|
433
|
+
operator: "&",
|
|
434
|
+
left,
|
|
435
|
+
right,
|
|
436
|
+
position: operator.position
|
|
437
|
+
};
|
|
438
|
+
operator = this.matchOperator(["&"]);
|
|
439
|
+
}
|
|
440
|
+
return left;
|
|
441
|
+
}
|
|
442
|
+
parseAdditive() {
|
|
443
|
+
let left = this.parseMultiplicative();
|
|
444
|
+
let operator = this.matchOperator(["+", "-"]);
|
|
445
|
+
while (operator) {
|
|
446
|
+
const right = this.parseMultiplicative();
|
|
447
|
+
left = {
|
|
448
|
+
kind: "binary",
|
|
449
|
+
operator: operator.text,
|
|
450
|
+
left,
|
|
451
|
+
right,
|
|
452
|
+
position: operator.position
|
|
453
|
+
};
|
|
454
|
+
operator = this.matchOperator(["+", "-"]);
|
|
455
|
+
}
|
|
456
|
+
return left;
|
|
457
|
+
}
|
|
458
|
+
parseMultiplicative() {
|
|
459
|
+
let left = this.parseUnary();
|
|
460
|
+
let operator = this.matchOperator(["*", "/", "%"]);
|
|
461
|
+
while (operator) {
|
|
462
|
+
const right = this.parseUnary();
|
|
463
|
+
left = {
|
|
464
|
+
kind: "binary",
|
|
465
|
+
operator: operator.text,
|
|
466
|
+
left,
|
|
467
|
+
right,
|
|
468
|
+
position: operator.position
|
|
469
|
+
};
|
|
470
|
+
operator = this.matchOperator(["*", "/", "%"]);
|
|
471
|
+
}
|
|
472
|
+
return left;
|
|
473
|
+
}
|
|
474
|
+
parseUnary() {
|
|
475
|
+
const minus = this.matchOperator(["-"]);
|
|
476
|
+
if (minus) {
|
|
477
|
+
return {
|
|
478
|
+
kind: "unary",
|
|
479
|
+
operator: "-",
|
|
480
|
+
operand: this.parseUnary(),
|
|
481
|
+
position: minus.position
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const plus = this.matchOperator(["+"]);
|
|
485
|
+
if (plus) return this.parseUnary();
|
|
486
|
+
return this.parsePrimary();
|
|
487
|
+
}
|
|
488
|
+
parsePrimary() {
|
|
489
|
+
const token = this.next();
|
|
490
|
+
if (token.type === "number") {
|
|
491
|
+
return { kind: "number", value: Number(token.text), position: token.position };
|
|
492
|
+
}
|
|
493
|
+
if (token.type === "string") {
|
|
494
|
+
return { kind: "string", value: token.text, position: token.position };
|
|
495
|
+
}
|
|
496
|
+
if (token.type === "reference") {
|
|
497
|
+
if (!this.references.includes(token.text)) this.references.push(token.text);
|
|
498
|
+
return { kind: "reference", name: token.text, position: token.position };
|
|
499
|
+
}
|
|
500
|
+
if (token.type === "paren-open") {
|
|
501
|
+
const inner = this.parseComparison();
|
|
502
|
+
const close = this.next();
|
|
503
|
+
if (close.type !== "paren-close") {
|
|
504
|
+
throw new FormulaError(
|
|
505
|
+
"This opening bracket `(` never closes.",
|
|
506
|
+
token.position
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return inner;
|
|
510
|
+
}
|
|
511
|
+
if (token.type === "identifier") {
|
|
512
|
+
const upper = token.text.toUpperCase();
|
|
513
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
514
|
+
return {
|
|
515
|
+
kind: "boolean",
|
|
516
|
+
value: upper === "TRUE",
|
|
517
|
+
position: token.position
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (!FUNCTION_NAMES.includes(upper)) {
|
|
521
|
+
throw new FormulaError(
|
|
522
|
+
`There is no function called \`${token.text}\`. A column goes in braces, like {${token.text}}.`,
|
|
523
|
+
token.position
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const name = upper;
|
|
527
|
+
const open = this.next();
|
|
528
|
+
if (open.type !== "paren-open") {
|
|
529
|
+
throw new FormulaError(
|
|
530
|
+
`\`${name}\` needs brackets after it, like ${FUNCTION_SPECS[name].signature}.`,
|
|
531
|
+
token.position
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const args = [];
|
|
535
|
+
if (this.peek().type === "paren-close") {
|
|
536
|
+
this.index += 1;
|
|
537
|
+
} else {
|
|
538
|
+
for (; ; ) {
|
|
539
|
+
args.push(this.parseComparison());
|
|
540
|
+
const separator = this.next();
|
|
541
|
+
if (separator.type === "comma") continue;
|
|
542
|
+
if (separator.type === "paren-close") break;
|
|
543
|
+
throw new FormulaError(
|
|
544
|
+
`\`${name}\` is missing a comma or its closing bracket.`,
|
|
545
|
+
separator.position
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const spec = FUNCTION_SPECS[name];
|
|
550
|
+
if (args.length < spec.min || args.length > spec.max) {
|
|
551
|
+
throw new FormulaError(
|
|
552
|
+
`\`${name}\` was given ${args.length} value${args.length === 1 ? "" : "s"}. Use ${spec.signature}.`,
|
|
553
|
+
token.position
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return { kind: "call", name, args, position: token.position };
|
|
557
|
+
}
|
|
558
|
+
if (token.type === "end") {
|
|
559
|
+
throw new FormulaError("The formula stops before it is finished.", token.position);
|
|
560
|
+
}
|
|
561
|
+
throw new FormulaError(
|
|
562
|
+
`\`${token.text}\` cannot start a value here.`,
|
|
563
|
+
token.position
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
function parseFormula(source) {
|
|
568
|
+
try {
|
|
569
|
+
const text = source ?? "";
|
|
570
|
+
if (text.trim() === "") {
|
|
571
|
+
return { ok: false, error: "This formula is empty.", position: 0 };
|
|
572
|
+
}
|
|
573
|
+
const parser = new Parser(tokenize(text));
|
|
574
|
+
const ast = parser.parseProgram();
|
|
575
|
+
return { ok: true, ast, references: parser.references };
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (error instanceof FormulaError) {
|
|
578
|
+
return { ok: false, error: error.message, position: error.position };
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
error: "This formula could not be read.",
|
|
583
|
+
position: 0
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
var MISSING = /* @__PURE__ */ Symbol("missing-column");
|
|
588
|
+
function isBlank(value) {
|
|
589
|
+
return value === null || value === "";
|
|
590
|
+
}
|
|
591
|
+
function numberToText(n) {
|
|
592
|
+
if (Number.isInteger(n)) return String(n);
|
|
593
|
+
const trimmed = Number(n.toPrecision(15));
|
|
594
|
+
return String(Number.isFinite(trimmed) ? trimmed : n);
|
|
595
|
+
}
|
|
596
|
+
function toText(value) {
|
|
597
|
+
if (value === null) return "";
|
|
598
|
+
if (typeof value === "number") return numberToText(value);
|
|
599
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function looseNumber(value) {
|
|
603
|
+
if (value === null || value === "") return 0;
|
|
604
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
605
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
606
|
+
const cleaned = value.replace(/[,\s$€£¥%]/g, "");
|
|
607
|
+
if (cleaned === "") return 0;
|
|
608
|
+
const n = Number(cleaned);
|
|
609
|
+
return Number.isFinite(n) ? n : null;
|
|
610
|
+
}
|
|
611
|
+
function requireNumber(value, what) {
|
|
612
|
+
const n = looseNumber(value);
|
|
613
|
+
if (n === null) {
|
|
614
|
+
throw new FormulaError(`${what} needs a number, but got "${toText(value)}".`);
|
|
615
|
+
}
|
|
616
|
+
return n;
|
|
617
|
+
}
|
|
618
|
+
function truthy(value) {
|
|
619
|
+
if (value === null || value === "") return false;
|
|
620
|
+
if (typeof value === "boolean") return value;
|
|
621
|
+
if (typeof value === "number") return value !== 0;
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
function compareValues(left, right) {
|
|
625
|
+
const looksNumeric = (v) => typeof v === "number" || typeof v === "string" && v.trim() !== "" && looseNumber(v) !== null;
|
|
626
|
+
if (isBlank(left) && isBlank(right)) return 0;
|
|
627
|
+
const blankBesideNumber = isBlank(left) && looksNumeric(right) || isBlank(right) && looksNumeric(left);
|
|
628
|
+
if (blankBesideNumber || looksNumeric(left) && looksNumeric(right)) {
|
|
629
|
+
const a2 = looseNumber(left) ?? 0;
|
|
630
|
+
const b2 = looseNumber(right) ?? 0;
|
|
631
|
+
return a2 < b2 ? -1 : a2 > b2 ? 1 : 0;
|
|
632
|
+
}
|
|
633
|
+
if (typeof left === "boolean" || typeof right === "boolean") {
|
|
634
|
+
const a2 = truthy(left);
|
|
635
|
+
const b2 = truthy(right);
|
|
636
|
+
return a2 === b2 ? 0 : a2 ? 1 : -1;
|
|
637
|
+
}
|
|
638
|
+
const a = toText(left);
|
|
639
|
+
const b = toText(right);
|
|
640
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
641
|
+
}
|
|
642
|
+
var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
643
|
+
function requireDate(value, what) {
|
|
644
|
+
if (value === null || value === "") {
|
|
645
|
+
throw new FormulaError(`${what} needs a date, but that value is empty.`);
|
|
646
|
+
}
|
|
647
|
+
if (typeof value === "string") {
|
|
648
|
+
const trimmed = value.trim();
|
|
649
|
+
const ms = Date.parse(trimmed);
|
|
650
|
+
if (!Number.isNaN(ms)) return { ms, dateOnly: DATE_ONLY_RE.test(trimmed) };
|
|
651
|
+
}
|
|
652
|
+
throw new FormulaError(`${what} needs a date, but got "${toText(value)}".`);
|
|
653
|
+
}
|
|
654
|
+
function isoFrom(ms, dateOnly) {
|
|
655
|
+
const iso = new Date(ms).toISOString();
|
|
656
|
+
return dateOnly ? iso.slice(0, 10) : iso;
|
|
657
|
+
}
|
|
658
|
+
var DATEDIFF_UNITS = {
|
|
659
|
+
days: 864e5,
|
|
660
|
+
hours: 36e5,
|
|
661
|
+
minutes: 6e4
|
|
662
|
+
};
|
|
663
|
+
function requireUnit(value, allowed, fn) {
|
|
664
|
+
const unit = toText(value).trim().toLowerCase();
|
|
665
|
+
if (!allowed.includes(unit)) {
|
|
666
|
+
throw new FormulaError(
|
|
667
|
+
`\`${fn}\` needs one of ${allowed.map((u) => `'${u}'`).join(", ")} \u2014 got "${toText(value)}".`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return unit;
|
|
671
|
+
}
|
|
672
|
+
function addMonths(ms, count) {
|
|
673
|
+
const d = new Date(ms);
|
|
674
|
+
const day = d.getUTCDate();
|
|
675
|
+
const target = new Date(ms);
|
|
676
|
+
target.setUTCDate(1);
|
|
677
|
+
target.setUTCMonth(target.getUTCMonth() + count);
|
|
678
|
+
const lastDay = new Date(
|
|
679
|
+
Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)
|
|
680
|
+
).getUTCDate();
|
|
681
|
+
target.setUTCDate(Math.min(day, lastDay));
|
|
682
|
+
return target.getTime();
|
|
683
|
+
}
|
|
684
|
+
function numericArgs(values, fn) {
|
|
685
|
+
const out = [];
|
|
686
|
+
for (const value of values) {
|
|
687
|
+
if (isBlank(value)) continue;
|
|
688
|
+
out.push(requireNumber(value, `\`${fn}\``));
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
function roundHalfAway(n, places) {
|
|
693
|
+
const factor = 10 ** places;
|
|
694
|
+
const scaled = n * factor;
|
|
695
|
+
const corrected = Number(scaled.toPrecision(15));
|
|
696
|
+
const rounded = corrected < 0 ? -Math.round(-corrected) : Math.round(corrected);
|
|
697
|
+
return rounded / factor;
|
|
698
|
+
}
|
|
699
|
+
function argAt(args, index) {
|
|
700
|
+
return args[index] ?? null;
|
|
701
|
+
}
|
|
702
|
+
function callFunction(name, args) {
|
|
703
|
+
switch (name) {
|
|
704
|
+
case "SUM":
|
|
705
|
+
return numericArgs(args, "SUM").reduce((a, b) => a + b, 0);
|
|
706
|
+
case "MIN": {
|
|
707
|
+
const nums = numericArgs(args, "MIN");
|
|
708
|
+
return nums.length === 0 ? null : Math.min(...nums);
|
|
709
|
+
}
|
|
710
|
+
case "MAX": {
|
|
711
|
+
const nums = numericArgs(args, "MAX");
|
|
712
|
+
return nums.length === 0 ? null : Math.max(...nums);
|
|
713
|
+
}
|
|
714
|
+
case "AVERAGE": {
|
|
715
|
+
const nums = numericArgs(args, "AVERAGE");
|
|
716
|
+
if (nums.length === 0) return null;
|
|
717
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
718
|
+
}
|
|
719
|
+
case "ROUND": {
|
|
720
|
+
const n = requireNumber(argAt(args, 0), "`ROUND`");
|
|
721
|
+
const places = args.length > 1 ? Math.trunc(requireNumber(argAt(args, 1), "`ROUND`")) : 0;
|
|
722
|
+
return roundHalfAway(n, places);
|
|
723
|
+
}
|
|
724
|
+
case "ABS":
|
|
725
|
+
return Math.abs(requireNumber(argAt(args, 0), "`ABS`"));
|
|
726
|
+
case "LEN":
|
|
727
|
+
return toText(argAt(args, 0)).length;
|
|
728
|
+
case "UPPER":
|
|
729
|
+
return toText(argAt(args, 0)).toUpperCase();
|
|
730
|
+
case "LOWER":
|
|
731
|
+
return toText(argAt(args, 0)).toLowerCase();
|
|
732
|
+
case "TRIM":
|
|
733
|
+
return toText(argAt(args, 0)).trim();
|
|
734
|
+
case "CONCATENATE":
|
|
735
|
+
return args.map(toText).join("");
|
|
736
|
+
case "LEFT": {
|
|
737
|
+
const count = Math.trunc(requireNumber(argAt(args, 1), "`LEFT`"));
|
|
738
|
+
return toText(argAt(args, 0)).slice(0, Math.max(0, count));
|
|
739
|
+
}
|
|
740
|
+
case "RIGHT": {
|
|
741
|
+
const count = Math.trunc(requireNumber(argAt(args, 1), "`RIGHT`"));
|
|
742
|
+
if (count <= 0) return "";
|
|
743
|
+
return toText(argAt(args, 0)).slice(-count);
|
|
744
|
+
}
|
|
745
|
+
case "CONTAINS":
|
|
746
|
+
return toText(argAt(args, 0)).toLowerCase().includes(toText(argAt(args, 1)).toLowerCase());
|
|
747
|
+
case "NOT":
|
|
748
|
+
return !truthy(argAt(args, 0));
|
|
749
|
+
case "BLANK":
|
|
750
|
+
return null;
|
|
751
|
+
case "ISBLANK":
|
|
752
|
+
return isBlank(argAt(args, 0));
|
|
753
|
+
case "TODAY":
|
|
754
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
755
|
+
case "NOW":
|
|
756
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
757
|
+
case "DATEDIFF": {
|
|
758
|
+
const from = requireDate(argAt(args, 0), "`DATEDIFF`");
|
|
759
|
+
const to = requireDate(argAt(args, 1), "`DATEDIFF`");
|
|
760
|
+
const unit = requireUnit(argAt(args, 2), Object.keys(DATEDIFF_UNITS), "DATEDIFF");
|
|
761
|
+
const perUnit = DATEDIFF_UNITS[unit];
|
|
762
|
+
if (perUnit === void 0) return null;
|
|
763
|
+
return Math.trunc((to.ms - from.ms) / perUnit);
|
|
764
|
+
}
|
|
765
|
+
case "YEAR":
|
|
766
|
+
return new Date(requireDate(argAt(args, 0), "`YEAR`").ms).getUTCFullYear();
|
|
767
|
+
case "MONTH":
|
|
768
|
+
return new Date(requireDate(argAt(args, 0), "`MONTH`").ms).getUTCMonth() + 1;
|
|
769
|
+
case "DAY":
|
|
770
|
+
return new Date(requireDate(argAt(args, 0), "`DAY`").ms).getUTCDate();
|
|
771
|
+
case "DATEADD": {
|
|
772
|
+
const base = requireDate(argAt(args, 0), "`DATEADD`");
|
|
773
|
+
const count = Math.trunc(requireNumber(argAt(args, 1), "`DATEADD`"));
|
|
774
|
+
const unit = requireUnit(argAt(args, 2), ["days", "months", "years"], "DATEADD");
|
|
775
|
+
const ms = unit === "days" ? base.ms + count * 864e5 : addMonths(base.ms, unit === "months" ? count : count * 12);
|
|
776
|
+
return isoFrom(ms, base.dateOnly);
|
|
777
|
+
}
|
|
778
|
+
// IF / AND / OR short-circuit in `evaluateNode` and never reach here.
|
|
779
|
+
case "IF":
|
|
780
|
+
case "AND":
|
|
781
|
+
case "OR":
|
|
782
|
+
throw new FormulaError(`\`${name}\` could not be evaluated.`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function applyBinary(operator, left, right) {
|
|
786
|
+
switch (operator) {
|
|
787
|
+
case "&":
|
|
788
|
+
return toText(left) + toText(right);
|
|
789
|
+
case "+":
|
|
790
|
+
return requireNumber(left, "`+`") + requireNumber(right, "`+`");
|
|
791
|
+
case "-":
|
|
792
|
+
return requireNumber(left, "`-`") - requireNumber(right, "`-`");
|
|
793
|
+
case "*":
|
|
794
|
+
return requireNumber(left, "`*`") * requireNumber(right, "`*`");
|
|
795
|
+
case "/": {
|
|
796
|
+
const divisor = requireNumber(right, "`/`");
|
|
797
|
+
if (divisor === 0) {
|
|
798
|
+
throw new FormulaError("This formula divides by zero.");
|
|
799
|
+
}
|
|
800
|
+
return requireNumber(left, "`/`") / divisor;
|
|
801
|
+
}
|
|
802
|
+
case "%": {
|
|
803
|
+
const divisor = requireNumber(right, "`%`");
|
|
804
|
+
if (divisor === 0) {
|
|
805
|
+
throw new FormulaError("This formula divides by zero.");
|
|
806
|
+
}
|
|
807
|
+
return requireNumber(left, "`%`") % divisor;
|
|
808
|
+
}
|
|
809
|
+
case "=":
|
|
810
|
+
return compareValues(left, right) === 0;
|
|
811
|
+
case "!=":
|
|
812
|
+
case "<>":
|
|
813
|
+
return compareValues(left, right) !== 0;
|
|
814
|
+
case "<":
|
|
815
|
+
return compareValues(left, right) < 0;
|
|
816
|
+
case "<=":
|
|
817
|
+
return compareValues(left, right) <= 0;
|
|
818
|
+
case ">":
|
|
819
|
+
return compareValues(left, right) > 0;
|
|
820
|
+
case ">=":
|
|
821
|
+
return compareValues(left, right) >= 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function normalizeCell(raw, name) {
|
|
825
|
+
if (raw === MISSING) {
|
|
826
|
+
throw new FormulaError(`There is no column called {${name}}.`);
|
|
827
|
+
}
|
|
828
|
+
if (raw === null || raw === void 0) return null;
|
|
829
|
+
if (typeof raw === "string" || typeof raw === "boolean") return raw;
|
|
830
|
+
if (typeof raw === "number") {
|
|
831
|
+
if (!Number.isFinite(raw)) {
|
|
832
|
+
throw new FormulaError(`The value in {${name}} is not a usable number.`);
|
|
833
|
+
}
|
|
834
|
+
return raw;
|
|
835
|
+
}
|
|
836
|
+
if (raw instanceof Date) {
|
|
837
|
+
return Number.isNaN(raw.getTime()) ? null : raw.toISOString();
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
return JSON.stringify(raw);
|
|
841
|
+
} catch {
|
|
842
|
+
return String(raw);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function evaluateNode(node, resolve) {
|
|
846
|
+
switch (node.kind) {
|
|
847
|
+
case "number":
|
|
848
|
+
case "string":
|
|
849
|
+
case "boolean":
|
|
850
|
+
return node.value;
|
|
851
|
+
case "reference": {
|
|
852
|
+
const raw = resolve(node.name);
|
|
853
|
+
return normalizeCell(raw === void 0 ? MISSING : raw, node.name);
|
|
854
|
+
}
|
|
855
|
+
case "unary":
|
|
856
|
+
return -requireNumber(evaluateNode(node.operand, resolve), "`-`");
|
|
857
|
+
case "binary":
|
|
858
|
+
return applyBinary(
|
|
859
|
+
node.operator,
|
|
860
|
+
evaluateNode(node.left, resolve),
|
|
861
|
+
evaluateNode(node.right, resolve)
|
|
862
|
+
);
|
|
863
|
+
case "call": {
|
|
864
|
+
if (node.name === "IF") {
|
|
865
|
+
const test = node.args[0];
|
|
866
|
+
const whenTrue = node.args[1];
|
|
867
|
+
const condition = truthy(test ? evaluateNode(test, resolve) : null);
|
|
868
|
+
if (condition) return whenTrue ? evaluateNode(whenTrue, resolve) : null;
|
|
869
|
+
const whenFalse = node.args[2];
|
|
870
|
+
return node.args.length > 2 && whenFalse ? evaluateNode(whenFalse, resolve) : null;
|
|
871
|
+
}
|
|
872
|
+
if (node.name === "AND") {
|
|
873
|
+
for (const arg of node.args) {
|
|
874
|
+
if (!truthy(evaluateNode(arg, resolve))) return false;
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
if (node.name === "OR") {
|
|
879
|
+
for (const arg of node.args) {
|
|
880
|
+
if (truthy(evaluateNode(arg, resolve))) return true;
|
|
881
|
+
}
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
return callFunction(
|
|
885
|
+
node.name,
|
|
886
|
+
node.args.map((arg) => evaluateNode(arg, resolve))
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function evaluateFormula(ast, resolve) {
|
|
892
|
+
try {
|
|
893
|
+
const value = evaluateNode(ast, resolve);
|
|
894
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
895
|
+
return { ok: false, error: "This formula produced a number too large to show." };
|
|
896
|
+
}
|
|
897
|
+
return { ok: true, value };
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (error instanceof FormulaError) return { ok: false, error: error.message };
|
|
900
|
+
return { ok: false, error: "This formula could not be calculated." };
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function formulaResultType(ast, columnType) {
|
|
904
|
+
switch (ast.kind) {
|
|
905
|
+
case "number":
|
|
906
|
+
return "number";
|
|
907
|
+
case "string":
|
|
908
|
+
return "text";
|
|
909
|
+
case "boolean":
|
|
910
|
+
return "boolean";
|
|
911
|
+
case "unary":
|
|
912
|
+
return "number";
|
|
913
|
+
case "reference":
|
|
914
|
+
return columnType(ast.name) ?? "unknown";
|
|
915
|
+
case "binary": {
|
|
916
|
+
if (ast.operator === "&") return "text";
|
|
917
|
+
if (COMPARISON_OPERATORS.includes(ast.operator)) return "boolean";
|
|
918
|
+
return "number";
|
|
919
|
+
}
|
|
920
|
+
case "call": {
|
|
921
|
+
if (ast.name === "IF") {
|
|
922
|
+
const thenArg = ast.args[1];
|
|
923
|
+
const then = thenArg ? formulaResultType(thenArg, columnType) : "unknown";
|
|
924
|
+
const otherwiseArg = ast.args[2];
|
|
925
|
+
if (ast.args.length < 3 || !otherwiseArg) return then;
|
|
926
|
+
const otherwise = formulaResultType(otherwiseArg, columnType);
|
|
927
|
+
if (then === otherwise) return then;
|
|
928
|
+
if (then === "unknown") return otherwise;
|
|
929
|
+
if (otherwise === "unknown") return then;
|
|
930
|
+
return "unknown";
|
|
931
|
+
}
|
|
932
|
+
if (ast.name === "MIN" || ast.name === "MAX") {
|
|
933
|
+
const types = ast.args.map((a) => formulaResultType(a, columnType));
|
|
934
|
+
if (types.length > 0 && types.every((t) => t === "date")) return "date";
|
|
935
|
+
return "number";
|
|
936
|
+
}
|
|
937
|
+
return FUNCTION_SPECS[ast.name].result;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function formulaExpressionOf(field) {
|
|
942
|
+
const metadata = field.metadata;
|
|
943
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
944
|
+
const format = metadata.format;
|
|
945
|
+
if (!format || typeof format !== "object") return null;
|
|
946
|
+
if (format.id !== "formula") return null;
|
|
947
|
+
const options = format.options;
|
|
948
|
+
const formula = options && typeof options === "object" ? options.formula : void 0;
|
|
949
|
+
const expression = formula && typeof formula === "object" ? formula.expression : void 0;
|
|
950
|
+
return typeof expression === "string" ? expression : "";
|
|
951
|
+
}
|
|
952
|
+
function isFormulaColumn(field) {
|
|
953
|
+
return formulaExpressionOf(field) !== null;
|
|
954
|
+
}
|
|
955
|
+
function formulaColumnsOf(fields) {
|
|
956
|
+
return fields.flatMap((field) => {
|
|
957
|
+
const expression = formulaExpressionOf(field);
|
|
958
|
+
if (expression === null) return [];
|
|
959
|
+
return [{ field, parsed: parseFormula(expression) }];
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function withComputedColumns(rows, fields) {
|
|
963
|
+
const columns = formulaColumnsOf(fields);
|
|
964
|
+
const formulaFieldNames = new Set(columns.map((c) => c.field.field_name));
|
|
965
|
+
const errors = /* @__PURE__ */ new Map();
|
|
966
|
+
if (columns.length === 0) {
|
|
967
|
+
return { rows, errors, formulaFieldNames };
|
|
968
|
+
}
|
|
969
|
+
const fieldNameByReference = /* @__PURE__ */ new Map();
|
|
970
|
+
for (const f of fields) {
|
|
971
|
+
fieldNameByReference.set(f.display_name.toLowerCase(), f.field_name);
|
|
972
|
+
}
|
|
973
|
+
for (const f of fields) {
|
|
974
|
+
fieldNameByReference.set(f.field_name.toLowerCase(), f.field_name);
|
|
975
|
+
}
|
|
976
|
+
const computed = rows.map((row) => {
|
|
977
|
+
const data = { ...row.data ?? {} };
|
|
978
|
+
const resolve = (name) => {
|
|
979
|
+
if (name in data) return data[name];
|
|
980
|
+
const fieldName = fieldNameByReference.get(name.toLowerCase());
|
|
981
|
+
if (fieldName === void 0) return void 0;
|
|
982
|
+
return data[fieldName] ?? null;
|
|
983
|
+
};
|
|
984
|
+
for (const { field, parsed } of columns) {
|
|
985
|
+
if (!parsed.ok) {
|
|
986
|
+
data[field.field_name] = null;
|
|
987
|
+
errors.set(`${row.id}::${field.field_name}`, parsed.error);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const result = evaluateFormula(parsed.ast, resolve);
|
|
991
|
+
if (result.ok) data[field.field_name] = result.value;
|
|
992
|
+
else {
|
|
993
|
+
data[field.field_name] = null;
|
|
994
|
+
errors.set(`${row.id}::${field.field_name}`, result.error);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return { ...row, data };
|
|
998
|
+
});
|
|
999
|
+
return { rows: computed, errors, formulaFieldNames };
|
|
1000
|
+
}
|
|
1001
|
+
function rewriteFormulaReferences(expression, from, to) {
|
|
1002
|
+
const target = from.trim().toLowerCase();
|
|
1003
|
+
if (!target) return expression;
|
|
1004
|
+
let out = "";
|
|
1005
|
+
let i = 0;
|
|
1006
|
+
while (i < expression.length) {
|
|
1007
|
+
const ch = expression[i];
|
|
1008
|
+
if (ch === '"' || ch === "'") {
|
|
1009
|
+
let j = i + 1;
|
|
1010
|
+
while (j < expression.length) {
|
|
1011
|
+
if (expression[j] === "\\" && j + 1 < expression.length) {
|
|
1012
|
+
j += 2;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (expression[j] === ch) {
|
|
1016
|
+
j += 1;
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
j += 1;
|
|
1020
|
+
}
|
|
1021
|
+
out += expression.slice(i, j);
|
|
1022
|
+
i = j;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (ch === "{") {
|
|
1026
|
+
const close = expression.indexOf("}", i + 1);
|
|
1027
|
+
if (close === -1) {
|
|
1028
|
+
out += expression.slice(i);
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
const name = expression.slice(i + 1, close);
|
|
1032
|
+
out += name.trim().toLowerCase() === target ? `{${to}}` : expression.slice(i, close + 1);
|
|
1033
|
+
i = close + 1;
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
out += ch;
|
|
1037
|
+
i += 1;
|
|
1038
|
+
}
|
|
1039
|
+
return out;
|
|
1040
|
+
}
|
|
1041
|
+
//# sourceMappingURL=index.cjs.map
|