@xeplr/expression-handler 1.0.6 → 2.1.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 +303 -67
- package/index.js +151 -36
- package/lib/analyze.js +71 -0
- package/lib/compile.js +329 -0
- package/lib/dates.js +86 -0
- package/lib/evaluate.js +54 -80
- package/lib/functions.js +276 -41
- package/lib/nlp.js +1 -1
- package/lib/operators.js +23 -1
- package/lib/parse.js +301 -0
- package/lib/text.js +103 -0
- package/package.json +2 -2
- package/lib/formula.js +0 -196
package/lib/functions.js
CHANGED
|
@@ -1,50 +1,285 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// The ONE function list — every function a formula can call, with the words
|
|
2
|
+
// the UI shows for it. BI's reference panel, autocomplete and validation and
|
|
3
|
+
// Workflow's condition picker are all built from this; adding a function is
|
|
4
|
+
// adding one entry here.
|
|
3
5
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// Moved from BI's report-engine/formulaFunctions.js; its implementations and
|
|
7
|
+
// their behaviour are unchanged (BI's saved reports depend on them). The 1.x
|
|
8
|
+
// functions merged in: weekday, between. 1.x's monthNum is now month.
|
|
9
|
+
//
|
|
10
|
+
// EVERY FUNCTION IS TOTAL — nonsense in, null out, never an exception. The
|
|
11
|
+
// engine only evaluates the branch an if() takes, but a function can still see
|
|
12
|
+
// a missing value or a column holding text where a number was expected, and a
|
|
13
|
+
// report must show an empty cell there, not fail.
|
|
14
|
+
//
|
|
15
|
+
// Entry shape:
|
|
16
|
+
// name, category, args, description, example what the UI shows
|
|
17
|
+
// fn the implementation
|
|
18
|
+
// min / max how many arguments it takes (max: Infinity = any number)
|
|
19
|
+
// context fn receives the run context first: fn(ctx, …args)
|
|
20
|
+
// volatile its answer depends on more than its arguments (the date), so
|
|
21
|
+
// it is never folded to a constant at compile time
|
|
22
|
+
// aggregate spans rows — compiled by the engine, see compile.js
|
|
23
|
+
// special syntax rather than a function (if, $thisrow) — listed so the
|
|
24
|
+
// reference shows it, compiled by the engine
|
|
25
|
+
|
|
26
|
+
var dates = require('./dates');
|
|
27
|
+
var asDate = dates.asDate;
|
|
28
|
+
|
|
29
|
+
var CATEGORIES = ['Math', 'Text', 'Date', 'Logic', 'Aggregate'];
|
|
30
|
+
|
|
31
|
+
function str(v) { return v === null || v === undefined ? '' : String(v); }
|
|
32
|
+
function num(v) { var n = Number(v); return isNaN(n) ? null : n; }
|
|
33
|
+
function isEmpty(v) { return v === null || v === undefined || v === ''; }
|
|
34
|
+
function math(f) { return function (v) { var n = num(v); return n === null ? null : f(n); }; }
|
|
35
|
+
function minMax(pick) {
|
|
36
|
+
return function () {
|
|
37
|
+
var vals = arguments.length === 1 && Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments);
|
|
38
|
+
if (!vals.length) return null;
|
|
39
|
+
var out = null;
|
|
40
|
+
for (var i = 0; i < vals.length; i++) {
|
|
41
|
+
var n = num(vals[i]);
|
|
42
|
+
if (n === null) return null;
|
|
43
|
+
out = out === null ? n : pick(out, n);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var LIST = [
|
|
50
|
+
// ── Math ──
|
|
51
|
+
{ name: 'round', category: 'Math', args: 'value, places', min: 1, max: 2,
|
|
52
|
+
description: 'Round to a number of decimal places', example: 'round(amount, 2)',
|
|
53
|
+
// Nothing rounds to 0, not null — Number(null) is 0, the coercion the rest
|
|
54
|
+
// of the engine uses (`revenue - missing` is revenue). Text that isn't a
|
|
55
|
+
// number is null, not NaN.
|
|
56
|
+
fn: function (v, places) {
|
|
57
|
+
var n = num(v);
|
|
58
|
+
if (n === null) return null;
|
|
59
|
+
var p = Math.pow(10, Number(places) || 0);
|
|
60
|
+
return Math.round(n * p) / p;
|
|
61
|
+
} },
|
|
62
|
+
{ name: 'abs', category: 'Math', args: 'value', min: 1, max: 1,
|
|
63
|
+
description: 'Distance from zero, ignoring sign', example: 'abs(variance)', fn: math(Math.abs) },
|
|
64
|
+
{ name: 'ceil', category: 'Math', args: 'value', min: 1, max: 1,
|
|
65
|
+
description: 'Round up to a whole number', example: 'ceil(days)', fn: math(Math.ceil) },
|
|
66
|
+
{ name: 'floor', category: 'Math', args: 'value', min: 1, max: 1,
|
|
67
|
+
description: 'Round down to a whole number', example: 'floor(days)', fn: math(Math.floor) },
|
|
68
|
+
{ name: 'sqrt', category: 'Math', args: 'value', min: 1, max: 1,
|
|
69
|
+
description: 'Square root', example: 'sqrt(area)', fn: math(Math.sqrt) },
|
|
70
|
+
{ name: 'min', category: 'Math', args: 'a, b, …', min: 1, max: Infinity,
|
|
71
|
+
description: 'Smallest of the values given — not across rows', example: 'min(price, cap)',
|
|
72
|
+
fn: minMax(function (a, b) { return b < a ? b : a; }) },
|
|
73
|
+
{ name: 'max', category: 'Math', args: 'a, b, …', min: 1, max: Infinity,
|
|
74
|
+
description: 'Largest of the values given — not across rows', example: 'max(price, floor_price)',
|
|
75
|
+
fn: minMax(function (a, b) { return b > a ? b : a; }) },
|
|
76
|
+
// expr-eval shipped these unlisted, so saved formulas may use them.
|
|
77
|
+
// log is the NATURAL log there (not Excel's base 10), kept for that reason.
|
|
78
|
+
{ name: 'trunc', category: 'Math', args: 'value', min: 1, max: 1,
|
|
79
|
+
description: 'Drop the decimals, toward zero', example: 'trunc(hours)', fn: math(Math.trunc) },
|
|
80
|
+
{ name: 'exp', category: 'Math', args: 'value', min: 1, max: 1,
|
|
81
|
+
description: 'e raised to the value', example: 'exp(rate)', fn: math(Math.exp) },
|
|
82
|
+
{ name: 'ln', category: 'Math', args: 'value', min: 1, max: 1,
|
|
83
|
+
description: 'Natural logarithm', example: 'ln(growth)', fn: math(Math.log) },
|
|
84
|
+
{ name: 'log', category: 'Math', args: 'value', min: 1, max: 1,
|
|
85
|
+
description: 'Natural logarithm — the same as ln, not base 10', example: 'log(growth)', fn: math(Math.log) },
|
|
86
|
+
{ name: 'log10', category: 'Math', args: 'value', min: 1, max: 1,
|
|
87
|
+
description: 'Base-10 logarithm', example: 'log10(size)', fn: math(Math.log10) },
|
|
88
|
+
{ name: 'log2', category: 'Math', args: 'value', min: 1, max: 1,
|
|
89
|
+
description: 'Base-2 logarithm', example: 'log2(size)', fn: math(Math.log2) },
|
|
90
|
+
{ name: 'cbrt', category: 'Math', args: 'value', min: 1, max: 1,
|
|
91
|
+
description: 'Cube root', example: 'cbrt(volume)', fn: math(Math.cbrt) },
|
|
92
|
+
{ name: 'pow', category: 'Math', args: 'value, exponent', min: 2, max: 2,
|
|
93
|
+
description: 'Raise to a power', example: 'pow(rate, years)',
|
|
94
|
+
fn: function (v, e) { var a = num(v), b = num(e); return a === null || b === null ? null : Math.pow(a, b); } },
|
|
95
|
+
|
|
96
|
+
// ── Text ──
|
|
97
|
+
// Text of nothing is empty, never the word "null" — that would show in a report.
|
|
98
|
+
{ name: 'upper', category: 'Text', args: 'text', min: 1, max: 1,
|
|
99
|
+
description: 'Convert to upper case', example: 'upper(city)', fn: function (v) { return str(v).toUpperCase(); } },
|
|
100
|
+
{ name: 'lower', category: 'Text', args: 'text', min: 1, max: 1,
|
|
101
|
+
description: 'Convert to lower case', example: 'lower(email)', fn: function (v) { return str(v).toLowerCase(); } },
|
|
102
|
+
{ name: 'trim', category: 'Text', args: 'text', min: 1, max: 1,
|
|
103
|
+
description: 'Remove leading and trailing spaces', example: 'trim(name)', fn: function (v) { return str(v).trim(); } },
|
|
104
|
+
{ name: 'concat', category: 'Text', args: 'a, b, …', min: 0, max: Infinity,
|
|
105
|
+
description: 'Join values into one piece of text', example: 'concat(first, " ", last)',
|
|
106
|
+
fn: function () { var s = ''; for (var i = 0; i < arguments.length; i++) s += str(arguments[i]); return s; } },
|
|
107
|
+
// 1-based, like every spreadsheet — a report author is not a programmer.
|
|
108
|
+
{ name: 'substring', category: 'Text', args: 'text, start, count', min: 2, max: 3,
|
|
109
|
+
description: 'Part of a value, counting from 1', example: 'substring(code, 1, 3)',
|
|
110
|
+
fn: function (v, start, count) {
|
|
111
|
+
var s = str(v);
|
|
112
|
+
var from = Math.max(0, (Number(start) || 1) - 1);
|
|
113
|
+
return count === undefined ? s.slice(from) : s.slice(from, from + (Number(count) || 0));
|
|
114
|
+
} },
|
|
115
|
+
{ name: 'left', category: 'Text', args: 'text, count', min: 2, max: 2,
|
|
116
|
+
description: 'First few characters', example: 'left(deptCode, 3)',
|
|
117
|
+
fn: function (v, count) { return str(v).slice(0, Number(count) || 0); } },
|
|
118
|
+
{ name: 'right', category: 'Text', args: 'text, count', min: 2, max: 2,
|
|
119
|
+
description: 'Last few characters', example: 'right(invoice, 4)',
|
|
120
|
+
fn: function (v, count) { var n = Number(count) || 0; return n <= 0 ? '' : str(v).slice(-n); } },
|
|
121
|
+
{ name: 'replace', category: 'Text', args: 'text, find, replace with', min: 3, max: 3,
|
|
122
|
+
description: 'Swap one piece of text for another', example: 'replace(phone, "-", "")',
|
|
123
|
+
fn: function (v, find, into) { return str(v).split(str(find)).join(str(into)); } },
|
|
124
|
+
{ name: 'contains', category: 'Text', args: 'text, find', min: 2, max: 2,
|
|
125
|
+
description: 'True when the text contains it', example: 'contains(notes, "urgent")',
|
|
126
|
+
fn: function (v, sub) { return str(v).indexOf(str(sub)) !== -1; } },
|
|
127
|
+
{ name: 'startsWith', category: 'Text', args: 'text, find', min: 2, max: 2,
|
|
128
|
+
description: 'True when the text begins with it', example: 'startsWith(sku, "IN")',
|
|
129
|
+
fn: function (v, sub) { return str(v).startsWith(str(sub)); } },
|
|
130
|
+
{ name: 'endsWith', category: 'Text', args: 'text, find', min: 2, max: 2,
|
|
131
|
+
description: 'True when the text ends with it', example: 'endsWith(file, ".pdf")',
|
|
132
|
+
fn: function (v, sub) { return str(v).endsWith(str(sub)); } },
|
|
133
|
+
{ name: 'length', category: 'Text', args: 'text', min: 1, max: 1,
|
|
134
|
+
description: 'How many characters', example: 'length(name)',
|
|
135
|
+
fn: function (v) { return Array.isArray(v) ? v.length : str(v).length; } },
|
|
136
|
+
|
|
137
|
+
// ── Date ── (local time; a date-only string is that calendar day — see dates.js)
|
|
138
|
+
{ name: 'year', category: 'Date', args: 'date', min: 1, max: 1,
|
|
139
|
+
description: 'The year, as a number', example: 'year(order_date)',
|
|
140
|
+
fn: function (v) { var d = asDate(v); return d ? d.getFullYear() : null; } },
|
|
141
|
+
{ name: 'month', category: 'Date', args: 'date', min: 1, max: 1,
|
|
142
|
+
description: 'The month, 1–12', example: 'month(order_date)',
|
|
143
|
+
fn: function (v) { var d = asDate(v); return d ? d.getMonth() + 1 : null; } },
|
|
144
|
+
{ name: 'day', category: 'Date', args: 'date', min: 1, max: 1,
|
|
145
|
+
description: 'Day of the month', example: 'day(order_date)',
|
|
146
|
+
fn: function (v) { var d = asDate(v); return d ? d.getDate() : null; } },
|
|
147
|
+
{ name: 'quarter', category: 'Date', args: 'date', min: 1, max: 1,
|
|
148
|
+
description: 'The quarter, 1–4', example: 'quarter(order_date)',
|
|
149
|
+
fn: function (v) { var d = asDate(v); return d ? Math.floor(d.getMonth() / 3) + 1 : null; } },
|
|
150
|
+
{ name: 'weekday', category: 'Date', args: 'date', min: 1, max: 1,
|
|
151
|
+
description: 'The day of the week by name', example: 'weekday(order_date)',
|
|
152
|
+
fn: function (v) { var d = asDate(v); return d ? dates.WEEKDAYS[d.getDay()] : null; } },
|
|
153
|
+
{ name: 'datediff', category: 'Date', args: 'from, to', min: 2, max: 2,
|
|
154
|
+
description: 'Whole days between two dates', example: 'datediff(ordered, shipped)',
|
|
155
|
+
// Later minus earlier.
|
|
156
|
+
fn: function (a, b) {
|
|
157
|
+
var from = asDate(a), to = asDate(b);
|
|
158
|
+
if (!from || !to) return null;
|
|
159
|
+
return Math.round((to.getTime() - from.getTime()) / dates.MS_PER_DAY);
|
|
160
|
+
} },
|
|
161
|
+
{ name: 'dateadd', category: 'Date', args: 'date, days', min: 2, max: 2,
|
|
162
|
+
description: 'Move a date by a number of days', example: 'dateadd(order_date, 30)',
|
|
163
|
+
// Built from calendar parts, not by adding milliseconds, so a DST boundary
|
|
164
|
+
// can't turn "+1 day" into 23 or 25 hours and land on the wrong date.
|
|
165
|
+
fn: function (v, days) {
|
|
166
|
+
var d = asDate(v);
|
|
167
|
+
if (!d) return null;
|
|
168
|
+
return dates.isoLocal(new Date(d.getFullYear(), d.getMonth(), d.getDate() + (Number(days) || 0)));
|
|
169
|
+
} },
|
|
170
|
+
{ name: 'monthname', category: 'Date', args: 'date or month number', min: 1, max: 1,
|
|
171
|
+
description: 'The month by name. Reads well, but sorts alphabetically — group by monthkey instead', example: 'monthname(order_date)',
|
|
172
|
+
// A number is always a month, never an epoch timestamp — monthname(13)
|
|
173
|
+
// must not read as 13ms after 1970 and answer "January".
|
|
174
|
+
fn: function (v) {
|
|
175
|
+
if (typeof v === 'number') return v >= 1 && v <= 12 ? dates.MONTH_NAMES[v - 1] : null;
|
|
176
|
+
var d = asDate(v);
|
|
177
|
+
return d ? dates.MONTH_NAMES[d.getMonth()] : null;
|
|
178
|
+
} },
|
|
179
|
+
// Grouping KEYS: they sort correctly as text ("2024-06" before "2024-10"),
|
|
180
|
+
// which is what makes them safe to pivot or group on, unlike monthname.
|
|
181
|
+
{ name: 'monthkey', category: 'Date', args: 'date', min: 1, max: 1,
|
|
182
|
+
description: 'The month as 2024-06 — sorts chronologically, so group or pivot by this', example: 'monthkey(order_date)',
|
|
183
|
+
fn: function (v) { var d = asDate(v); return d ? d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') : null; } },
|
|
184
|
+
{ name: 'quarterkey', category: 'Date', args: 'date', min: 1, max: 1,
|
|
185
|
+
description: 'The quarter as 2024-Q2 — sorts chronologically', example: 'quarterkey(order_date)',
|
|
186
|
+
fn: function (v) { var d = asDate(v); return d ? d.getFullYear() + '-Q' + (Math.floor(d.getMonth() / 3) + 1) : null; } },
|
|
187
|
+
{ name: 'daykey', category: 'Date', args: 'date', min: 1, max: 1,
|
|
188
|
+
description: 'The day as 2024-06-17 — sorts chronologically', example: 'daykey(order_date)',
|
|
189
|
+
fn: function (v) { var d = asDate(v); return d ? dates.isoLocal(d) : null; } },
|
|
190
|
+
// Predicates, not date maths — they exist to go inside sumif / countif. Both
|
|
191
|
+
// ends matter: a date later this year is NOT year-to-date. False, not null,
|
|
192
|
+
// on nothing — the honest answer, and it composes with `not`.
|
|
193
|
+
{ name: 'ytd', category: 'Date', args: 'date', min: 1, max: 1, context: true, volatile: true,
|
|
194
|
+
description: 'True when the date is in this calendar year, up to today. Pair it with sumif', example: 'ytd(order_date)',
|
|
195
|
+
fn: function (ctx, v) {
|
|
196
|
+
var d = asDate(v);
|
|
197
|
+
if (!d) return false;
|
|
198
|
+
var now = ctx.now();
|
|
199
|
+
return d.getFullYear() === now.year && d.getTime() <= now.end.getTime();
|
|
200
|
+
} },
|
|
201
|
+
{ name: 'fytd', category: 'Date', args: 'date', min: 1, max: 1, context: true, volatile: true,
|
|
202
|
+
description: 'True when the date is in this financial year, up to today. Pair it with sumif', example: 'fytd(order_date)',
|
|
203
|
+
fn: function (ctx, v) {
|
|
204
|
+
var d = asDate(v);
|
|
205
|
+
if (!d) return false;
|
|
206
|
+
var now = ctx.now();
|
|
207
|
+
return d.getTime() >= now.fyStart.getTime() && d.getTime() <= now.end.getTime();
|
|
208
|
+
} },
|
|
209
|
+
|
|
210
|
+
// ── Logic ──
|
|
211
|
+
{ name: 'if', category: 'Logic', args: 'condition, then, otherwise', min: 3, max: 3, special: true,
|
|
212
|
+
description: 'Choose between two values. Nests freely', example: 'if(qty > 10, "bulk", "single")' },
|
|
213
|
+
// Empty text counts as missing: a database NULL and an empty cell mean the
|
|
214
|
+
// same thing to someone reading a report. Zero does not.
|
|
215
|
+
{ name: 'coalesce', category: 'Logic', args: 'a, b, …', min: 1, max: Infinity,
|
|
216
|
+
description: 'First value that is not empty', example: 'coalesce(nickname, name)',
|
|
217
|
+
fn: function () { for (var i = 0; i < arguments.length; i++) if (!isEmpty(arguments[i])) return arguments[i]; return null; } },
|
|
218
|
+
{ name: 'ifnull', category: 'Logic', args: 'value, fallback', min: 2, max: 2,
|
|
219
|
+
description: 'Use a fallback when the value is empty', example: 'ifnull(discount, 0)',
|
|
220
|
+
fn: function (v, fallback) { return isEmpty(v) ? fallback : v; } },
|
|
221
|
+
{ name: 'isnull', category: 'Logic', args: 'value', min: 1, max: 1,
|
|
222
|
+
description: 'True when the value is empty', example: 'isnull(closed_date)',
|
|
223
|
+
fn: function (v) { return isEmpty(v); } },
|
|
224
|
+
{ name: 'between', category: 'Logic', args: 'value, low, high', min: 3, max: 3,
|
|
225
|
+
description: 'True when the value is from low to high, both included', example: 'between(score, 600, 750)',
|
|
226
|
+
// Numbers compare as numbers; anything else as text, which orders ISO
|
|
227
|
+
// dates ("2026-01-31") correctly. Nothing is never between.
|
|
228
|
+
fn: function (v, lo, hi) {
|
|
229
|
+
if (isEmpty(v) || isEmpty(lo) || isEmpty(hi)) return false;
|
|
230
|
+
var x = num(v), a = num(lo), b = num(hi);
|
|
231
|
+
if (x !== null && a !== null && b !== null) return x >= a && x <= b;
|
|
232
|
+
var s = String(v);
|
|
233
|
+
return s >= String(lo) && s <= String(hi);
|
|
234
|
+
} },
|
|
235
|
+
|
|
236
|
+
// ── Aggregate ── spans ROWS; compiled by the engine (compile.js).
|
|
237
|
+
// In BI the rows are the current group: sumif(status = "paid", amount).
|
|
238
|
+
// Anywhere else the list comes first: sumif(orders, status = "paid", amount).
|
|
239
|
+
{ name: 'sum', category: 'Aggregate', args: 'value', min: 1, max: 2, aggregate: true, shape: { cond: false, value: true },
|
|
240
|
+
description: 'Total across the rows in this group', example: 'sum(amount)' },
|
|
241
|
+
{ 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 == "refunded", amount)' },
|
|
243
|
+
{ name: 'countif', category: 'Aggregate', args: 'condition', min: 1, max: 2, aggregate: true, shape: { cond: true, value: false },
|
|
244
|
+
description: 'How many rows match', example: 'countif(qty > 10)' },
|
|
245
|
+
{ 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 == "paid", customer_id)' },
|
|
247
|
+
// Not a function — a reference, usable inside any aggregate's condition.
|
|
248
|
+
{ 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 == $thisrow.month, revenue)' }
|
|
12
250
|
];
|
|
13
|
-
var WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
14
251
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
252
|
+
// Lookup by lower-cased name. A null-prototype map, so no formula can reach
|
|
253
|
+
// Object.prototype through a function name.
|
|
254
|
+
var FUNCTIONS = Object.create(null);
|
|
255
|
+
LIST.forEach(function (f) { if (!f.special || f.name === 'if') FUNCTIONS[f.name.toLowerCase()] = f; });
|
|
256
|
+
// 1.x name, kept so stored 1.x conditions still run. Not listed.
|
|
257
|
+
FUNCTIONS.monthnum = Object.assign({}, FUNCTIONS.month, { name: 'monthNum', hidden: true });
|
|
258
|
+
|
|
259
|
+
function lookup(name) {
|
|
260
|
+
return FUNCTIONS[String(name).toLowerCase()] || null;
|
|
19
261
|
}
|
|
20
262
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
weekday: function (v) { var d = toDate(v); return d ? WEEKDAYS[d.getUTCDay()] : null; } // 'Monday'
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function registerFunction(name, fn) {
|
|
40
|
-
if (typeof fn !== 'function') throw new Error('registerFunction: fn must be a function');
|
|
41
|
-
FUNCTIONS[name] = fn;
|
|
263
|
+
// registerFunction(entry) or, as in 1.x, registerFunction(name, fn).
|
|
264
|
+
function registerFunction(entryOrName, fn) {
|
|
265
|
+
var entry = typeof entryOrName === 'string'
|
|
266
|
+
? { name: entryOrName, fn: fn, category: 'Custom', args: '', description: '', example: '' }
|
|
267
|
+
: Object.assign({}, entryOrName);
|
|
268
|
+
if (!entry.name || typeof entry.fn !== 'function') throw new Error('registerFunction: needs a name and a function');
|
|
269
|
+
if (entry.min === undefined) entry.min = 0;
|
|
270
|
+
if (entry.max === undefined) entry.max = Infinity;
|
|
271
|
+
var key = entry.name.toLowerCase();
|
|
272
|
+
if (FUNCTIONS[key] && (FUNCTIONS[key].special || FUNCTIONS[key].aggregate)) {
|
|
273
|
+
throw new Error('registerFunction: "' + entry.name + '" is built into the language');
|
|
274
|
+
}
|
|
275
|
+
var at = LIST.findIndex(function (f) { return f.name.toLowerCase() === key; });
|
|
276
|
+
if (at === -1) LIST.push(entry); else LIST[at] = entry;
|
|
277
|
+
FUNCTIONS[key] = entry;
|
|
42
278
|
}
|
|
43
279
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return fn(value);
|
|
280
|
+
/** The list the UI renders: every visible entry, in reference order. */
|
|
281
|
+
function listFunctions() {
|
|
282
|
+
return LIST.filter(function (f) { return !f.hidden; });
|
|
48
283
|
}
|
|
49
284
|
|
|
50
|
-
module.exports = { FUNCTIONS, registerFunction,
|
|
285
|
+
module.exports = { FUNCTIONS: FUNCTIONS, CATEGORIES: CATEGORIES, lookup: lookup, registerFunction: registerFunction, listFunctions: listFunctions, str: str, num: num, isEmpty: isEmpty };
|
package/lib/nlp.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// and callers can feature-detect. Wire an LLM/grammar backend here later.
|
|
8
8
|
|
|
9
9
|
function parse(_text, _options) {
|
|
10
|
-
throw new Error('nlp wrapper: not implemented yet (stub). Use
|
|
10
|
+
throw new Error('nlp wrapper: not implemented yet (stub). Use xf.parse / xf.compile or the structured JSON API.');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
module.exports = {
|
package/lib/operators.js
CHANGED
|
@@ -59,6 +59,28 @@ OPERATORS.equals = OPERATORS.eq;
|
|
|
59
59
|
OPERATORS.notEquals = OPERATORS.neq;
|
|
60
60
|
OPERATORS.doesNotContain = OPERATORS.notContains;
|
|
61
61
|
|
|
62
|
+
// THE SAME OPERATORS, IGNORING CASE — chosen per formula (compile option
|
|
63
|
+
// `ignoreCase`), never by changing the ones above: Workflow's conditions and
|
|
64
|
+
// every 1.x caller keep exact matching. Only text is folded; numbers and
|
|
65
|
+
// null compare exactly as before.
|
|
66
|
+
function fold(v) { return typeof v === 'string' ? v.toLowerCase() : v; }
|
|
67
|
+
function looseEqIgnoreCase(a, b) { return looseEq(fold(a), fold(b)); }
|
|
68
|
+
function lower(v) { return str(v).toLowerCase(); }
|
|
69
|
+
|
|
70
|
+
var IGNORE_CASE = {
|
|
71
|
+
eq: { arity: 2, fn: function (a, b) { return looseEqIgnoreCase(a, b); } },
|
|
72
|
+
neq: { arity: 2, fn: function (a, b) { return !looseEqIgnoreCase(a, b); } },
|
|
73
|
+
contains: { arity: 2, fn: function (a, b) { return lower(a).indexOf(lower(b)) !== -1; } },
|
|
74
|
+
notContains: { arity: 2, fn: function (a, b) { return lower(a).indexOf(lower(b)) === -1; } },
|
|
75
|
+
startsWith: { arity: 2, fn: function (a, b) { return lower(a).indexOf(lower(b)) === 0; } },
|
|
76
|
+
endsWith: { arity: 2, fn: function (a, b) { var s = lower(a), t = lower(b); return t === '' || s.slice(-t.length) === t; } },
|
|
77
|
+
in: { arity: 2, fn: function (a, b) { return Array.isArray(b) && b.some(function (x) { return looseEqIgnoreCase(a, x); }); } },
|
|
78
|
+
notIn: { arity: 2, fn: function (a, b) { return !(Array.isArray(b) && b.some(function (x) { return looseEqIgnoreCase(a, x); })); } }
|
|
79
|
+
};
|
|
80
|
+
IGNORE_CASE.equals = IGNORE_CASE.eq;
|
|
81
|
+
IGNORE_CASE.notEquals = IGNORE_CASE.neq;
|
|
82
|
+
IGNORE_CASE.doesNotContain = IGNORE_CASE.notContains;
|
|
83
|
+
|
|
62
84
|
function registerOperator(name, def) {
|
|
63
85
|
if (typeof def === 'function') def = { arity: 2, fn: def };
|
|
64
86
|
if (!def || typeof def.fn !== 'function') throw new Error('registerOperator: needs a function or { arity, fn }');
|
|
@@ -66,4 +88,4 @@ function registerOperator(name, def) {
|
|
|
66
88
|
OPERATORS[name] = def;
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
module.exports = { OPERATORS, registerOperator, looseEq, num, str, isNil };
|
|
91
|
+
module.exports = { OPERATORS, IGNORE_CASE, registerOperator, looseEq, looseEqIgnoreCase, num, str, isNil };
|