@bpmnkit/feel 0.0.20 → 0.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 +3 -1
- package/dist/ast.d.ts +4 -0
- package/dist/builtins.d.ts +7 -0
- package/dist/builtins.js +824 -190
- package/dist/evaluator.d.ts +4 -1
- package/dist/evaluator.js +251 -139
- package/dist/formatter.js +16 -1
- package/dist/highlighter.js +22 -9
- package/dist/index.d.ts +1 -1
- package/dist/lexer.d.ts +7 -0
- package/dist/lexer.js +169 -51
- package/dist/parser.d.ts +11 -2
- package/dist/parser.js +333 -94
- package/dist/types.js +35 -2
- package/package.json +5 -2
package/dist/builtins.js
CHANGED
|
@@ -41,14 +41,43 @@ function epochDaysToDate(days) {
|
|
|
41
41
|
}
|
|
42
42
|
return { type: "date", year, month, day: remaining + 1 };
|
|
43
43
|
}
|
|
44
|
+
// The widest year XSD's date types allow, which bounds DMN's too.
|
|
45
|
+
const MAX_YEAR = 999999999;
|
|
46
|
+
/** Builds a date, or null when the day does not exist in that month. */
|
|
47
|
+
function makeDate(year, month, day) {
|
|
48
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day))
|
|
49
|
+
return null;
|
|
50
|
+
if (Math.abs(year) > MAX_YEAR)
|
|
51
|
+
return null;
|
|
52
|
+
if (month < 1 || month > 12)
|
|
53
|
+
return null;
|
|
54
|
+
if (day < 1 || day > daysInMonth(year, month))
|
|
55
|
+
return null;
|
|
56
|
+
return { type: "date", year, month, day };
|
|
57
|
+
}
|
|
44
58
|
function parseDate(s) {
|
|
45
|
-
|
|
59
|
+
// Exactly four year digits: "01211" carries a leading zero and
|
|
60
|
+
// "9999999999" is past any calendar, and neither is a date.
|
|
61
|
+
const m = /^(-?)(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
46
62
|
if (!m)
|
|
47
63
|
return null;
|
|
48
|
-
|
|
64
|
+
const year = Number(m[2]) * (m[1] === "-" ? -1 : 1);
|
|
65
|
+
return makeDate(year, Number(m[3]), Number(m[4]));
|
|
66
|
+
}
|
|
67
|
+
// The largest UTC offset XSD allows.
|
|
68
|
+
const MAX_OFFSET_SECONDS = 18 * 3600;
|
|
69
|
+
/** True for a zone name the platform's time zone database knows. */
|
|
70
|
+
function isKnownTimezone(name) {
|
|
71
|
+
try {
|
|
72
|
+
new Intl.DateTimeFormat("en-US", { timeZone: name });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
49
78
|
}
|
|
50
79
|
function parseTime(s) {
|
|
51
|
-
const m = /^(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(?:([+-])(\d{2}):(\d{2})
|
|
80
|
+
const m = /^(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(?:([+-])(\d{2}):(\d{2})(?::(\d{2}))?|Z)?(?:@(.+))?$/.exec(s);
|
|
52
81
|
if (!m)
|
|
53
82
|
return null;
|
|
54
83
|
const hour = Number(m[1]);
|
|
@@ -58,19 +87,39 @@ function parseTime(s) {
|
|
|
58
87
|
let timezone;
|
|
59
88
|
if (m[4]) {
|
|
60
89
|
const sign = m[4] === "+" ? 1 : -1;
|
|
61
|
-
offsetSeconds = sign * (Number(m[5]) * 3600 + Number(m[6]) * 60);
|
|
90
|
+
offsetSeconds = sign * (Number(m[5]) * 3600 + Number(m[6]) * 60 + (m[7] ? Number(m[7]) : 0));
|
|
91
|
+
if (Math.abs(offsetSeconds) > MAX_OFFSET_SECONDS)
|
|
92
|
+
return null;
|
|
62
93
|
}
|
|
63
|
-
else if (
|
|
94
|
+
else if (s.includes("Z")) {
|
|
64
95
|
offsetSeconds = 0;
|
|
65
96
|
}
|
|
66
|
-
if (m[8])
|
|
97
|
+
if (m[8]) {
|
|
98
|
+
// A zone and an offset name the same thing twice, and may disagree.
|
|
99
|
+
if (offsetSeconds !== undefined)
|
|
100
|
+
return null;
|
|
101
|
+
if (!isKnownTimezone(m[8]))
|
|
102
|
+
return null;
|
|
67
103
|
timezone = m[8];
|
|
104
|
+
}
|
|
105
|
+
if (!isValidTime(hour, minute, second))
|
|
106
|
+
return null;
|
|
68
107
|
return { type: "time", hour, minute, second, offsetSeconds, timezone };
|
|
69
108
|
}
|
|
109
|
+
/** 24:00:00 is the end-of-day form the ISO calendar allows; 24:00:01 is not. */
|
|
110
|
+
function isValidTime(hour, minute, second) {
|
|
111
|
+
if (hour < 0 || hour > 24 || minute < 0 || minute > 59 || second < 0 || second >= 60)
|
|
112
|
+
return false;
|
|
113
|
+
return hour !== 24 || (minute === 0 && second === 0);
|
|
114
|
+
}
|
|
70
115
|
function parseDateTime(s) {
|
|
71
116
|
const idx = s.indexOf("T");
|
|
72
|
-
if (idx < 0)
|
|
73
|
-
|
|
117
|
+
if (idx < 0) {
|
|
118
|
+
const dateOnly = parseDate(s);
|
|
119
|
+
return dateOnly
|
|
120
|
+
? { type: "date-time", date: dateOnly, time: { type: "time", hour: 0, minute: 0, second: 0 } }
|
|
121
|
+
: null;
|
|
122
|
+
}
|
|
74
123
|
const d = parseDate(s.slice(0, idx));
|
|
75
124
|
const t = parseTime(s.slice(idx + 1));
|
|
76
125
|
if (!d || !t)
|
|
@@ -80,14 +129,15 @@ function parseDateTime(s) {
|
|
|
80
129
|
function parseDuration(s) {
|
|
81
130
|
// P[n]Y[n]M or P[n]DT[n]H[n]M[n]S
|
|
82
131
|
const ymMatch = /^-?P(\d+Y)?(\d+M)?$/.exec(s);
|
|
83
|
-
if (ymMatch) {
|
|
132
|
+
if (ymMatch && /\d/.test(s)) {
|
|
84
133
|
const sign = s.startsWith("-") ? -1 : 1;
|
|
85
134
|
const years = ymMatch[1] ? Number(ymMatch[1].slice(0, -1)) : 0;
|
|
86
135
|
const months = ymMatch[2] ? Number(ymMatch[2].slice(0, -1)) : 0;
|
|
87
|
-
|
|
136
|
+
// "|| 0" keeps a negative zero out: -P0M is the same duration as P0M.
|
|
137
|
+
return { type: "years-months-duration", months: sign * (years * 12 + months) || 0 };
|
|
88
138
|
}
|
|
89
|
-
const dtMatch = /^-?P(\d+D)?(?:T(\d+H)?(\d+M)?(\d+(?:\.\d
|
|
90
|
-
if (dtMatch && s
|
|
139
|
+
const dtMatch = /^-?P(\d+D)?(?:T(\d+H)?(\d+M)?(\d+(?:\.\d*)?S)?)?$/.exec(s);
|
|
140
|
+
if (dtMatch && /\d/.test(s)) {
|
|
91
141
|
const sign = s.startsWith("-") ? -1 : 1;
|
|
92
142
|
const days = dtMatch[1] ? Number(dtMatch[1].slice(0, -1)) : 0;
|
|
93
143
|
const hours = dtMatch[2] ? Number(dtMatch[2].slice(0, -1)) : 0;
|
|
@@ -95,7 +145,7 @@ function parseDuration(s) {
|
|
|
95
145
|
const seconds = dtMatch[4] ? Number(dtMatch[4].slice(0, -1)) : 0;
|
|
96
146
|
return {
|
|
97
147
|
type: "days-time-duration",
|
|
98
|
-
seconds: sign * (days * 86400 + hours * 3600 + minutes * 60 + seconds),
|
|
148
|
+
seconds: sign * (days * 86400 + hours * 3600 + minutes * 60 + seconds) || 0,
|
|
99
149
|
};
|
|
100
150
|
}
|
|
101
151
|
return null;
|
|
@@ -120,8 +170,14 @@ function parseTemporal(raw) {
|
|
|
120
170
|
function formatDate(d) {
|
|
121
171
|
return `${String(d.year).padStart(4, "0")}-${String(d.month).padStart(2, "0")}-${String(d.day).padStart(2, "0")}`;
|
|
122
172
|
}
|
|
173
|
+
/** Two digits before the decimal point, and the fraction as written. */
|
|
174
|
+
function formatSeconds(second) {
|
|
175
|
+
const whole = Math.floor(second);
|
|
176
|
+
const fraction = `${second}`.split(".")[1];
|
|
177
|
+
return String(whole).padStart(2, "0") + (fraction ? `.${fraction}` : "");
|
|
178
|
+
}
|
|
123
179
|
function formatTime(t) {
|
|
124
|
-
let s = `${String(t.hour).padStart(2, "0")}:${String(t.minute).padStart(2, "0")}:${
|
|
180
|
+
let s = `${String(t.hour).padStart(2, "0")}:${String(t.minute).padStart(2, "0")}:${formatSeconds(t.second)}`;
|
|
125
181
|
if (t.offsetSeconds !== undefined) {
|
|
126
182
|
if (t.offsetSeconds === 0) {
|
|
127
183
|
s += "Z";
|
|
@@ -129,7 +185,11 @@ function formatTime(t) {
|
|
|
129
185
|
else {
|
|
130
186
|
const sign = t.offsetSeconds >= 0 ? "+" : "-";
|
|
131
187
|
const abs = Math.abs(t.offsetSeconds);
|
|
132
|
-
|
|
188
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
189
|
+
s += `${sign}${pad(Math.floor(abs / 3600))}:${pad(Math.floor((abs % 3600) / 60))}`;
|
|
190
|
+
// Offsets are written to the second only when they have one.
|
|
191
|
+
if (abs % 60 !== 0)
|
|
192
|
+
s += `:${pad(abs % 60)}`;
|
|
133
193
|
}
|
|
134
194
|
}
|
|
135
195
|
if (t.timezone)
|
|
@@ -150,14 +210,57 @@ function dayOfYear(d) {
|
|
|
150
210
|
// -------------------------------------------------------------------------
|
|
151
211
|
// Helpers
|
|
152
212
|
// -------------------------------------------------------------------------
|
|
213
|
+
/**
|
|
214
|
+
* A number argument. FEEL does not coerce, so "1.5" is not a number here;
|
|
215
|
+
* number() is the way to convert one.
|
|
216
|
+
*/
|
|
153
217
|
function toNum(v) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* A rounding scale: an optional integer. Absent means 0, but an explicitly
|
|
222
|
+
* null or non-numeric scale is an error. Beyond float64's reach the value is
|
|
223
|
+
* already exact at that scale, so it is returned unrounded.
|
|
224
|
+
*/
|
|
225
|
+
const MIN_SCALE = -6111;
|
|
226
|
+
const MAX_SCALE = 6176;
|
|
227
|
+
const SCALE_LIMIT = 300;
|
|
228
|
+
function toScale(v) {
|
|
229
|
+
if (v === undefined)
|
|
230
|
+
return 0;
|
|
231
|
+
const n = toNum(v);
|
|
232
|
+
if (n === null)
|
|
233
|
+
return null;
|
|
234
|
+
const scale = Math.trunc(n);
|
|
235
|
+
return scale < MIN_SCALE || scale > MAX_SCALE ? null : scale;
|
|
236
|
+
}
|
|
237
|
+
/** Rounds `n` at `scale` with the given rounding of a value exactly halfway. */
|
|
238
|
+
function roundAt(n, scale, round) {
|
|
239
|
+
if (Math.abs(scale) > SCALE_LIMIT)
|
|
240
|
+
return n;
|
|
241
|
+
const factor = 10 ** scale;
|
|
242
|
+
// Re-reading the scaled value through its decimal form keeps a product like
|
|
243
|
+
// 1.005 * 100 from landing just under the halfway point it should sit on.
|
|
244
|
+
const scaled = Number(`${n}e${scale}`);
|
|
245
|
+
return Number(`${round(scaled)}e${-scale}`) || round(scaled) / factor;
|
|
246
|
+
}
|
|
247
|
+
/** Round half to even, the rounding DMN's decimal() uses. */
|
|
248
|
+
function roundHalfEven(x) {
|
|
249
|
+
const floor = Math.floor(x);
|
|
250
|
+
const diff = x - floor;
|
|
251
|
+
if (diff > 0.5)
|
|
252
|
+
return floor + 1;
|
|
253
|
+
if (diff < 0.5)
|
|
254
|
+
return floor;
|
|
255
|
+
return floor % 2 === 0 ? floor : floor + 1;
|
|
256
|
+
}
|
|
257
|
+
/** Round half away from zero. */
|
|
258
|
+
function roundHalfUp(x) {
|
|
259
|
+
return x >= 0 ? Math.floor(x + 0.5) : Math.ceil(x - 0.5);
|
|
260
|
+
}
|
|
261
|
+
/** Round half toward zero. */
|
|
262
|
+
function roundHalfDown(x) {
|
|
263
|
+
return x >= 0 ? Math.ceil(x - 0.5) : Math.floor(x + 0.5);
|
|
161
264
|
}
|
|
162
265
|
function toStr(v) {
|
|
163
266
|
if (typeof v === "string")
|
|
@@ -188,6 +291,55 @@ function inRange(v, r) {
|
|
|
188
291
|
const endOk = r.endIncluded ? cmpEnd <= 0 : cmpEnd < 0;
|
|
189
292
|
return startOk && endOk;
|
|
190
293
|
}
|
|
294
|
+
const offsetCache = new Map();
|
|
295
|
+
/**
|
|
296
|
+
* The UTC offset a zone is on at the given wall-clock day. Resolved through
|
|
297
|
+
* the platform's time zone database, so it follows daylight saving: Melbourne
|
|
298
|
+
* is +11:00 in April and +10:00 in October.
|
|
299
|
+
*/
|
|
300
|
+
function zoneOffsetSeconds(timezone, epochDays) {
|
|
301
|
+
const key = `${timezone}/${epochDays}`;
|
|
302
|
+
const cached = offsetCache.get(key);
|
|
303
|
+
if (cached !== undefined)
|
|
304
|
+
return cached;
|
|
305
|
+
let offset = 0;
|
|
306
|
+
try {
|
|
307
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
308
|
+
timeZone: timezone,
|
|
309
|
+
timeZoneName: "longOffset",
|
|
310
|
+
}).formatToParts(new Date(epochDays * 86400_000));
|
|
311
|
+
const name = parts.find((part) => part.type === "timeZoneName")?.value ?? "";
|
|
312
|
+
const m = /GMT([+-])(\d{2}):(\d{2})/.exec(name);
|
|
313
|
+
if (m)
|
|
314
|
+
offset = (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 3600 + Number(m[3]) * 60);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
offset = 0;
|
|
318
|
+
}
|
|
319
|
+
if (offsetCache.size > 512)
|
|
320
|
+
offsetCache.clear();
|
|
321
|
+
offsetCache.set(key, offset);
|
|
322
|
+
return offset;
|
|
323
|
+
}
|
|
324
|
+
/** The offset a time is on, or undefined when it is a local time. */
|
|
325
|
+
function offsetOf(t, epochDays) {
|
|
326
|
+
if (t.offsetSeconds !== undefined)
|
|
327
|
+
return t.offsetSeconds;
|
|
328
|
+
if (t.timezone !== undefined)
|
|
329
|
+
return zoneOffsetSeconds(t.timezone, epochDays);
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Seconds since midnight, shifted to UTC where the time says which UTC it
|
|
334
|
+
* means. A local time carries no offset, so it is left where it is.
|
|
335
|
+
*/
|
|
336
|
+
function timeToSeconds(t, epochDays = 0) {
|
|
337
|
+
return t.hour * 3600 + t.minute * 60 + t.second - (offsetOf(t, epochDays) ?? 0);
|
|
338
|
+
}
|
|
339
|
+
/** True when both times are local, or both say which UTC they mean. */
|
|
340
|
+
function sameTimeKind(a, b, epochA, epochB) {
|
|
341
|
+
return (offsetOf(a, epochA) === undefined) === (offsetOf(b, epochB) === undefined);
|
|
342
|
+
}
|
|
191
343
|
function compareValues(a, b) {
|
|
192
344
|
if (typeof a === "number" && typeof b === "number")
|
|
193
345
|
return a - b;
|
|
@@ -195,12 +347,30 @@ function compareValues(a, b) {
|
|
|
195
347
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
196
348
|
if (isFeelDate(a) && isFeelDate(b))
|
|
197
349
|
return dateToEpochDays(a) - dateToEpochDays(b);
|
|
350
|
+
if (isFeelTime(a) && isFeelTime(b)) {
|
|
351
|
+
// A local time and one at a known offset name different things and
|
|
352
|
+
// cannot be ordered against each other.
|
|
353
|
+
if (!sameTimeKind(a, b, 0, 0))
|
|
354
|
+
return null;
|
|
355
|
+
return timeToSeconds(a) - timeToSeconds(b);
|
|
356
|
+
}
|
|
357
|
+
if (isFeelDateTime(a) && isFeelDateTime(b)) {
|
|
358
|
+
const epochA = dateToEpochDays(a.date);
|
|
359
|
+
const epochB = dateToEpochDays(b.date);
|
|
360
|
+
if (!sameTimeKind(a.time, b.time, epochA, epochB))
|
|
361
|
+
return null;
|
|
362
|
+
return dateTimeToSeconds(a) - dateTimeToSeconds(b);
|
|
363
|
+
}
|
|
198
364
|
if (isFeelDayTimeDuration(a) && isFeelDayTimeDuration(b))
|
|
199
365
|
return a.seconds - b.seconds;
|
|
200
366
|
if (isFeelYearsMonthsDuration(a) && isFeelYearsMonthsDuration(b))
|
|
201
367
|
return a.months - b.months;
|
|
202
368
|
return null;
|
|
203
369
|
}
|
|
370
|
+
function dateTimeToSeconds(dt) {
|
|
371
|
+
const epochDays = dateToEpochDays(dt.date);
|
|
372
|
+
return epochDays * 86400 + timeToSeconds(dt.time, epochDays);
|
|
373
|
+
}
|
|
204
374
|
const builtinMap = new Map();
|
|
205
375
|
function reg(name, fn) {
|
|
206
376
|
builtinMap.set(name, fn);
|
|
@@ -209,8 +379,30 @@ function reg(name, fn) {
|
|
|
209
379
|
// String functions
|
|
210
380
|
// -------------------------------------------------------------------------
|
|
211
381
|
reg("string", (v) => {
|
|
382
|
+
if (v === undefined)
|
|
383
|
+
return null;
|
|
384
|
+
return stringify(v, false);
|
|
385
|
+
});
|
|
386
|
+
/**
|
|
387
|
+
* Renders a value the way FEEL's string() does. Strings nested inside a list
|
|
388
|
+
* or context are quoted; a string rendered on its own is not.
|
|
389
|
+
*/
|
|
390
|
+
function stringify(v, nested) {
|
|
212
391
|
if (v === null)
|
|
213
|
-
return
|
|
392
|
+
return null;
|
|
393
|
+
if (typeof v === "string")
|
|
394
|
+
return nested ? JSON.stringify(v) : v;
|
|
395
|
+
if (isFeelList(v)) {
|
|
396
|
+
const parts = v.map((item) => stringify(item, true) ?? "null");
|
|
397
|
+
return `[${parts.join(", ")}]`;
|
|
398
|
+
}
|
|
399
|
+
if (isFeelContext(v)) {
|
|
400
|
+
const parts = Object.entries(v).map(([k, value]) => `${k}: ${stringify(value, true) ?? "null"}`);
|
|
401
|
+
return `{${parts.join(", ")}}`;
|
|
402
|
+
}
|
|
403
|
+
return scalarToString(v);
|
|
404
|
+
}
|
|
405
|
+
function scalarToString(v) {
|
|
214
406
|
if (typeof v === "string")
|
|
215
407
|
return v;
|
|
216
408
|
if (typeof v === "number")
|
|
@@ -252,10 +444,11 @@ reg("string", (v) => {
|
|
|
252
444
|
return r;
|
|
253
445
|
}
|
|
254
446
|
return null;
|
|
255
|
-
}
|
|
447
|
+
}
|
|
256
448
|
reg("string length", (s) => {
|
|
257
449
|
const str = toStr(s);
|
|
258
|
-
|
|
450
|
+
// FEEL counts characters, so an astral character counts once, not twice.
|
|
451
|
+
return str === null ? null : [...str].length;
|
|
259
452
|
});
|
|
260
453
|
reg("substring", (str, start, length) => {
|
|
261
454
|
const s = toStr(str);
|
|
@@ -264,15 +457,17 @@ reg("substring", (str, start, length) => {
|
|
|
264
457
|
const st = toNum(start);
|
|
265
458
|
if (st === null)
|
|
266
459
|
return null;
|
|
267
|
-
// FEEL substring is 1-based, negative counts
|
|
268
|
-
|
|
460
|
+
// FEEL substring is 1-based over characters, and a negative start counts
|
|
461
|
+
// back from the end.
|
|
462
|
+
const chars = [...s];
|
|
463
|
+
const idx = st > 0 ? st - 1 : Math.max(0, chars.length + st);
|
|
269
464
|
if (length !== undefined && length !== null) {
|
|
270
465
|
const len = toNum(length);
|
|
271
466
|
if (len === null)
|
|
272
467
|
return null;
|
|
273
|
-
return
|
|
468
|
+
return chars.slice(idx, idx + len).join("");
|
|
274
469
|
}
|
|
275
|
-
return
|
|
470
|
+
return chars.slice(idx).join("");
|
|
276
471
|
});
|
|
277
472
|
reg("substring before", (str, match) => {
|
|
278
473
|
const s = toStr(str);
|
|
@@ -319,18 +514,85 @@ reg("ends with", (str, match) => {
|
|
|
319
514
|
return null;
|
|
320
515
|
return s.endsWith(m);
|
|
321
516
|
});
|
|
517
|
+
// Patterns in FEEL come from static expression text, so the same few regexes
|
|
518
|
+
// are compiled over and over inside loops and decision tables; keep them.
|
|
519
|
+
const REGEX_CACHE_LIMIT = 256;
|
|
520
|
+
const regexCache = new Map();
|
|
521
|
+
/** Compiled regex for `pattern`/`flags`, or null when the pattern is invalid. */
|
|
522
|
+
/**
|
|
523
|
+
* Translates the XPath flags FEEL uses into a JavaScript regex. "i", "s" and
|
|
524
|
+
* "m" map straight across; "x" (ignore whitespace in the pattern) and "q"
|
|
525
|
+
* (treat the pattern as a literal) have no JavaScript equivalent and are
|
|
526
|
+
* applied to the pattern instead. Any other flag makes the call fail.
|
|
527
|
+
*/
|
|
528
|
+
function toJsRegExp(pattern, flags) {
|
|
529
|
+
let jsFlags = "";
|
|
530
|
+
for (const flag of flags) {
|
|
531
|
+
if (flag === "i" || flag === "s" || flag === "m" || flag === "g") {
|
|
532
|
+
if (!jsFlags.includes(flag))
|
|
533
|
+
jsFlags += flag;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (flag !== "x" && flag !== "q")
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
let source = pattern;
|
|
540
|
+
if (flags.includes("q")) {
|
|
541
|
+
source = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
542
|
+
}
|
|
543
|
+
else if (flags.includes("x")) {
|
|
544
|
+
source = stripPatternWhitespace(pattern);
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
return new RegExp(source, jsFlags);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/** Removes the whitespace an "x"-flagged pattern ignores, keeping character classes intact. */
|
|
554
|
+
function stripPatternWhitespace(pattern) {
|
|
555
|
+
let out = "";
|
|
556
|
+
let inClass = false;
|
|
557
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
558
|
+
const c = pattern[i];
|
|
559
|
+
if (c === "\\" && i + 1 < pattern.length) {
|
|
560
|
+
out += c + pattern[i + 1];
|
|
561
|
+
i++;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (c === "[")
|
|
565
|
+
inClass = true;
|
|
566
|
+
else if (c === "]")
|
|
567
|
+
inClass = false;
|
|
568
|
+
if (!inClass && /\s/.test(c))
|
|
569
|
+
continue;
|
|
570
|
+
out += c;
|
|
571
|
+
}
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
function cachedRegExp(pattern, flags) {
|
|
575
|
+
const key = `${flags}/${pattern}`;
|
|
576
|
+
const hit = regexCache.get(key);
|
|
577
|
+
if (hit !== undefined)
|
|
578
|
+
return hit;
|
|
579
|
+
const re = toJsRegExp(pattern, flags);
|
|
580
|
+
if (regexCache.size >= REGEX_CACHE_LIMIT)
|
|
581
|
+
regexCache.clear();
|
|
582
|
+
regexCache.set(key, re);
|
|
583
|
+
return re;
|
|
584
|
+
}
|
|
322
585
|
reg("matches", (str, pattern, flags) => {
|
|
323
586
|
const s = toStr(str);
|
|
324
587
|
const p = toStr(pattern);
|
|
325
588
|
if (s === null || p === null)
|
|
326
589
|
return null;
|
|
327
590
|
const f = flags !== undefined && flags !== null ? (toStr(flags) ?? "") : "";
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
591
|
+
const re = cachedRegExp(p, f);
|
|
592
|
+
if (re === null)
|
|
332
593
|
return null;
|
|
333
|
-
|
|
594
|
+
re.lastIndex = 0;
|
|
595
|
+
return re.test(s);
|
|
334
596
|
});
|
|
335
597
|
reg("replace", (str, pattern, replacement, flags) => {
|
|
336
598
|
const s = toStr(str);
|
|
@@ -339,121 +601,128 @@ reg("replace", (str, pattern, replacement, flags) => {
|
|
|
339
601
|
if (s === null || p === null || r === null)
|
|
340
602
|
return null;
|
|
341
603
|
const f = flags !== undefined && flags !== null ? (toStr(flags) ?? "g") : "g";
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
604
|
+
const re = cachedRegExp(p, f.includes("g") ? f : `${f}g`);
|
|
605
|
+
if (re === null)
|
|
346
606
|
return null;
|
|
347
|
-
|
|
607
|
+
re.lastIndex = 0;
|
|
608
|
+
// $0 is XPath's whole match, which JavaScript spells $&.
|
|
609
|
+
return s.replace(re, r.replace(/\$&/g, "$$$$&").replace(/\$0/g, "$$&"));
|
|
348
610
|
});
|
|
349
611
|
reg("split", (str, delimiter) => {
|
|
350
612
|
const s = toStr(str);
|
|
351
613
|
const d = toStr(delimiter);
|
|
352
614
|
if (s === null || d === null)
|
|
353
615
|
return null;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
return s.split(d);
|
|
359
|
-
}
|
|
616
|
+
const re = cachedRegExp(d, "");
|
|
617
|
+
return re === null ? s.split(d) : s.split(re);
|
|
360
618
|
});
|
|
361
|
-
reg("string join", (
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
list = first;
|
|
369
|
-
delimiter = flat.length >= 2 ? (toStr(at(flat, 1)) ?? "") : "";
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
list = flat;
|
|
373
|
-
}
|
|
619
|
+
reg("string join", (value, delimiter, prefix, suffix) => {
|
|
620
|
+
// A value that is not a list joins as a list of one, so the delimiter
|
|
621
|
+
// never appears. A null, or anything in the list that is not a string,
|
|
622
|
+
// has no joined form.
|
|
623
|
+
if (value === null || value === undefined)
|
|
624
|
+
return null;
|
|
625
|
+
const list = isFeelList(value) ? value : [value];
|
|
374
626
|
const parts = [];
|
|
375
627
|
for (const v of list) {
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
628
|
+
// Nulls are skipped; any other non-string is an error.
|
|
629
|
+
if (v === null)
|
|
630
|
+
continue;
|
|
631
|
+
const text = toStr(v);
|
|
632
|
+
if (text === null)
|
|
633
|
+
return null;
|
|
634
|
+
parts.push(text);
|
|
379
635
|
}
|
|
380
|
-
|
|
636
|
+
const between = delimiter === undefined || delimiter === null ? "" : toStr(delimiter);
|
|
637
|
+
if (between === null)
|
|
638
|
+
return null;
|
|
639
|
+
const head = prefix === undefined || prefix === null ? "" : toStr(prefix);
|
|
640
|
+
const tail = suffix === undefined || suffix === null ? "" : toStr(suffix);
|
|
641
|
+
if (head === null || tail === null)
|
|
642
|
+
return null;
|
|
643
|
+
return head + parts.join(between) + tail;
|
|
381
644
|
});
|
|
382
645
|
// -------------------------------------------------------------------------
|
|
383
646
|
// Number functions
|
|
384
647
|
// -------------------------------------------------------------------------
|
|
385
|
-
reg("number", (v) => {
|
|
648
|
+
reg("number", (v, groupingSeparator, decimalSeparator) => {
|
|
649
|
+
const hasSeparators = groupingSeparator !== undefined || decimalSeparator !== undefined;
|
|
386
650
|
if (typeof v === "number")
|
|
387
|
-
return v;
|
|
388
|
-
if (typeof v
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
651
|
+
return hasSeparators ? null : v;
|
|
652
|
+
if (typeof v !== "string")
|
|
653
|
+
return null;
|
|
654
|
+
const grouping = separatorArg(groupingSeparator, [" ", ",", "."]);
|
|
655
|
+
const decimal = separatorArg(decimalSeparator, [",", "."]);
|
|
656
|
+
if (grouping === undefined || decimal === undefined)
|
|
657
|
+
return null;
|
|
658
|
+
if (grouping !== null && grouping === decimal)
|
|
659
|
+
return null;
|
|
660
|
+
let text = v;
|
|
661
|
+
if (grouping !== null)
|
|
662
|
+
text = text.split(grouping).join("");
|
|
663
|
+
if (decimal !== null)
|
|
664
|
+
text = text.split(decimal).join(".");
|
|
665
|
+
if (text.trim() === "")
|
|
666
|
+
return null;
|
|
667
|
+
const n = Number(text);
|
|
668
|
+
return Number.isNaN(n) ? null : n;
|
|
393
669
|
});
|
|
670
|
+
/** Reads a number() separator argument: null when absent, undefined when invalid. */
|
|
671
|
+
function separatorArg(v, allowed) {
|
|
672
|
+
if (v === undefined || v === null)
|
|
673
|
+
return null;
|
|
674
|
+
if (typeof v !== "string" || !allowed.includes(v))
|
|
675
|
+
return undefined;
|
|
676
|
+
return v;
|
|
677
|
+
}
|
|
394
678
|
reg("decimal", (n, scale) => {
|
|
395
679
|
const num = toNum(n);
|
|
396
|
-
const sc =
|
|
397
|
-
if (num === null || sc === null)
|
|
680
|
+
const sc = toScale(scale);
|
|
681
|
+
if (num === null || sc === null || scale === undefined)
|
|
398
682
|
return null;
|
|
399
|
-
|
|
400
|
-
return Math.round(num * factor) / factor;
|
|
683
|
+
return roundAt(num, sc, roundHalfEven);
|
|
401
684
|
});
|
|
402
685
|
reg("floor", (n, scale) => {
|
|
403
686
|
const num = toNum(n);
|
|
404
|
-
|
|
687
|
+
const sc = toScale(scale);
|
|
688
|
+
if (num === null || sc === null)
|
|
405
689
|
return null;
|
|
406
|
-
|
|
407
|
-
const sc = toNum(scale) ?? 0;
|
|
408
|
-
const factor = 10 ** sc;
|
|
409
|
-
return Math.floor(num * factor) / factor;
|
|
410
|
-
}
|
|
411
|
-
return Math.floor(num);
|
|
690
|
+
return roundAt(num, sc, Math.floor);
|
|
412
691
|
});
|
|
413
692
|
reg("ceiling", (n, scale) => {
|
|
414
693
|
const num = toNum(n);
|
|
415
|
-
|
|
694
|
+
const sc = toScale(scale);
|
|
695
|
+
if (num === null || sc === null)
|
|
416
696
|
return null;
|
|
417
|
-
|
|
418
|
-
const sc = toNum(scale) ?? 0;
|
|
419
|
-
const factor = 10 ** sc;
|
|
420
|
-
return Math.ceil(num * factor) / factor;
|
|
421
|
-
}
|
|
422
|
-
return Math.ceil(num);
|
|
697
|
+
return roundAt(num, sc, Math.ceil);
|
|
423
698
|
});
|
|
424
699
|
reg("round half up", (n, scale) => {
|
|
425
700
|
const num = toNum(n);
|
|
426
|
-
const sc =
|
|
427
|
-
if (num === null)
|
|
701
|
+
const sc = toScale(scale);
|
|
702
|
+
if (num === null || sc === null)
|
|
428
703
|
return null;
|
|
429
|
-
|
|
430
|
-
return Math.round(num * factor) / factor;
|
|
704
|
+
return roundAt(num, sc, roundHalfUp);
|
|
431
705
|
});
|
|
432
706
|
reg("round half down", (n, scale) => {
|
|
433
707
|
const num = toNum(n);
|
|
434
|
-
const sc =
|
|
435
|
-
if (num === null)
|
|
708
|
+
const sc = toScale(scale);
|
|
709
|
+
if (num === null || sc === null)
|
|
436
710
|
return null;
|
|
437
|
-
|
|
438
|
-
const scaled = num * factor;
|
|
439
|
-
return (scaled > 0 ? Math.ceil(scaled - 0.5) : Math.floor(scaled + 0.5)) / factor;
|
|
711
|
+
return roundAt(num, sc, roundHalfDown);
|
|
440
712
|
});
|
|
441
713
|
reg("round up", (n, scale) => {
|
|
442
714
|
const num = toNum(n);
|
|
443
|
-
const sc =
|
|
444
|
-
if (num === null)
|
|
715
|
+
const sc = toScale(scale);
|
|
716
|
+
if (num === null || sc === null)
|
|
445
717
|
return null;
|
|
446
|
-
|
|
447
|
-
const scaled = num * factor;
|
|
448
|
-
return (scaled > 0 ? Math.ceil(scaled) : Math.floor(scaled)) / factor;
|
|
718
|
+
return roundAt(num, sc, (x) => (x >= 0 ? Math.ceil(x) : Math.floor(x)));
|
|
449
719
|
});
|
|
450
720
|
reg("round down", (n, scale) => {
|
|
451
721
|
const num = toNum(n);
|
|
452
|
-
const sc =
|
|
453
|
-
if (num === null)
|
|
722
|
+
const sc = toScale(scale);
|
|
723
|
+
if (num === null || sc === null)
|
|
454
724
|
return null;
|
|
455
|
-
|
|
456
|
-
return Math.trunc(num * factor) / factor;
|
|
725
|
+
return roundAt(num, sc, Math.trunc);
|
|
457
726
|
});
|
|
458
727
|
reg("abs", (n) => {
|
|
459
728
|
if (typeof n === "number")
|
|
@@ -496,6 +765,9 @@ reg("random number", () => Math.random());
|
|
|
496
765
|
// List functions
|
|
497
766
|
// -------------------------------------------------------------------------
|
|
498
767
|
reg("count", (...args) => {
|
|
768
|
+
// count() takes a list; a null argument is a type error, not a one-item list.
|
|
769
|
+
if (args.length === 1 && args[0] === null)
|
|
770
|
+
return null;
|
|
499
771
|
const list = flattenToList(args);
|
|
500
772
|
const first = list[0];
|
|
501
773
|
if (list.length === 1 && first !== undefined && isFeelList(first))
|
|
@@ -549,7 +821,11 @@ reg("sum", (...args) => {
|
|
|
549
821
|
return s;
|
|
550
822
|
});
|
|
551
823
|
reg("product", (...args) => {
|
|
824
|
+
if (args.length === 0)
|
|
825
|
+
return null;
|
|
552
826
|
const list = unwrapList(flattenToList(args));
|
|
827
|
+
if (list.length === 0)
|
|
828
|
+
return null;
|
|
553
829
|
let p = 1;
|
|
554
830
|
for (const v of list) {
|
|
555
831
|
const n = toNum(v);
|
|
@@ -603,24 +879,28 @@ reg("stddev", (...args) => {
|
|
|
603
879
|
return Math.sqrt(variance);
|
|
604
880
|
});
|
|
605
881
|
reg("mode", (...args) => {
|
|
882
|
+
if (args.length === 0)
|
|
883
|
+
return null;
|
|
606
884
|
const list = unwrapList(flattenToList(args));
|
|
607
885
|
const counts = new Map();
|
|
608
886
|
for (const v of list) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (cnt > maxCount)
|
|
614
|
-
maxCount = cnt;
|
|
615
|
-
}
|
|
616
|
-
const modes = [];
|
|
617
|
-
for (const [v, cnt] of counts) {
|
|
618
|
-
if (cnt === maxCount)
|
|
619
|
-
modes.push(v);
|
|
887
|
+
const n = toNum(v);
|
|
888
|
+
if (n === null)
|
|
889
|
+
return null;
|
|
890
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
620
891
|
}
|
|
621
|
-
|
|
892
|
+
if (counts.size === 0)
|
|
893
|
+
return [];
|
|
894
|
+
const maxCount = Math.max(...counts.values());
|
|
895
|
+
// The most frequent values, in ascending order.
|
|
896
|
+
return [...counts]
|
|
897
|
+
.filter(([, count]) => count === maxCount)
|
|
898
|
+
.map(([value]) => value)
|
|
899
|
+
.sort((a, b) => a - b);
|
|
622
900
|
});
|
|
623
901
|
reg("all", (...args) => {
|
|
902
|
+
if (args.length === 0)
|
|
903
|
+
return null;
|
|
624
904
|
const list = unwrapList(flattenToList(args));
|
|
625
905
|
let hasNull = false;
|
|
626
906
|
for (const v of list) {
|
|
@@ -628,10 +908,14 @@ reg("all", (...args) => {
|
|
|
628
908
|
return false;
|
|
629
909
|
if (v === null)
|
|
630
910
|
hasNull = true;
|
|
911
|
+
else if (typeof v !== "boolean")
|
|
912
|
+
return null;
|
|
631
913
|
}
|
|
632
914
|
return hasNull ? null : true;
|
|
633
915
|
});
|
|
634
916
|
reg("any", (...args) => {
|
|
917
|
+
if (args.length === 0)
|
|
918
|
+
return null;
|
|
635
919
|
const list = unwrapList(flattenToList(args));
|
|
636
920
|
let hasNull = false;
|
|
637
921
|
for (const v of list) {
|
|
@@ -639,6 +923,8 @@ reg("any", (...args) => {
|
|
|
639
923
|
return true;
|
|
640
924
|
if (v === null)
|
|
641
925
|
hasNull = true;
|
|
926
|
+
else if (typeof v !== "boolean")
|
|
927
|
+
return null;
|
|
642
928
|
}
|
|
643
929
|
return hasNull ? null : false;
|
|
644
930
|
});
|
|
@@ -713,45 +999,40 @@ reg("index of", (list, match) => {
|
|
|
713
999
|
}
|
|
714
1000
|
return result;
|
|
715
1001
|
});
|
|
1002
|
+
// Set membership is SameValueZero, exactly what Array#includes used here, so
|
|
1003
|
+
// de-duplication keeps its semantics while dropping from O(n²) to O(n).
|
|
716
1004
|
reg("union", (...args) => {
|
|
717
|
-
const
|
|
1005
|
+
const seen = new Set();
|
|
718
1006
|
for (const v of args) {
|
|
719
1007
|
if (isFeelList(v)) {
|
|
720
|
-
for (const item of v)
|
|
721
|
-
|
|
722
|
-
result.push(item);
|
|
723
|
-
}
|
|
1008
|
+
for (const item of v)
|
|
1009
|
+
seen.add(item);
|
|
724
1010
|
}
|
|
725
|
-
else
|
|
726
|
-
|
|
1011
|
+
else {
|
|
1012
|
+
seen.add(v);
|
|
727
1013
|
}
|
|
728
1014
|
}
|
|
729
|
-
return
|
|
1015
|
+
return [...seen];
|
|
730
1016
|
});
|
|
731
1017
|
reg("distinct values", (list) => {
|
|
732
1018
|
if (!isFeelList(list))
|
|
733
1019
|
return null;
|
|
734
|
-
|
|
735
|
-
for (const v of list) {
|
|
736
|
-
if (!result.includes(v))
|
|
737
|
-
result.push(v);
|
|
738
|
-
}
|
|
739
|
-
return result;
|
|
1020
|
+
return [...new Set(list)];
|
|
740
1021
|
});
|
|
741
1022
|
reg("flatten", (list) => {
|
|
742
1023
|
if (!isFeelList(list))
|
|
743
1024
|
return null;
|
|
1025
|
+
const result = [];
|
|
744
1026
|
const flat = (arr) => {
|
|
745
|
-
const result = [];
|
|
746
1027
|
for (const v of arr) {
|
|
747
1028
|
if (isFeelList(v))
|
|
748
|
-
|
|
1029
|
+
flat(v);
|
|
749
1030
|
else
|
|
750
1031
|
result.push(v);
|
|
751
1032
|
}
|
|
752
|
-
return result;
|
|
753
1033
|
};
|
|
754
|
-
|
|
1034
|
+
flat(list);
|
|
1035
|
+
return result;
|
|
755
1036
|
});
|
|
756
1037
|
reg("sort", (list, fn) => {
|
|
757
1038
|
if (!isFeelList(list))
|
|
@@ -787,11 +1068,19 @@ reg("get or else", (v, defaultVal) => {
|
|
|
787
1068
|
reg("get value", (ctx, key) => {
|
|
788
1069
|
if (!isFeelContext(ctx))
|
|
789
1070
|
return null;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
|
|
1071
|
+
// A list of keys walks into nested contexts: get value(c, ["y", "a"]).
|
|
1072
|
+
const path = isFeelList(key) ? key : [key];
|
|
1073
|
+
let current = ctx;
|
|
1074
|
+
for (const step of path) {
|
|
1075
|
+
const name = toStr(step);
|
|
1076
|
+
if (name === null || !isFeelContext(current))
|
|
1077
|
+
return null;
|
|
1078
|
+
const next = current[name];
|
|
1079
|
+
if (next === undefined)
|
|
1080
|
+
return null;
|
|
1081
|
+
current = next;
|
|
1082
|
+
}
|
|
1083
|
+
return current;
|
|
795
1084
|
});
|
|
796
1085
|
reg("get entries", (ctx) => {
|
|
797
1086
|
if (!isFeelContext(ctx))
|
|
@@ -801,18 +1090,42 @@ reg("get entries", (ctx) => {
|
|
|
801
1090
|
reg("context put", (ctx, key, value) => {
|
|
802
1091
|
if (!isFeelContext(ctx))
|
|
803
1092
|
return null;
|
|
804
|
-
|
|
805
|
-
if (k === null)
|
|
1093
|
+
if (value === undefined)
|
|
806
1094
|
return null;
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
return result;
|
|
1095
|
+
const path = isFeelList(key) ? key : [key];
|
|
1096
|
+
if (path.length === 0)
|
|
1097
|
+
return null;
|
|
1098
|
+
return putPath(ctx, path, value);
|
|
812
1099
|
});
|
|
1100
|
+
/** Copies a context with `path` set to `value`, creating contexts on the way. */
|
|
1101
|
+
function putPath(ctx, path, value) {
|
|
1102
|
+
const name = toStr(path[0] ?? null);
|
|
1103
|
+
if (name === null)
|
|
1104
|
+
return null;
|
|
1105
|
+
const result = { ...ctx };
|
|
1106
|
+
if (path.length === 1) {
|
|
1107
|
+
result[name] = value;
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
const nested = result[name];
|
|
1111
|
+
// A step onto something that is not a context has nowhere to go.
|
|
1112
|
+
if (nested !== undefined && !isFeelContext(nested))
|
|
1113
|
+
return null;
|
|
1114
|
+
const inner = putPath(nested ?? {}, path.slice(1), value);
|
|
1115
|
+
if (inner === null)
|
|
1116
|
+
return null;
|
|
1117
|
+
result[name] = inner;
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
813
1120
|
reg("context merge", (...args) => {
|
|
1121
|
+
// The signature is one list of contexts; a bare argument list is accepted
|
|
1122
|
+
// too, the way the other list built-ins are.
|
|
1123
|
+
const first = args[0] ?? null;
|
|
1124
|
+
const contexts = args.length === 1 && isFeelList(first) ? first : args;
|
|
1125
|
+
if (contexts.length === 0)
|
|
1126
|
+
return null;
|
|
814
1127
|
const result = {};
|
|
815
|
-
for (const v of
|
|
1128
|
+
for (const v of contexts) {
|
|
816
1129
|
if (!isFeelContext(v))
|
|
817
1130
|
return null;
|
|
818
1131
|
for (const [k, cv] of Object.entries(v))
|
|
@@ -820,17 +1133,19 @@ reg("context merge", (...args) => {
|
|
|
820
1133
|
}
|
|
821
1134
|
return result;
|
|
822
1135
|
});
|
|
823
|
-
reg("context", (
|
|
824
|
-
|
|
825
|
-
return null;
|
|
1136
|
+
reg("context", (entries) => {
|
|
1137
|
+
const list = isFeelList(entries) ? entries : [entries];
|
|
826
1138
|
const result = {};
|
|
827
1139
|
for (const item of list) {
|
|
828
1140
|
if (!isFeelContext(item))
|
|
829
1141
|
return null;
|
|
830
1142
|
const k = item.key;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1143
|
+
if (typeof k !== "string")
|
|
1144
|
+
return null;
|
|
1145
|
+
// An entry naming a key already set is a conflict, not an overwrite.
|
|
1146
|
+
if (k in result)
|
|
1147
|
+
return null;
|
|
1148
|
+
result[k] = item.value !== undefined ? item.value : null;
|
|
834
1149
|
}
|
|
835
1150
|
return result;
|
|
836
1151
|
});
|
|
@@ -844,6 +1159,8 @@ reg("date", (...args) => {
|
|
|
844
1159
|
return parseDate(v);
|
|
845
1160
|
if (isFeelDateTime(v))
|
|
846
1161
|
return v.date;
|
|
1162
|
+
if (isFeelDate(v))
|
|
1163
|
+
return v;
|
|
847
1164
|
return null;
|
|
848
1165
|
}
|
|
849
1166
|
if (args.length === 3) {
|
|
@@ -852,7 +1169,7 @@ reg("date", (...args) => {
|
|
|
852
1169
|
const d = toNum(at(args, 2));
|
|
853
1170
|
if (y === null || m === null || d === null)
|
|
854
1171
|
return null;
|
|
855
|
-
return
|
|
1172
|
+
return makeDate(y, m, d);
|
|
856
1173
|
}
|
|
857
1174
|
return null;
|
|
858
1175
|
});
|
|
@@ -863,6 +1180,10 @@ reg("time", (...args) => {
|
|
|
863
1180
|
return parseTime(v);
|
|
864
1181
|
if (isFeelDateTime(v))
|
|
865
1182
|
return v.time;
|
|
1183
|
+
if (isFeelTime(v))
|
|
1184
|
+
return v;
|
|
1185
|
+
if (isFeelDate(v))
|
|
1186
|
+
return { type: "time", hour: 0, minute: 0, second: 0 };
|
|
866
1187
|
return null;
|
|
867
1188
|
}
|
|
868
1189
|
if (args.length >= 3) {
|
|
@@ -871,6 +1192,8 @@ reg("time", (...args) => {
|
|
|
871
1192
|
const s = toNum(at(args, 2));
|
|
872
1193
|
if (h === null || m === null || s === null)
|
|
873
1194
|
return null;
|
|
1195
|
+
if (!isValidTime(h, m, s))
|
|
1196
|
+
return null;
|
|
874
1197
|
const t = { type: "time", hour: h, minute: m, second: s };
|
|
875
1198
|
const off = at(args, 3);
|
|
876
1199
|
if (off !== null && isFeelDayTimeDuration(off))
|
|
@@ -915,9 +1238,29 @@ reg("years and months duration", (from, to) => {
|
|
|
915
1238
|
d2 = to.date;
|
|
916
1239
|
if (!d1 || !d2)
|
|
917
1240
|
return null;
|
|
918
|
-
|
|
1241
|
+
let months = (d2.year - d1.year) * 12 + (d2.month - d1.month);
|
|
1242
|
+
// Only whole months count, so a end that has not yet reached the start's
|
|
1243
|
+
// day-and-time within the month gives back the month it was counted.
|
|
1244
|
+
const remainder = compareWithinMonth(from, to);
|
|
1245
|
+
if (months > 0 && remainder < 0)
|
|
1246
|
+
months -= 1;
|
|
1247
|
+
else if (months < 0 && remainder > 0)
|
|
1248
|
+
months += 1;
|
|
919
1249
|
return { type: "years-months-duration", months };
|
|
920
1250
|
});
|
|
1251
|
+
/** Orders two temporals by day of month and time of day, ignoring year and month. */
|
|
1252
|
+
function compareWithinMonth(a, b) {
|
|
1253
|
+
const partsOf = (v) => {
|
|
1254
|
+
if (isFeelDate(v))
|
|
1255
|
+
return [v.day, 0];
|
|
1256
|
+
if (isFeelDateTime(v))
|
|
1257
|
+
return [v.date.day, timeToSeconds(v.time)];
|
|
1258
|
+
return [0, 0];
|
|
1259
|
+
};
|
|
1260
|
+
const [dayA, secA] = partsOf(a);
|
|
1261
|
+
const [dayB, secB] = partsOf(b);
|
|
1262
|
+
return dayB - dayA || secB - secA;
|
|
1263
|
+
}
|
|
921
1264
|
// -------------------------------------------------------------------------
|
|
922
1265
|
// Temporal utility functions
|
|
923
1266
|
// -------------------------------------------------------------------------
|
|
@@ -974,17 +1317,26 @@ reg("week of year", (d) => {
|
|
|
974
1317
|
date = d.date;
|
|
975
1318
|
if (!date)
|
|
976
1319
|
return null;
|
|
977
|
-
// ISO week
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
const jan4dow = ((jan4 % 7) + 7 + 4) % 7; // Monday=0
|
|
982
|
-
const weekStart = jan4 - ((jan4dow + 6) % 7);
|
|
983
|
-
const week = Math.floor((epochDays - weekStart) / 7) + 1;
|
|
1320
|
+
// ISO 8601: week 1 is the one holding the first Thursday, so the turn of
|
|
1321
|
+
// the year can fall in the neighbouring year's last or first week.
|
|
1322
|
+
const weekday = isoWeekday(date);
|
|
1323
|
+
const week = Math.floor((dayOfYear(date) - weekday + 10) / 7);
|
|
984
1324
|
if (week < 1)
|
|
985
|
-
return
|
|
1325
|
+
return isoWeeksInYear(date.year - 1);
|
|
1326
|
+
if (week > isoWeeksInYear(date.year))
|
|
1327
|
+
return 1;
|
|
986
1328
|
return week;
|
|
987
1329
|
});
|
|
1330
|
+
/** Monday is 1, Sunday is 7. Epoch day 0, 1970-01-01, was a Thursday. */
|
|
1331
|
+
function isoWeekday(d) {
|
|
1332
|
+
return ((((dateToEpochDays(d) + 3) % 7) + 7) % 7) + 1;
|
|
1333
|
+
}
|
|
1334
|
+
/** 52 or 53, whichever ISO 8601 gives the year. */
|
|
1335
|
+
function isoWeeksInYear(year) {
|
|
1336
|
+
const jan1 = isoWeekday({ type: "date", year, month: 1, day: 1 });
|
|
1337
|
+
const long = jan1 === 4 || (isLeapYear(year) && jan1 === 3);
|
|
1338
|
+
return long ? 53 : 52;
|
|
1339
|
+
}
|
|
988
1340
|
reg("month of year", (d) => {
|
|
989
1341
|
const MONTH_NAMES = [
|
|
990
1342
|
"January",
|
|
@@ -1131,41 +1483,101 @@ reg("includes", (a, b) => {
|
|
|
1131
1483
|
return cmpPts(as_, bs, "start") <= 0 && cmpPts(be, ae, "end") <= 0;
|
|
1132
1484
|
});
|
|
1133
1485
|
reg("starts", (a, b) => {
|
|
1134
|
-
|
|
1486
|
+
// starts(point, range): the range begins at that point, inclusively.
|
|
1487
|
+
if (!isFeelRange(b))
|
|
1135
1488
|
return null;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
return cmpPts(as_, bs, "start") === 0 && cmpPts(ae, be, "end") <= 0;
|
|
1489
|
+
if (!isFeelRange(a)) {
|
|
1490
|
+
return b.startIncluded && compareValues(a, b.start) === 0;
|
|
1491
|
+
}
|
|
1492
|
+
return cmpPts(startOf(a), startOf(b), "start") === 0 && cmpPts(endOf(a), endOf(b), "end") <= 0;
|
|
1141
1493
|
});
|
|
1142
1494
|
reg("started by", (a, b) => {
|
|
1143
|
-
if (!isFeelRange(a)
|
|
1495
|
+
if (!isFeelRange(a))
|
|
1144
1496
|
return null;
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
return cmpPts(as_, bs, "start") === 0 && cmpPts(be, ae, "end") <= 0;
|
|
1497
|
+
if (!isFeelRange(b)) {
|
|
1498
|
+
return a.startIncluded && compareValues(a.start, b) === 0;
|
|
1499
|
+
}
|
|
1500
|
+
return cmpPts(startOf(a), startOf(b), "start") === 0 && cmpPts(endOf(b), endOf(a), "end") <= 0;
|
|
1150
1501
|
});
|
|
1151
1502
|
reg("finishes", (a, b) => {
|
|
1152
|
-
|
|
1503
|
+
// finishes(point, range): the range ends at that point, inclusively.
|
|
1504
|
+
if (!isFeelRange(b))
|
|
1153
1505
|
return null;
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
return cmpPts(ae, be, "end") === 0 && cmpPts(bs, as_, "start") <= 0;
|
|
1506
|
+
if (!isFeelRange(a)) {
|
|
1507
|
+
return b.endIncluded && compareValues(a, b.end) === 0;
|
|
1508
|
+
}
|
|
1509
|
+
return cmpPts(endOf(a), endOf(b), "end") === 0 && cmpPts(startOf(b), startOf(a), "start") <= 0;
|
|
1159
1510
|
});
|
|
1160
1511
|
reg("finished by", (a, b) => {
|
|
1161
|
-
if (!isFeelRange(a)
|
|
1512
|
+
if (!isFeelRange(a))
|
|
1162
1513
|
return null;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1514
|
+
if (!isFeelRange(b)) {
|
|
1515
|
+
return a.endIncluded && compareValues(a.end, b) === 0;
|
|
1516
|
+
}
|
|
1517
|
+
return cmpPts(endOf(a), endOf(b), "end") === 0 && cmpPts(startOf(a), startOf(b), "start") <= 0;
|
|
1518
|
+
});
|
|
1519
|
+
/**
|
|
1520
|
+
* DMN's is(): whether two values are the same value, not merely equal ones.
|
|
1521
|
+
* Temporal values differ when they are written differently even where they
|
|
1522
|
+
* name the same instant, so a local time is not the same value as one at
|
|
1523
|
+
* UTC, and a zone is not the same value as the offset it currently has.
|
|
1524
|
+
*/
|
|
1525
|
+
reg("is", (a, b, ...rest) => {
|
|
1526
|
+
if (rest.length > 0)
|
|
1527
|
+
return null;
|
|
1528
|
+
if (b === undefined)
|
|
1529
|
+
return false;
|
|
1530
|
+
if (a === null || b === null)
|
|
1531
|
+
return a === null && b === null;
|
|
1532
|
+
if (isFeelTime(a) || isFeelDateTime(a) || isFeelTime(b) || isFeelDateTime(b)) {
|
|
1533
|
+
return sameTemporal(a, b);
|
|
1534
|
+
}
|
|
1535
|
+
if (valueType(a) !== valueType(b))
|
|
1536
|
+
return false;
|
|
1537
|
+
return compareValues(a, b) === 0 || deepEquals(a, b);
|
|
1538
|
+
});
|
|
1539
|
+
/** Compares the written form of a time or date-time, field by field. */
|
|
1540
|
+
function sameTemporal(a, b) {
|
|
1541
|
+
if (isFeelTime(a) && isFeelTime(b)) {
|
|
1542
|
+
return (a.hour === b.hour &&
|
|
1543
|
+
a.minute === b.minute &&
|
|
1544
|
+
a.second === b.second &&
|
|
1545
|
+
a.offsetSeconds === b.offsetSeconds &&
|
|
1546
|
+
a.timezone === b.timezone);
|
|
1547
|
+
}
|
|
1548
|
+
if (isFeelDateTime(a) && isFeelDateTime(b)) {
|
|
1549
|
+
return (a.date.year === b.date.year &&
|
|
1550
|
+
a.date.month === b.date.month &&
|
|
1551
|
+
a.date.day === b.date.day &&
|
|
1552
|
+
sameTemporal(a.time, b.time));
|
|
1553
|
+
}
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
/** The FEEL type of a value, for deciding whether two values are the same kind. */
|
|
1557
|
+
function valueType(v) {
|
|
1558
|
+
if (v === null)
|
|
1559
|
+
return "null";
|
|
1560
|
+
if (Array.isArray(v))
|
|
1561
|
+
return "list";
|
|
1562
|
+
if (typeof v !== "object")
|
|
1563
|
+
return typeof v;
|
|
1564
|
+
const tagged = v.type;
|
|
1565
|
+
return typeof tagged === "string" ? tagged : "context";
|
|
1566
|
+
}
|
|
1567
|
+
function deepEquals(a, b) {
|
|
1568
|
+
if (a === b)
|
|
1569
|
+
return true;
|
|
1570
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1571
|
+
return a.length === b.length && a.every((x, i) => deepEquals(x, b[i] ?? null));
|
|
1572
|
+
}
|
|
1573
|
+
if (isFeelContext(a) && isFeelContext(b)) {
|
|
1574
|
+
const keys = Object.keys(a);
|
|
1575
|
+
if (keys.length !== Object.keys(b).length)
|
|
1576
|
+
return false;
|
|
1577
|
+
return keys.every((k) => deepEquals(a[k] ?? null, b[k] ?? null));
|
|
1578
|
+
}
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1169
1581
|
reg("coincides", (a, b) => {
|
|
1170
1582
|
if (isFeelRange(a) && isFeelRange(b)) {
|
|
1171
1583
|
return cmpPts(startOf(a), startOf(b), "start") === 0 && cmpPts(endOf(a), endOf(b), "end") === 0;
|
|
@@ -1176,14 +1588,236 @@ reg("coincides", (a, b) => {
|
|
|
1176
1588
|
return null;
|
|
1177
1589
|
});
|
|
1178
1590
|
// -------------------------------------------------------------------------
|
|
1591
|
+
// Parameter names
|
|
1592
|
+
// -------------------------------------------------------------------------
|
|
1593
|
+
// Parameter names of every built-in, in declaration order, so that a named
|
|
1594
|
+
// invocation such as `substring(start position: 2, string: "hello")` binds by
|
|
1595
|
+
// name rather than by the order the arguments happen to appear in. Built-ins
|
|
1596
|
+
// with several signatures list one entry per signature.
|
|
1597
|
+
const PARAM_SIGNATURES = {
|
|
1598
|
+
// Conversion
|
|
1599
|
+
string: [["from"]],
|
|
1600
|
+
number: [
|
|
1601
|
+
["from"],
|
|
1602
|
+
["from", "grouping separator"],
|
|
1603
|
+
["from", "grouping separator", "decimal separator"],
|
|
1604
|
+
],
|
|
1605
|
+
context: [["entries"]],
|
|
1606
|
+
date: [["from"], ["year", "month", "day"]],
|
|
1607
|
+
time: [["from"], ["hour", "minute", "second"], ["hour", "minute", "second", "offset"]],
|
|
1608
|
+
"date and time": [["from"], ["date", "time"], ["date", "timezone"]],
|
|
1609
|
+
duration: [["from"]],
|
|
1610
|
+
"years and months duration": [["from", "to"]],
|
|
1611
|
+
// Boolean
|
|
1612
|
+
not: [["negand"]],
|
|
1613
|
+
is: [["value1"], ["value2"], ["value1", "value2"]],
|
|
1614
|
+
"is defined": [["value"]],
|
|
1615
|
+
"get or else": [["value", "default"]],
|
|
1616
|
+
// String
|
|
1617
|
+
substring: [
|
|
1618
|
+
["string", "start position"],
|
|
1619
|
+
["string", "start position", "length"],
|
|
1620
|
+
],
|
|
1621
|
+
"string length": [["string"]],
|
|
1622
|
+
"upper case": [["string"]],
|
|
1623
|
+
"lower case": [["string"]],
|
|
1624
|
+
"substring before": [["string", "match"]],
|
|
1625
|
+
"substring after": [["string", "match"]],
|
|
1626
|
+
contains: [["string", "match"]],
|
|
1627
|
+
"starts with": [["string", "match"]],
|
|
1628
|
+
"ends with": [["string", "match"]],
|
|
1629
|
+
matches: [
|
|
1630
|
+
["input", "pattern"],
|
|
1631
|
+
["input", "pattern", "flags"],
|
|
1632
|
+
],
|
|
1633
|
+
replace: [
|
|
1634
|
+
["input", "pattern", "replacement"],
|
|
1635
|
+
["input", "pattern", "replacement", "flags"],
|
|
1636
|
+
],
|
|
1637
|
+
split: [["string", "delimiter"]],
|
|
1638
|
+
"string join": [["list"], ["list", "delimiter"], ["list", "delimiter", "prefix", "suffix"]],
|
|
1639
|
+
// List
|
|
1640
|
+
"list contains": [["list", "element"]],
|
|
1641
|
+
count: [["list"]],
|
|
1642
|
+
min: [["list"]],
|
|
1643
|
+
max: [["list"]],
|
|
1644
|
+
sum: [["list"]],
|
|
1645
|
+
product: [["list"]],
|
|
1646
|
+
mean: [["list"]],
|
|
1647
|
+
median: [["list"]],
|
|
1648
|
+
stddev: [["list"]],
|
|
1649
|
+
mode: [["list"]],
|
|
1650
|
+
all: [["list"]],
|
|
1651
|
+
any: [["list"]],
|
|
1652
|
+
sublist: [
|
|
1653
|
+
["list", "start position"],
|
|
1654
|
+
["list", "start position", "length"],
|
|
1655
|
+
],
|
|
1656
|
+
append: [["list", "items"]],
|
|
1657
|
+
concatenate: [["lists"]],
|
|
1658
|
+
"insert before": [["list", "position", "newItem"]],
|
|
1659
|
+
remove: [["list", "position"]],
|
|
1660
|
+
reverse: [["list"]],
|
|
1661
|
+
"index of": [["list", "match"]],
|
|
1662
|
+
union: [["list"]],
|
|
1663
|
+
"distinct values": [["list"]],
|
|
1664
|
+
flatten: [["list"]],
|
|
1665
|
+
sort: [["list", "precedes"]],
|
|
1666
|
+
// Numeric
|
|
1667
|
+
decimal: [["n", "scale"]],
|
|
1668
|
+
floor: [["n"], ["n", "scale"]],
|
|
1669
|
+
ceiling: [["n"], ["n", "scale"]],
|
|
1670
|
+
"round up": [["n", "scale"]],
|
|
1671
|
+
"round down": [["n", "scale"]],
|
|
1672
|
+
"round half up": [["n", "scale"]],
|
|
1673
|
+
"round half down": [["n", "scale"]],
|
|
1674
|
+
abs: [["number"], ["n"]],
|
|
1675
|
+
modulo: [["dividend", "divisor"]],
|
|
1676
|
+
sqrt: [["number"]],
|
|
1677
|
+
log: [["number"]],
|
|
1678
|
+
exp: [["number"]],
|
|
1679
|
+
odd: [["number"]],
|
|
1680
|
+
even: [["number"]],
|
|
1681
|
+
"random number": [[]],
|
|
1682
|
+
// Context
|
|
1683
|
+
"get value": [
|
|
1684
|
+
["context", "key"],
|
|
1685
|
+
["context", "keys"],
|
|
1686
|
+
["m", "key"],
|
|
1687
|
+
["m", "keys"],
|
|
1688
|
+
],
|
|
1689
|
+
"get entries": [["context"], ["m"]],
|
|
1690
|
+
"context put": [
|
|
1691
|
+
["context", "key", "value"],
|
|
1692
|
+
["context", "keys", "value"],
|
|
1693
|
+
],
|
|
1694
|
+
"context merge": [["contexts"]],
|
|
1695
|
+
// Temporal
|
|
1696
|
+
now: [[]],
|
|
1697
|
+
today: [[]],
|
|
1698
|
+
"day of week": [["date"]],
|
|
1699
|
+
"day of year": [["date"]],
|
|
1700
|
+
"week of year": [["date"]],
|
|
1701
|
+
"month of year": [["date"]],
|
|
1702
|
+
"last day of month": [["date"]],
|
|
1703
|
+
// Range
|
|
1704
|
+
before: [
|
|
1705
|
+
["point1", "point2"],
|
|
1706
|
+
["range", "point"],
|
|
1707
|
+
["point", "range"],
|
|
1708
|
+
["range1", "range2"],
|
|
1709
|
+
],
|
|
1710
|
+
after: [
|
|
1711
|
+
["point1", "point2"],
|
|
1712
|
+
["range", "point"],
|
|
1713
|
+
["point", "range"],
|
|
1714
|
+
["range1", "range2"],
|
|
1715
|
+
],
|
|
1716
|
+
meets: [["range1", "range2"]],
|
|
1717
|
+
"met by": [["range1", "range2"]],
|
|
1718
|
+
overlaps: [["range1", "range2"]],
|
|
1719
|
+
"overlaps before": [["range1", "range2"]],
|
|
1720
|
+
"overlaps after": [["range1", "range2"]],
|
|
1721
|
+
finishes: [
|
|
1722
|
+
["point", "range"],
|
|
1723
|
+
["range1", "range2"],
|
|
1724
|
+
],
|
|
1725
|
+
"finished by": [
|
|
1726
|
+
["range", "point"],
|
|
1727
|
+
["range1", "range2"],
|
|
1728
|
+
],
|
|
1729
|
+
includes: [
|
|
1730
|
+
["range", "point"],
|
|
1731
|
+
["range1", "range2"],
|
|
1732
|
+
],
|
|
1733
|
+
during: [
|
|
1734
|
+
["point", "range"],
|
|
1735
|
+
["range1", "range2"],
|
|
1736
|
+
],
|
|
1737
|
+
starts: [
|
|
1738
|
+
["point", "range"],
|
|
1739
|
+
["range1", "range2"],
|
|
1740
|
+
],
|
|
1741
|
+
"started by": [
|
|
1742
|
+
["range", "point"],
|
|
1743
|
+
["range1", "range2"],
|
|
1744
|
+
],
|
|
1745
|
+
coincides: [
|
|
1746
|
+
["point1", "point2"],
|
|
1747
|
+
["range1", "range2"],
|
|
1748
|
+
],
|
|
1749
|
+
};
|
|
1750
|
+
// Built-ins DMN also defines over a bare argument list, so that max(1,2,3)
|
|
1751
|
+
// means max([1,2,3]). Their arity is not checked; everything else's is.
|
|
1752
|
+
const VARIADIC = new Set([
|
|
1753
|
+
"min",
|
|
1754
|
+
"max",
|
|
1755
|
+
"sum",
|
|
1756
|
+
"product",
|
|
1757
|
+
"mean",
|
|
1758
|
+
"median",
|
|
1759
|
+
"stddev",
|
|
1760
|
+
"mode",
|
|
1761
|
+
"all",
|
|
1762
|
+
"any",
|
|
1763
|
+
"count",
|
|
1764
|
+
"append",
|
|
1765
|
+
"concatenate",
|
|
1766
|
+
"union",
|
|
1767
|
+
"context merge",
|
|
1768
|
+
]);
|
|
1769
|
+
/** The argument counts a built-in accepts, or undefined when it takes any. */
|
|
1770
|
+
function aritiesOf(name) {
|
|
1771
|
+
if (VARIADIC.has(name))
|
|
1772
|
+
return undefined;
|
|
1773
|
+
const signatures = PARAM_SIGNATURES[name];
|
|
1774
|
+
if (!signatures)
|
|
1775
|
+
return undefined;
|
|
1776
|
+
return new Set(signatures.map((params) => params.length));
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Orders the arguments of a named invocation to match a built-in's signature.
|
|
1780
|
+
* Returns, for each parameter position, the index of the argument supplying
|
|
1781
|
+
* it, or null when no signature of the built-in accepts exactly these names —
|
|
1782
|
+
* which FEEL treats as an invocation error rather than a positional call.
|
|
1783
|
+
*/
|
|
1784
|
+
export function orderNamedArgs(name, argNames) {
|
|
1785
|
+
const signatures = PARAM_SIGNATURES[name];
|
|
1786
|
+
if (!signatures)
|
|
1787
|
+
return null;
|
|
1788
|
+
for (const params of signatures) {
|
|
1789
|
+
if (params.length !== argNames.length)
|
|
1790
|
+
continue;
|
|
1791
|
+
const order = params.map((param) => argNames.indexOf(param));
|
|
1792
|
+
if (order.every((idx) => idx >= 0))
|
|
1793
|
+
return order;
|
|
1794
|
+
}
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
// -------------------------------------------------------------------------
|
|
1179
1798
|
// Exports
|
|
1180
1799
|
// -------------------------------------------------------------------------
|
|
1800
|
+
// One FeelFunction wrapper per built-in, created on first use and shared: name
|
|
1801
|
+
// resolution runs for every identifier the evaluator meets, so allocating a
|
|
1802
|
+
// wrapper and closure per lookup showed up on every loop iteration.
|
|
1803
|
+
const builtinWrappers = new Map();
|
|
1181
1804
|
/** Look up a built-in function by name. Returns undefined if not found. */
|
|
1182
1805
|
export function getBuiltin(name) {
|
|
1806
|
+
const cached = builtinWrappers.get(name);
|
|
1807
|
+
if (cached)
|
|
1808
|
+
return cached;
|
|
1183
1809
|
const fn = builtinMap.get(name);
|
|
1184
1810
|
if (!fn)
|
|
1185
1811
|
return undefined;
|
|
1186
|
-
|
|
1812
|
+
const arities = aritiesOf(name);
|
|
1813
|
+
const wrapper = {
|
|
1814
|
+
type: "function",
|
|
1815
|
+
// Calling a built-in with a number of arguments no signature accepts is
|
|
1816
|
+
// an error, and FEEL reports an error as null.
|
|
1817
|
+
call: (args) => (arities && !arities.has(args.length) ? null : fn(...args)),
|
|
1818
|
+
};
|
|
1819
|
+
builtinWrappers.set(name, wrapper);
|
|
1820
|
+
return wrapper;
|
|
1187
1821
|
}
|
|
1188
1822
|
/** All built-in names. */
|
|
1189
1823
|
export function builtinNames() {
|