@xeplr/expression-handler 2.1.0 → 2.3.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 +21 -2
- package/index.js +12 -2
- package/lib/compile.js +4 -3
- package/lib/functions.js +120 -11
- package/lib/parse.js +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,9 @@ f({ country: 'India', score: 600, referred: true }); // → 'India payroll'
|
|
|
29
29
|
| `xf.toText(tree)` | the tree back as text, for an editor showing a stored formula |
|
|
30
30
|
| `xf.check(text, context?)` | `{ ok, tree, error, position }` — never throws; for editors |
|
|
31
31
|
| `xf.analyze(textOrTree)` | `{ fields, fieldsOutsideTotals, thisrow, functions, aggregates, levels }` |
|
|
32
|
-
| `xf.functions()` | the function list, for pickers and help |
|
|
32
|
+
| `xf.functions()` | the function list, for pickers and help; each entry carries `params` |
|
|
33
|
+
| `xf.usage(name, { rows }?)` | how to call it: `{ signature, params: [{ name, about, example }], description, example, min, max }`; `rows: false` for a total that names its list first. For signature help, a reference, an error |
|
|
34
|
+
| `xf.arityMessage(name, given, { rows }?)` | the words for a wrong argument count, built from `usage` |
|
|
33
35
|
| `xf.implementations(context?)` | `{ name: fn }` ready to call, for a caller that wants the functions without the language |
|
|
34
36
|
| `xf.registerFunction(entry)` | adds one to the list |
|
|
35
37
|
| `xf.evaluate(condition, row)` | 1.x: a condition → `true` / `false` |
|
|
@@ -132,6 +134,23 @@ Each function is one entry: its code **and** what the UI shows.
|
|
|
132
134
|
validation, and Workflow's condition picker are all built from it.
|
|
133
135
|
`xf.registerFunction(entry)` adds one.
|
|
134
136
|
|
|
137
|
+
**Every argument is documented** — its name, what it is, and an example
|
|
138
|
+
(`PARAMS` in `lib/functions.js`; a test fails for a function without them).
|
|
139
|
+
`xf.usage(name)` gives them to an editor's signature help, and a call with the
|
|
140
|
+
wrong number of arguments says exactly what it needs:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
sumif(condition, value) needs 2 arguments, not 1. condition: Which rows to add:
|
|
144
|
+
a test that is true or false for each row (e.g. status = "Paid"). value: What to
|
|
145
|
+
add up for those rows — a column or a calculation (e.g. amount).
|
|
146
|
+
Example: sumif(status = "Refunded", amount)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The words follow the caller: over a report group's rows, a total is
|
|
150
|
+
`sumif(condition, value)`; where it names its list (Workflow, a `compile`
|
|
151
|
+
without `aggregate`), `sumif(list, condition, value)`. Why: "takes 2 to 3
|
|
152
|
+
arguments" named neither the arguments nor which form applied.
|
|
153
|
+
|
|
135
154
|
| Category | Functions |
|
|
136
155
|
|---|---|
|
|
137
156
|
| Math | `round` `abs` `ceil` `floor` `sqrt` `min` `max` `pow` `trunc` `exp` `ln` `log` `log10` `log2` `cbrt` |
|
|
@@ -171,7 +190,7 @@ const f = xf.compile('sumif(fytd(order_date), revenue)', {
|
|
|
171
190
|
| `fiscalYearStart` | `4` (April) | `fytd` and any financial-year function |
|
|
172
191
|
| `today` | now | `ytd`, `fytd` — pin it for tests and scheduled runs |
|
|
173
192
|
| `missing` | `'null'` | `'error'` makes a name the row lacks an error |
|
|
174
|
-
| `ignoreCase` | `false` | `true` makes `=`, `!=`, `in
|
|
193
|
+
| `ignoreCase` | `false` | `true` makes `=`, `!=`, `in`, the text operators (`contains`, `startsWith`, `endsWith`, `notIn`, …) and the functions `contains()`, `startsWith()`, `endsWith()` ignore case. Numbers and null compare exactly as before. BI's report formulas set it, so a formula and the SQL it is translated into agree whatever the database's collation; Workflow's conditions leave it off |
|
|
175
194
|
| `maxListItems` · `maxDistinct` · `maxSteps` · `maxLevels` | see [Limits](#limits) | |
|
|
176
195
|
| `aggregate(name, args, { compile, level })` | none | supplies the rows for the group form of a total (BI) |
|
|
177
196
|
|
package/index.js
CHANGED
|
@@ -53,6 +53,12 @@ function contextKey(options) {
|
|
|
53
53
|
// key to build: this is the path a caller passing text per row takes.
|
|
54
54
|
var plainCache = new Map();
|
|
55
55
|
|
|
56
|
+
// A total's words in an error follow the caller: over a report's rows when it
|
|
57
|
+
// supplies them (options.aggregate), else naming a list first.
|
|
58
|
+
function parseOptions(options) {
|
|
59
|
+
return { rows: Boolean(options && typeof options.aggregate === 'function') };
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
function compile(formula, options) {
|
|
57
63
|
if (typeof formula === 'string' && options === undefined) {
|
|
58
64
|
var plain = plainCache.get(formula);
|
|
@@ -64,11 +70,11 @@ function compile(formula, options) {
|
|
|
64
70
|
}
|
|
65
71
|
var key = contextKey(options);
|
|
66
72
|
if (typeof formula === 'string') {
|
|
67
|
-
if (key === null) return compileTree(parse(formula), options);
|
|
73
|
+
if (key === null) return compileTree(parse(formula, parseOptions(options)), options);
|
|
68
74
|
var k = key + '\u0000' + formula;
|
|
69
75
|
var hit = textCache.get(k);
|
|
70
76
|
if (hit) return hit;
|
|
71
|
-
var f = compileTree(parse(formula), options);
|
|
77
|
+
var f = compileTree(parse(formula, parseOptions(options)), options);
|
|
72
78
|
if (textCache.size >= CACHE_MAX) textCache.clear();
|
|
73
79
|
textCache.set(k, f);
|
|
74
80
|
return f;
|
|
@@ -149,6 +155,10 @@ module.exports = {
|
|
|
149
155
|
},
|
|
150
156
|
|
|
151
157
|
functions: fns.listFunctions,
|
|
158
|
+
// How to call one: its signature and every argument named, explained and
|
|
159
|
+
// exemplified — for signature help, a reference list, an error.
|
|
160
|
+
usage: fns.usage,
|
|
161
|
+
arityMessage: fns.arityMessage,
|
|
152
162
|
implementations: implementations,
|
|
153
163
|
CATEGORIES: fns.CATEGORIES,
|
|
154
164
|
registerFunction: registerFunction,
|
package/lib/compile.js
CHANGED
|
@@ -50,7 +50,7 @@ function compileTree(tree, options) {
|
|
|
50
50
|
var fy = Number(opts.fiscalYearStart);
|
|
51
51
|
if (!(fy >= 1 && fy <= 12 && Math.floor(fy) === fy)) fail('fiscalYearStart must be a month, 1–12');
|
|
52
52
|
|
|
53
|
-
var ctx = { now: dates.todayFor(opts), fiscalYearStart: fy };
|
|
53
|
+
var ctx = { now: dates.todayFor(opts), fiscalYearStart: fy, ignoreCase: Boolean(opts.ignoreCase) };
|
|
54
54
|
// Equality for `=`, `in` and the word operators: exact, or folded when the
|
|
55
55
|
// caller asked (opts.ignoreCase) — see operators.IGNORE_CASE.
|
|
56
56
|
var eq = opts.ignoreCase ? operators.looseEqIgnoreCase : operators.looseEq;
|
|
@@ -133,7 +133,7 @@ function compileTree(tree, options) {
|
|
|
133
133
|
var entry = functions.lookup(name);
|
|
134
134
|
if (!entry || entry.special) fail('Unknown function "' + name + '"');
|
|
135
135
|
if (entry.aggregate) fail(entry.name + '() must be written as a call, not a { fn } operand');
|
|
136
|
-
if (args.length < entry.min || args.length > entry.max) fail(
|
|
136
|
+
if (args.length < entry.min || args.length > entry.max) fail(functions.arityMessage(entry.name, args.length));
|
|
137
137
|
var fn = entry.fn;
|
|
138
138
|
if (entry.context) {
|
|
139
139
|
var inner = fn;
|
|
@@ -253,7 +253,8 @@ function compileTree(tree, options) {
|
|
|
253
253
|
}
|
|
254
254
|
return opts.aggregate(entry.name, args, { level: inner, compile: function (n) { return c(n, inner); } });
|
|
255
255
|
}
|
|
256
|
-
|
|
256
|
+
// The words for the form this caller writes: a report's rows, or a list.
|
|
257
|
+
if (args.length !== implicit + 1) fail(functions.arityMessage(entry.name, args.length, { rows: typeof opts.aggregate === 'function' }));
|
|
257
258
|
|
|
258
259
|
var list = c(args[0], level);
|
|
259
260
|
var cond = entry.shape.cond ? c(args[1], inner) : null;
|
package/lib/functions.js
CHANGED
|
@@ -28,6 +28,9 @@ var asDate = dates.asDate;
|
|
|
28
28
|
|
|
29
29
|
var CATEGORIES = ['Math', 'Text', 'Date', 'Logic', 'Aggregate'];
|
|
30
30
|
|
|
31
|
+
// Text for a comparison: lower-cased when the formula ignores case.
|
|
32
|
+
function fold(c, v) { var s = str(v); return c && c.ignoreCase ? s.toLowerCase() : s; }
|
|
33
|
+
|
|
31
34
|
function str(v) { return v === null || v === undefined ? '' : String(v); }
|
|
32
35
|
function num(v) { var n = Number(v); return isNaN(n) ? null : n; }
|
|
33
36
|
function isEmpty(v) { return v === null || v === undefined || v === ''; }
|
|
@@ -121,15 +124,17 @@ var LIST = [
|
|
|
121
124
|
{ name: 'replace', category: 'Text', args: 'text, find, replace with', min: 3, max: 3,
|
|
122
125
|
description: 'Swap one piece of text for another', example: 'replace(phone, "-", "")',
|
|
123
126
|
fn: function (v, find, into) { return str(v).split(str(find)).join(str(into)); } },
|
|
124
|
-
|
|
127
|
+
// context: the compile option ignoreCase folds both sides, as it does for
|
|
128
|
+
// = and in — one rule for every text comparison in a formula.
|
|
129
|
+
{ name: 'contains', category: 'Text', args: 'text, find', min: 2, max: 2, context: true,
|
|
125
130
|
description: 'True when the text contains it', example: 'contains(notes, "urgent")',
|
|
126
|
-
fn: function (v, sub) { return
|
|
127
|
-
{ name: 'startsWith', category: 'Text', args: 'text, find', min: 2, max: 2,
|
|
131
|
+
fn: function (c, v, sub) { return fold(c, v).indexOf(fold(c, sub)) !== -1; } },
|
|
132
|
+
{ name: 'startsWith', category: 'Text', args: 'text, find', min: 2, max: 2, context: true,
|
|
128
133
|
description: 'True when the text begins with it', example: 'startsWith(sku, "IN")',
|
|
129
|
-
fn: function (v, sub) { return
|
|
130
|
-
{ name: 'endsWith', category: 'Text', args: 'text, find', min: 2, max: 2,
|
|
134
|
+
fn: function (c, v, sub) { return fold(c, v).startsWith(fold(c, sub)); } },
|
|
135
|
+
{ name: 'endsWith', category: 'Text', args: 'text, find', min: 2, max: 2, context: true,
|
|
131
136
|
description: 'True when the text ends with it', example: 'endsWith(file, ".pdf")',
|
|
132
|
-
fn: function (v, sub) { return
|
|
137
|
+
fn: function (c, v, sub) { return fold(c, v).endsWith(fold(c, sub)); } },
|
|
133
138
|
{ name: 'length', category: 'Text', args: 'text', min: 1, max: 1,
|
|
134
139
|
description: 'How many characters', example: 'length(name)',
|
|
135
140
|
fn: function (v) { return Array.isArray(v) ? v.length : str(v).length; } },
|
|
@@ -239,16 +244,118 @@ var LIST = [
|
|
|
239
244
|
{ name: 'sum', category: 'Aggregate', args: 'value', min: 1, max: 2, aggregate: true, shape: { cond: false, value: true },
|
|
240
245
|
description: 'Total across the rows in this group', example: 'sum(amount)' },
|
|
241
246
|
{ name: 'sumif', category: 'Aggregate', args: 'condition, value', min: 2, max: 3, aggregate: true, shape: { cond: true, value: true },
|
|
242
|
-
description: 'Total across the rows that match', example: 'sumif(status
|
|
247
|
+
description: 'Total across the rows that match', example: 'sumif(status = "Refunded", amount)' },
|
|
243
248
|
{ name: 'countif', category: 'Aggregate', args: 'condition', min: 1, max: 2, aggregate: true, shape: { cond: true, value: false },
|
|
244
249
|
description: 'How many rows match', example: 'countif(qty > 10)' },
|
|
245
250
|
{ name: 'countifunique', category: 'Aggregate', args: 'condition, value', min: 2, max: 3, aggregate: true, shape: { cond: true, value: true },
|
|
246
|
-
description: 'How many different values among the rows that match', example: 'countifunique(status
|
|
251
|
+
description: 'How many different values among the rows that match', example: 'countifunique(status = "Paid", customer_id)' },
|
|
247
252
|
// Not a function — a reference, usable inside any aggregate's condition.
|
|
248
253
|
{ name: '$thisrow.column', category: 'Aggregate', args: '', special: true,
|
|
249
|
-
description: 'This row\'s own value, so a total can cover every row sharing it', example: 'sumif(month
|
|
254
|
+
description: 'This row\'s own value, so a total can cover every row sharing it', example: 'sumif(month = $thisrow.month, revenue)' }
|
|
250
255
|
];
|
|
251
256
|
|
|
257
|
+
// ── What each argument IS ───────────────────────────────────────────────
|
|
258
|
+
// [name, what it is, an example]. Read by usage() — the signature help in an
|
|
259
|
+
// editor, the reference list and every "wrong number of arguments" message
|
|
260
|
+
// come from here, so they cannot disagree. Optional ones say so; `…` repeats.
|
|
261
|
+
var PARAMS = {
|
|
262
|
+
round: [['value', 'The number to round', 'amount'], ['places', 'Optional. Decimal places to keep; 0 when left out', '2']],
|
|
263
|
+
abs: [['value', 'A number', 'variance']],
|
|
264
|
+
ceil: [['value', 'A number', 'days']],
|
|
265
|
+
floor: [['value', 'A number', 'days']],
|
|
266
|
+
sqrt: [['value', 'A number, 0 or more', 'area']],
|
|
267
|
+
min: [['value …', 'Two or more values to compare, on the same row', 'price, cap']],
|
|
268
|
+
max: [['value …', 'Two or more values to compare, on the same row', 'price, floor_price']],
|
|
269
|
+
trunc: [['value', 'A number', 'hours']],
|
|
270
|
+
exp: [['value', 'The power to raise e to', 'rate']],
|
|
271
|
+
ln: [['value', 'A number above 0', 'growth']],
|
|
272
|
+
log: [['value', 'A number above 0', 'growth']],
|
|
273
|
+
log10: [['value', 'A number above 0', 'size']],
|
|
274
|
+
log2: [['value', 'A number above 0', 'size']],
|
|
275
|
+
cbrt: [['value', 'A number', 'volume']],
|
|
276
|
+
pow: [['value', 'The number to raise', 'rate'], ['exponent', 'The power to raise it to', 'years']],
|
|
277
|
+
upper: [['text', 'The text to change', 'city']],
|
|
278
|
+
lower: [['text', 'The text to change', 'email']],
|
|
279
|
+
trim: [['text', 'The text to clean', 'name']],
|
|
280
|
+
concat: [['value …', 'The pieces to join, in order — columns or "text in quotes"', 'first, " ", last']],
|
|
281
|
+
substring: [['text', 'The text to take from', 'code'], ['start', 'Where to start; the first character is 1', '1'], ['count', 'Optional. How many characters; to the end when left out', '3']],
|
|
282
|
+
left: [['text', 'The text to take from', 'deptCode'], ['count', 'How many characters from the start', '3']],
|
|
283
|
+
right: [['text', 'The text to take from', 'invoice'], ['count', 'How many characters from the end', '4']],
|
|
284
|
+
replace: [['text', 'The text to change', 'phone'], ['find', 'What to look for, in quotes', '"-"'], ['replace with', 'What to put instead, in quotes ("" removes it)', '""']],
|
|
285
|
+
contains: [['text', 'The text to look in', 'notes'], ['find', 'What to look for, in quotes; capitals ignored', '"urgent"']],
|
|
286
|
+
startsWith: [['text', 'The text to look at', 'sku'], ['find', 'What it should begin with, in quotes; capitals ignored', '"IN"']],
|
|
287
|
+
endsWith: [['text', 'The text to look at', 'file'], ['find', 'What it should end with, in quotes; capitals ignored', '".pdf"']],
|
|
288
|
+
length: [['text', 'The text to count', 'name']],
|
|
289
|
+
year: [['date', 'A date column', 'order_date']],
|
|
290
|
+
month: [['date', 'A date column', 'order_date']],
|
|
291
|
+
day: [['date', 'A date column', 'order_date']],
|
|
292
|
+
quarter: [['date', 'A date column', 'order_date']],
|
|
293
|
+
weekday: [['date', 'A date column', 'order_date']],
|
|
294
|
+
datediff: [['from', 'The earlier date', 'ordered'], ['to', 'The later date; days from "from" to this', 'shipped']],
|
|
295
|
+
dateadd: [['date', 'The date to move', 'order_date'], ['days', 'How many days to add; negative goes back', '30']],
|
|
296
|
+
monthname: [['date', 'A date column, or a month number 1–12', 'order_date']],
|
|
297
|
+
monthkey: [['date', 'A date column', 'order_date']],
|
|
298
|
+
quarterkey: [['date', 'A date column', 'order_date']],
|
|
299
|
+
daykey: [['date', 'A date column', 'order_date']],
|
|
300
|
+
ytd: [['date', 'The date column that decides the year', 'order_date']],
|
|
301
|
+
fytd: [['date', 'The date column that decides the financial year', 'order_date']],
|
|
302
|
+
if: [['condition', 'A test that is true or false', 'qty > 10'], ['then', 'The result when it is true', '"bulk"'], ['otherwise', 'The result when it is false', '"single"']],
|
|
303
|
+
coalesce: [['value …', 'Values to try in order; the first that is not empty wins', 'nickname, name']],
|
|
304
|
+
ifnull: [['value', 'The value to use', 'discount'], ['fallback', 'What to use when it is empty', '0']],
|
|
305
|
+
isnull: [['value', 'The value to test', 'closed_date']],
|
|
306
|
+
between: [['value', 'The value to test', 'score'], ['low', 'The lowest allowed, included', '600'], ['high', 'The highest allowed, included', '750']],
|
|
307
|
+
sum: [['value', 'What to add up for each row of the group — a column or a calculation', 'amount']],
|
|
308
|
+
sumif: [['condition', 'Which rows to add: a test that is true or false for each row', 'status = "Paid"'], ['value', 'What to add up for those rows — a column or a calculation', 'amount']],
|
|
309
|
+
countif: [['condition', 'Which rows to count: a test that is true or false for each row', 'qty > 10']],
|
|
310
|
+
countifunique: [['condition', 'Which rows to look at: a test that is true or false for each row', 'status = "Paid"'], ['value', 'What to count the different values of', 'customer_id']]
|
|
311
|
+
};
|
|
312
|
+
// Written first when a total names its own list (outside a report).
|
|
313
|
+
var LIST_PARAM = ['list', 'The list to go through — outside a report a total names it first', 'orders'];
|
|
314
|
+
|
|
315
|
+
function paramsOf(entry) {
|
|
316
|
+
return (PARAMS[entry.name] || []).map(function (p) { return { name: p[0], about: p[1], example: p[2] }; });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* How to call a function, with every argument named and explained.
|
|
321
|
+
*
|
|
322
|
+
* @param name the function's name
|
|
323
|
+
* @param options { rows: true } — a total over the rows of a report group (the
|
|
324
|
+
* default for a total); { rows: false } — a total that names
|
|
325
|
+
* its list first, sumif(orders, condition, value)
|
|
326
|
+
* @returns {{ name, signature, params: [{ name, about, example }], description,
|
|
327
|
+
* example, min, max }} or null for a name that is no function
|
|
328
|
+
*/
|
|
329
|
+
function usage(name, options) {
|
|
330
|
+
var entry = lookup(name);
|
|
331
|
+
if (!entry) return null;
|
|
332
|
+
var params = paramsOf(entry);
|
|
333
|
+
var listed = entry.aggregate && options && options.rows === false;
|
|
334
|
+
if (listed) params = [{ name: LIST_PARAM[0], about: LIST_PARAM[1], example: LIST_PARAM[2] }].concat(params);
|
|
335
|
+
var count = entry.aggregate ? params.length : null;
|
|
336
|
+
return {
|
|
337
|
+
name: entry.name,
|
|
338
|
+
signature: entry.name + '(' + params.map(function (p) { return p.name; }).join(', ') + ')',
|
|
339
|
+
params: params,
|
|
340
|
+
description: entry.description || '',
|
|
341
|
+
example: listed ? entry.name + '(' + params.map(function (p) { return p.example; }).join(', ') + ')' : (entry.example || ''),
|
|
342
|
+
min: count === null ? entry.min : count,
|
|
343
|
+
max: count === null ? entry.max : count
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The words for a call with the wrong number of arguments: the signature,
|
|
349
|
+
* how many it needs, how many it got, and each argument with an example.
|
|
350
|
+
*/
|
|
351
|
+
function arityMessage(name, given, options) {
|
|
352
|
+
var u = usage(name, options);
|
|
353
|
+
var need = u.min === u.max ? String(u.min) : u.max === Infinity ? u.min + ' or more' : u.max === u.min + 1 ? u.min + ' or ' + u.max : u.min + ' to ' + u.max;
|
|
354
|
+
return u.signature + ' needs ' + need + ' argument' + (need === '1' ? '' : 's') + ', not ' + given + '. ' +
|
|
355
|
+
u.params.map(function (p) { return p.name + ': ' + p.about + ' (e.g. ' + p.example + ')'; }).join('. ') +
|
|
356
|
+
(u.example ? '. Example: ' + u.example : '');
|
|
357
|
+
}
|
|
358
|
+
|
|
252
359
|
// Lookup by lower-cased name. A null-prototype map, so no formula can reach
|
|
253
360
|
// Object.prototype through a function name.
|
|
254
361
|
var FUNCTIONS = Object.create(null);
|
|
@@ -279,7 +386,9 @@ function registerFunction(entryOrName, fn) {
|
|
|
279
386
|
|
|
280
387
|
/** The list the UI renders: every visible entry, in reference order. */
|
|
281
388
|
function listFunctions() {
|
|
282
|
-
return LIST.filter(function (f) { return !f.hidden; })
|
|
389
|
+
return LIST.filter(function (f) { return !f.hidden; }).map(function (f) {
|
|
390
|
+
return f.special && f.name !== 'if' ? f : Object.assign({}, f, { params: f.params || paramsOf(f) });
|
|
391
|
+
});
|
|
283
392
|
}
|
|
284
393
|
|
|
285
|
-
module.exports = { FUNCTIONS: FUNCTIONS, CATEGORIES: CATEGORIES, lookup: lookup, registerFunction: registerFunction, listFunctions: listFunctions, str: str, num: num, isEmpty: isEmpty };
|
|
394
|
+
module.exports = { FUNCTIONS: FUNCTIONS, CATEGORIES: CATEGORIES, lookup: lookup, usage: usage, arityMessage: arityMessage, registerFunction: registerFunction, listFunctions: listFunctions, str: str, num: num, isEmpty: isEmpty };
|
package/lib/parse.js
CHANGED
|
@@ -110,8 +110,12 @@ function tokenize(src) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// ── grammar ───────────────────────────────────────────────────────────────
|
|
113
|
-
|
|
113
|
+
// options.rows: false when a total names its list first (Workflow);
|
|
114
|
+
// otherwise a total is over a report group's rows — sumif(condition, value).
|
|
115
|
+
// Only the WORDS of an error depend on it.
|
|
116
|
+
function parse(src, options) {
|
|
114
117
|
if (typeof src !== 'string') throw new Error('parse: formula must be text');
|
|
118
|
+
var rows = !(options && options.rows === false);
|
|
115
119
|
if (!src.trim()) fail('The formula is empty');
|
|
116
120
|
var toks = tokenize(src);
|
|
117
121
|
var pos = 0;
|
|
@@ -285,10 +289,7 @@ function parse(src) {
|
|
|
285
289
|
while (isPunct(',')) { next(); args.push(conditional()); }
|
|
286
290
|
}
|
|
287
291
|
expect(')');
|
|
288
|
-
if (args.length < entry.min || args.length > entry.max) {
|
|
289
|
-
var want = entry.min === entry.max ? String(entry.min) : entry.max === Infinity ? 'at least ' + entry.min : entry.min + ' to ' + entry.max;
|
|
290
|
-
fail(entry.name + '() takes ' + want + ' argument' + (want === '1' ? '' : 's') + ', not ' + args.length, nameTok.at);
|
|
291
|
-
}
|
|
292
|
+
if (args.length < entry.min || args.length > entry.max) fail(functions.arityMessage(entry.name, args.length, { rows: rows }), nameTok.at);
|
|
292
293
|
if (entry.name === 'if') return { if: args[0], then: args[1], else: args[2] };
|
|
293
294
|
return { call: entry.name, args: args };
|
|
294
295
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/expression-handler",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "The one formula engine for xeplr: its own parser (no eval, no dependencies), one function list, compiled-once formulas, totals over lists, AND/OR conditions. Runs in Node and the browser.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|