@bilig/formula 0.1.2 → 0.1.4
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/dist/builtins/placeholder.d.ts +2 -2
- package/dist/builtins/text-format-builtins.d.ts +14 -0
- package/dist/builtins/text-format-builtins.js +537 -0
- package/dist/builtins/text-format-builtins.js.map +1 -0
- package/dist/builtins/text-search-builtins.d.ts +21 -0
- package/dist/builtins/text-search-builtins.js +469 -0
- package/dist/builtins/text-search-builtins.js.map +1 -0
- package/dist/builtins/text.js +31 -968
- package/dist/builtins/text.js.map +1 -1
- package/dist/js-evaluator-array-special-calls.d.ts +28 -0
- package/dist/js-evaluator-array-special-calls.js +340 -0
- package/dist/js-evaluator-array-special-calls.js.map +1 -0
- package/dist/js-evaluator-context-special-calls.d.ts +24 -0
- package/dist/js-evaluator-context-special-calls.js +156 -0
- package/dist/js-evaluator-context-special-calls.js.map +1 -0
- package/dist/js-evaluator-types.d.ts +117 -0
- package/dist/js-evaluator-types.js +2 -0
- package/dist/js-evaluator-types.js.map +1 -0
- package/dist/js-evaluator-workbook-special-calls.d.ts +24 -0
- package/dist/js-evaluator-workbook-special-calls.js +185 -0
- package/dist/js-evaluator-workbook-special-calls.js.map +1 -0
- package/dist/js-evaluator.d.ts +3 -100
- package/dist/js-evaluator.js +58 -672
- package/dist/js-evaluator.js.map +1 -1
- package/package.json +6 -6
package/dist/builtins/text.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ErrorCode, ValueTag } from "@bilig/protocol";
|
|
2
|
-
import { excelSerialToDateParts } from "./datetime.js";
|
|
3
2
|
import { createBlockedBuiltinMap, textPlaceholderBuiltinNames } from "./placeholder.js";
|
|
4
3
|
import { createTextCoreBuiltins } from "./text-core-builtins.js";
|
|
4
|
+
import { createTextFormatBuiltins } from "./text-format-builtins.js";
|
|
5
|
+
import { createTextSearchBuiltins } from "./text-search-builtins.js";
|
|
5
6
|
function error(code) {
|
|
6
7
|
return { tag: ValueTag.Error, code };
|
|
7
8
|
}
|
|
@@ -210,512 +211,6 @@ function substituteText(text, oldText, newText, instance) {
|
|
|
210
211
|
}
|
|
211
212
|
return text;
|
|
212
213
|
}
|
|
213
|
-
function regexFlags(caseSensitivity, global = false) {
|
|
214
|
-
return `${global ? "g" : ""}${caseSensitivity === 1 ? "i" : ""}`;
|
|
215
|
-
}
|
|
216
|
-
function compileRegex(pattern, caseSensitivity, global = false) {
|
|
217
|
-
try {
|
|
218
|
-
return new RegExp(pattern, regexFlags(caseSensitivity, global));
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
return error(ErrorCode.Value);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
function isRegexError(value) {
|
|
225
|
-
return !(value instanceof RegExp);
|
|
226
|
-
}
|
|
227
|
-
function applyReplacementTemplate(template, match, captures) {
|
|
228
|
-
return template.replace(/\$(\$|&|[0-9]{1,2})/g, (_whole, token) => {
|
|
229
|
-
if (token === "$") {
|
|
230
|
-
return "$";
|
|
231
|
-
}
|
|
232
|
-
if (token === "&") {
|
|
233
|
-
return match;
|
|
234
|
-
}
|
|
235
|
-
const index = Number(token);
|
|
236
|
-
if (!Number.isInteger(index) || index <= 0) {
|
|
237
|
-
return "";
|
|
238
|
-
}
|
|
239
|
-
return captures[index - 1] ?? "";
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
function valueToTextResult(value, format) {
|
|
243
|
-
if (format !== 0 && format !== 1) {
|
|
244
|
-
return error(ErrorCode.Value);
|
|
245
|
-
}
|
|
246
|
-
switch (value.tag) {
|
|
247
|
-
case ValueTag.Empty:
|
|
248
|
-
return stringResult("");
|
|
249
|
-
case ValueTag.Number:
|
|
250
|
-
return stringResult(String(value.value));
|
|
251
|
-
case ValueTag.Boolean:
|
|
252
|
-
return stringResult(value.value ? "TRUE" : "FALSE");
|
|
253
|
-
case ValueTag.String:
|
|
254
|
-
return stringResult(format === 1 ? JSON.stringify(value.value) : value.value);
|
|
255
|
-
case ValueTag.Error: {
|
|
256
|
-
const label = value.code === ErrorCode.Div0
|
|
257
|
-
? "#DIV/0!"
|
|
258
|
-
: value.code === ErrorCode.Ref
|
|
259
|
-
? "#REF!"
|
|
260
|
-
: value.code === ErrorCode.Value
|
|
261
|
-
? "#VALUE!"
|
|
262
|
-
: value.code === ErrorCode.Name
|
|
263
|
-
? "#NAME?"
|
|
264
|
-
: value.code === ErrorCode.NA
|
|
265
|
-
? "#N/A"
|
|
266
|
-
: value.code === ErrorCode.Cycle
|
|
267
|
-
? "#CYCLE!"
|
|
268
|
-
: value.code === ErrorCode.Spill
|
|
269
|
-
? "#SPILL!"
|
|
270
|
-
: value.code === ErrorCode.Blocked
|
|
271
|
-
? "#BLOCKED!"
|
|
272
|
-
: "#ERROR!";
|
|
273
|
-
return stringResult(label);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const shortMonthNames = [
|
|
278
|
-
"Jan",
|
|
279
|
-
"Feb",
|
|
280
|
-
"Mar",
|
|
281
|
-
"Apr",
|
|
282
|
-
"May",
|
|
283
|
-
"Jun",
|
|
284
|
-
"Jul",
|
|
285
|
-
"Aug",
|
|
286
|
-
"Sep",
|
|
287
|
-
"Oct",
|
|
288
|
-
"Nov",
|
|
289
|
-
"Dec",
|
|
290
|
-
];
|
|
291
|
-
const fullMonthNames = [
|
|
292
|
-
"January",
|
|
293
|
-
"February",
|
|
294
|
-
"March",
|
|
295
|
-
"April",
|
|
296
|
-
"May",
|
|
297
|
-
"June",
|
|
298
|
-
"July",
|
|
299
|
-
"August",
|
|
300
|
-
"September",
|
|
301
|
-
"October",
|
|
302
|
-
"November",
|
|
303
|
-
"December",
|
|
304
|
-
];
|
|
305
|
-
const shortWeekdayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
306
|
-
const fullWeekdayNames = [
|
|
307
|
-
"Sunday",
|
|
308
|
-
"Monday",
|
|
309
|
-
"Tuesday",
|
|
310
|
-
"Wednesday",
|
|
311
|
-
"Thursday",
|
|
312
|
-
"Friday",
|
|
313
|
-
"Saturday",
|
|
314
|
-
];
|
|
315
|
-
function splitFormatSections(format) {
|
|
316
|
-
const sections = [];
|
|
317
|
-
let current = "";
|
|
318
|
-
let inQuotes = false;
|
|
319
|
-
let bracketDepth = 0;
|
|
320
|
-
let escaped = false;
|
|
321
|
-
for (let index = 0; index < format.length; index += 1) {
|
|
322
|
-
const char = format[index];
|
|
323
|
-
if (escaped) {
|
|
324
|
-
current += char;
|
|
325
|
-
escaped = false;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (char === "\\") {
|
|
329
|
-
current += char;
|
|
330
|
-
escaped = true;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (char === '"') {
|
|
334
|
-
current += char;
|
|
335
|
-
inQuotes = !inQuotes;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (!inQuotes && char === "[") {
|
|
339
|
-
bracketDepth += 1;
|
|
340
|
-
current += char;
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
if (!inQuotes && char === "]" && bracketDepth > 0) {
|
|
344
|
-
bracketDepth -= 1;
|
|
345
|
-
current += char;
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (!inQuotes && bracketDepth === 0 && char === ";") {
|
|
349
|
-
sections.push(current);
|
|
350
|
-
current = "";
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
current += char;
|
|
354
|
-
}
|
|
355
|
-
sections.push(current);
|
|
356
|
-
return sections;
|
|
357
|
-
}
|
|
358
|
-
function stripFormatDecorations(section) {
|
|
359
|
-
let output = "";
|
|
360
|
-
let inQuotes = false;
|
|
361
|
-
for (let index = 0; index < section.length; index += 1) {
|
|
362
|
-
const char = section[index];
|
|
363
|
-
if (inQuotes) {
|
|
364
|
-
if (char === '"') {
|
|
365
|
-
inQuotes = false;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
output += char;
|
|
369
|
-
}
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
if (char === '"') {
|
|
373
|
-
inQuotes = true;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (char === "\\") {
|
|
377
|
-
output += section[index + 1] ?? "";
|
|
378
|
-
index += 1;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
if (char === "_") {
|
|
382
|
-
output += " ";
|
|
383
|
-
index += 1;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (char === "*") {
|
|
387
|
-
index += 1;
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (char === "[") {
|
|
391
|
-
const end = section.indexOf("]", index + 1);
|
|
392
|
-
if (end === -1) {
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
index = end;
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
output += char;
|
|
399
|
-
}
|
|
400
|
-
return output;
|
|
401
|
-
}
|
|
402
|
-
function formatThousandsText(integerPart) {
|
|
403
|
-
return integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
404
|
-
}
|
|
405
|
-
function zeroPadText(value, width) {
|
|
406
|
-
return String(Math.trunc(Math.abs(value))).padStart(width, "0");
|
|
407
|
-
}
|
|
408
|
-
function roundToDigits(value, digits) {
|
|
409
|
-
if (!Number.isFinite(value)) {
|
|
410
|
-
return Number.NaN;
|
|
411
|
-
}
|
|
412
|
-
const factor = 10 ** Math.max(0, digits);
|
|
413
|
-
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
414
|
-
}
|
|
415
|
-
function excelSecondOfDay(serial) {
|
|
416
|
-
if (!Number.isFinite(serial)) {
|
|
417
|
-
return undefined;
|
|
418
|
-
}
|
|
419
|
-
const whole = Math.floor(serial);
|
|
420
|
-
let fraction = serial - whole;
|
|
421
|
-
if (fraction < 0) {
|
|
422
|
-
fraction += 1;
|
|
423
|
-
}
|
|
424
|
-
let seconds = Math.floor(fraction * 86_400 + 1e-9);
|
|
425
|
-
if (seconds >= 86_400) {
|
|
426
|
-
seconds = 0;
|
|
427
|
-
}
|
|
428
|
-
return seconds;
|
|
429
|
-
}
|
|
430
|
-
function excelWeekdayIndex(serial) {
|
|
431
|
-
if (!Number.isFinite(serial)) {
|
|
432
|
-
return undefined;
|
|
433
|
-
}
|
|
434
|
-
const whole = Math.floor(serial);
|
|
435
|
-
if (whole < 0) {
|
|
436
|
-
return undefined;
|
|
437
|
-
}
|
|
438
|
-
const adjustedWhole = whole < 60 ? whole : whole - 1;
|
|
439
|
-
return ((adjustedWhole % 7) + 7) % 7;
|
|
440
|
-
}
|
|
441
|
-
function isDateTimeFormat(section) {
|
|
442
|
-
const cleaned = stripFormatDecorations(section).toUpperCase();
|
|
443
|
-
return (cleaned.includes("AM/PM") ||
|
|
444
|
-
cleaned.includes("A/P") ||
|
|
445
|
-
/[YDSH]/.test(cleaned) ||
|
|
446
|
-
/(^|[^0#?])M+([^0#?]|$)/.test(cleaned));
|
|
447
|
-
}
|
|
448
|
-
function isTextFormat(section) {
|
|
449
|
-
return stripFormatDecorations(section).includes("@");
|
|
450
|
-
}
|
|
451
|
-
function chooseFormatSection(value, formatText) {
|
|
452
|
-
const sections = splitFormatSections(formatText);
|
|
453
|
-
if (value.tag === ValueTag.String) {
|
|
454
|
-
return { section: sections[3] ?? sections[0] ?? "", autoNegative: false };
|
|
455
|
-
}
|
|
456
|
-
const numeric = coerceNumber(value);
|
|
457
|
-
if (numeric === undefined) {
|
|
458
|
-
return error(ErrorCode.Value);
|
|
459
|
-
}
|
|
460
|
-
if (numeric < 0) {
|
|
461
|
-
if (sections[1] !== undefined) {
|
|
462
|
-
return { section: sections[1], numeric: -numeric, autoNegative: false };
|
|
463
|
-
}
|
|
464
|
-
return { section: sections[0] ?? "", numeric: -numeric, autoNegative: true };
|
|
465
|
-
}
|
|
466
|
-
if (numeric === 0 && sections[2] !== undefined) {
|
|
467
|
-
return { section: sections[2], numeric, autoNegative: false };
|
|
468
|
-
}
|
|
469
|
-
return { section: sections[0] ?? "", numeric, autoNegative: false };
|
|
470
|
-
}
|
|
471
|
-
function formatTextSectionValue(value, section) {
|
|
472
|
-
const cleaned = stripFormatDecorations(section);
|
|
473
|
-
return cleaned.includes("@") ? cleaned.replace(/@/g, value) : cleaned;
|
|
474
|
-
}
|
|
475
|
-
function tokenizeDateTimeFormat(section) {
|
|
476
|
-
const cleaned = stripFormatDecorations(section);
|
|
477
|
-
const tokens = [];
|
|
478
|
-
let index = 0;
|
|
479
|
-
while (index < cleaned.length) {
|
|
480
|
-
const remainder = cleaned.slice(index);
|
|
481
|
-
const upperRemainder = remainder.toUpperCase();
|
|
482
|
-
if (upperRemainder.startsWith("AM/PM")) {
|
|
483
|
-
tokens.push({ kind: "ampm", text: cleaned.slice(index, index + 5) });
|
|
484
|
-
index += 5;
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
if (upperRemainder.startsWith("A/P")) {
|
|
488
|
-
tokens.push({ kind: "ampm", text: cleaned.slice(index, index + 3) });
|
|
489
|
-
index += 3;
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
const char = cleaned[index];
|
|
493
|
-
const lower = char.toLowerCase();
|
|
494
|
-
if ("ymdhms".includes(lower)) {
|
|
495
|
-
let end = index + 1;
|
|
496
|
-
while (end < cleaned.length && cleaned[end].toLowerCase() === lower) {
|
|
497
|
-
end += 1;
|
|
498
|
-
}
|
|
499
|
-
const tokenText = cleaned.slice(index, end);
|
|
500
|
-
const baseKind = lower === "y"
|
|
501
|
-
? "year"
|
|
502
|
-
: lower === "d"
|
|
503
|
-
? "day"
|
|
504
|
-
: lower === "h"
|
|
505
|
-
? "hour"
|
|
506
|
-
: lower === "s"
|
|
507
|
-
? "second"
|
|
508
|
-
: "month";
|
|
509
|
-
tokens.push({ kind: baseKind, text: tokenText });
|
|
510
|
-
index = end;
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
tokens.push({ kind: "literal", text: char });
|
|
514
|
-
index += 1;
|
|
515
|
-
}
|
|
516
|
-
return tokens.map((token, tokenIndex, allTokens) => {
|
|
517
|
-
if (token.kind !== "month") {
|
|
518
|
-
return token;
|
|
519
|
-
}
|
|
520
|
-
const previous = allTokens.slice(0, tokenIndex).findLast((entry) => entry.kind !== "literal");
|
|
521
|
-
const next = allTokens.slice(tokenIndex + 1).find((entry) => entry.kind !== "literal");
|
|
522
|
-
if (previous?.kind === "hour" || previous?.kind === "minute" || next?.kind === "second") {
|
|
523
|
-
return { kind: "minute", text: token.text };
|
|
524
|
-
}
|
|
525
|
-
return token;
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
function formatAmPmToken(token, hour) {
|
|
529
|
-
const isPm = hour >= 12;
|
|
530
|
-
const upper = token.toUpperCase();
|
|
531
|
-
if (upper === "A/P") {
|
|
532
|
-
const letter = isPm ? "P" : "A";
|
|
533
|
-
return token === token.toLowerCase() ? letter.toLowerCase() : letter;
|
|
534
|
-
}
|
|
535
|
-
if (token === token.toLowerCase()) {
|
|
536
|
-
return isPm ? "pm" : "am";
|
|
537
|
-
}
|
|
538
|
-
return isPm ? "PM" : "AM";
|
|
539
|
-
}
|
|
540
|
-
function formatDateTimeSectionValue(serial, section) {
|
|
541
|
-
const dateParts = excelSerialToDateParts(serial);
|
|
542
|
-
const weekdayIndex = excelWeekdayIndex(serial);
|
|
543
|
-
const secondOfDay = excelSecondOfDay(serial);
|
|
544
|
-
if (!dateParts || weekdayIndex === undefined || secondOfDay === undefined) {
|
|
545
|
-
return undefined;
|
|
546
|
-
}
|
|
547
|
-
const hour24 = Math.floor(secondOfDay / 3600);
|
|
548
|
-
const minute = Math.floor((secondOfDay % 3600) / 60);
|
|
549
|
-
const second = secondOfDay % 60;
|
|
550
|
-
const tokens = tokenizeDateTimeFormat(section);
|
|
551
|
-
const hasAmPm = tokens.some((token) => token.kind === "ampm");
|
|
552
|
-
return tokens
|
|
553
|
-
.map((token) => {
|
|
554
|
-
switch (token.kind) {
|
|
555
|
-
case "literal":
|
|
556
|
-
return token.text;
|
|
557
|
-
case "year":
|
|
558
|
-
return token.text.length === 2
|
|
559
|
-
? zeroPadText(dateParts.year % 100, 2)
|
|
560
|
-
: String(dateParts.year).padStart(Math.max(4, token.text.length), "0");
|
|
561
|
-
case "month":
|
|
562
|
-
return token.text.length === 1
|
|
563
|
-
? String(dateParts.month)
|
|
564
|
-
: token.text.length === 2
|
|
565
|
-
? zeroPadText(dateParts.month, 2)
|
|
566
|
-
: token.text.length === 3
|
|
567
|
-
? shortMonthNames[dateParts.month - 1]
|
|
568
|
-
: fullMonthNames[dateParts.month - 1];
|
|
569
|
-
case "minute":
|
|
570
|
-
return token.text.length >= 2 ? zeroPadText(minute, 2) : String(minute);
|
|
571
|
-
case "day":
|
|
572
|
-
return token.text.length === 1
|
|
573
|
-
? String(dateParts.day)
|
|
574
|
-
: token.text.length === 2
|
|
575
|
-
? zeroPadText(dateParts.day, 2)
|
|
576
|
-
: token.text.length === 3
|
|
577
|
-
? shortWeekdayNames[weekdayIndex]
|
|
578
|
-
: fullWeekdayNames[weekdayIndex];
|
|
579
|
-
case "hour": {
|
|
580
|
-
const normalizedHour = hasAmPm ? ((hour24 + 11) % 12) + 1 : hour24;
|
|
581
|
-
return token.text.length >= 2 ? zeroPadText(normalizedHour, 2) : String(normalizedHour);
|
|
582
|
-
}
|
|
583
|
-
case "second":
|
|
584
|
-
return token.text.length >= 2 ? zeroPadText(second, 2) : String(second);
|
|
585
|
-
case "ampm":
|
|
586
|
-
return formatAmPmToken(token.text, hour24);
|
|
587
|
-
}
|
|
588
|
-
})
|
|
589
|
-
.join("");
|
|
590
|
-
}
|
|
591
|
-
function trimOptionalFractionDigits(fraction, minDigits) {
|
|
592
|
-
let trimmed = fraction;
|
|
593
|
-
while (trimmed.length > minDigits && trimmed.endsWith("0")) {
|
|
594
|
-
trimmed = trimmed.slice(0, -1);
|
|
595
|
-
}
|
|
596
|
-
return trimmed;
|
|
597
|
-
}
|
|
598
|
-
function formatScientificSection(value, core) {
|
|
599
|
-
const exponentIndex = core.search(/[Ee][+-]/);
|
|
600
|
-
const mantissaPattern = core.slice(0, exponentIndex);
|
|
601
|
-
const exponentPattern = core.slice(exponentIndex + 2);
|
|
602
|
-
const mantissaParts = mantissaPattern.split(".");
|
|
603
|
-
const fractionPattern = mantissaParts[1] ?? "";
|
|
604
|
-
const maxFractionDigits = (fractionPattern.match(/[0#?]/g) ?? []).length;
|
|
605
|
-
const minFractionDigits = (fractionPattern.match(/0/g) ?? []).length;
|
|
606
|
-
const [mantissaRaw = "0", exponentRaw] = value.toExponential(maxFractionDigits).split("e");
|
|
607
|
-
let [integerPart = "0", fractionPart = ""] = mantissaRaw.split(".");
|
|
608
|
-
fractionPart = trimOptionalFractionDigits(fractionPart, minFractionDigits);
|
|
609
|
-
const exponentValue = Number(exponentRaw ?? 0);
|
|
610
|
-
const exponentText = String(Math.abs(exponentValue)).padStart(exponentPattern.length, "0");
|
|
611
|
-
return `${integerPart}${fractionPart === "" ? "" : `.${fractionPart}`}E${exponentValue < 0 ? "-" : "+"}${exponentText}`;
|
|
612
|
-
}
|
|
613
|
-
function formatNumericSectionValue(value, section, autoNegative) {
|
|
614
|
-
const cleaned = stripFormatDecorations(section);
|
|
615
|
-
if (!/[0#?]/.test(cleaned)) {
|
|
616
|
-
return autoNegative && !cleaned.startsWith("-") ? `-${cleaned}` : cleaned;
|
|
617
|
-
}
|
|
618
|
-
const firstPlaceholder = cleaned.search(/[0#?]/);
|
|
619
|
-
let lastPlaceholder = -1;
|
|
620
|
-
for (let index = cleaned.length - 1; index >= 0; index -= 1) {
|
|
621
|
-
if (/[0#?]/.test(cleaned[index])) {
|
|
622
|
-
lastPlaceholder = index;
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
const prefix = cleaned.slice(0, firstPlaceholder);
|
|
627
|
-
const core = cleaned.slice(firstPlaceholder, lastPlaceholder + 1);
|
|
628
|
-
const suffix = cleaned.slice(lastPlaceholder + 1);
|
|
629
|
-
const percentCount = (cleaned.match(/%/g) ?? []).length;
|
|
630
|
-
const scaledValue = Math.abs(value) * 100 ** percentCount;
|
|
631
|
-
let numericText = "";
|
|
632
|
-
if (/[Ee][+-]/.test(core)) {
|
|
633
|
-
numericText = formatScientificSection(scaledValue, core);
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
const decimalIndex = core.indexOf(".");
|
|
637
|
-
const integerPattern = (decimalIndex === -1 ? core : core.slice(0, decimalIndex)).replaceAll(",", "");
|
|
638
|
-
const fractionPattern = decimalIndex === -1 ? "" : core.slice(decimalIndex + 1);
|
|
639
|
-
const maxFractionDigits = (fractionPattern.match(/[0#?]/g) ?? []).length;
|
|
640
|
-
const minFractionDigits = (fractionPattern.match(/0/g) ?? []).length;
|
|
641
|
-
const minIntegerDigits = (integerPattern.match(/0/g) ?? []).length;
|
|
642
|
-
const roundedValue = roundToDigits(scaledValue, maxFractionDigits);
|
|
643
|
-
const fixed = roundedValue.toFixed(maxFractionDigits);
|
|
644
|
-
let [integerPart = "0", fractionPart = ""] = fixed.split(".");
|
|
645
|
-
if (integerPart.length < minIntegerDigits) {
|
|
646
|
-
integerPart = integerPart.padStart(minIntegerDigits, "0");
|
|
647
|
-
}
|
|
648
|
-
if (core.includes(",")) {
|
|
649
|
-
integerPart = formatThousandsText(integerPart);
|
|
650
|
-
}
|
|
651
|
-
fractionPart = trimOptionalFractionDigits(fractionPart, minFractionDigits);
|
|
652
|
-
numericText = `${integerPart}${fractionPart === "" ? "" : `.${fractionPart}`}`;
|
|
653
|
-
}
|
|
654
|
-
const combined = `${prefix}${numericText}${suffix}`;
|
|
655
|
-
return autoNegative && !combined.startsWith("-") ? `-${combined}` : combined;
|
|
656
|
-
}
|
|
657
|
-
function formatTextBuiltinValue(value, formatText) {
|
|
658
|
-
const chosen = chooseFormatSection(value, formatText);
|
|
659
|
-
if ("tag" in chosen) {
|
|
660
|
-
return chosen;
|
|
661
|
-
}
|
|
662
|
-
const { section, numeric, autoNegative } = chosen;
|
|
663
|
-
if (value.tag === ValueTag.String) {
|
|
664
|
-
const cleaned = stripFormatDecorations(section);
|
|
665
|
-
if (isTextFormat(section) || !/[0#?YMDHS]/i.test(cleaned)) {
|
|
666
|
-
return stringResult(formatTextSectionValue(value.value, section));
|
|
667
|
-
}
|
|
668
|
-
return error(ErrorCode.Value);
|
|
669
|
-
}
|
|
670
|
-
if (numeric === undefined) {
|
|
671
|
-
return error(ErrorCode.Value);
|
|
672
|
-
}
|
|
673
|
-
if (isDateTimeFormat(section)) {
|
|
674
|
-
const formatted = formatDateTimeSectionValue(numeric, section);
|
|
675
|
-
return formatted === undefined ? error(ErrorCode.Value) : stringResult(formatted);
|
|
676
|
-
}
|
|
677
|
-
return stringResult(formatNumericSectionValue(numeric, section, autoNegative));
|
|
678
|
-
}
|
|
679
|
-
function parseNumberValueText(input, decimalSeparator, groupSeparator) {
|
|
680
|
-
const compact = input.replaceAll(/\s+/g, "");
|
|
681
|
-
if (compact === "") {
|
|
682
|
-
return 0;
|
|
683
|
-
}
|
|
684
|
-
const percentMatch = compact.match(/%+$/);
|
|
685
|
-
const percentCount = percentMatch?.[0].length ?? 0;
|
|
686
|
-
const core = percentCount === 0 ? compact : compact.slice(0, -percentCount);
|
|
687
|
-
if (core.includes("%")) {
|
|
688
|
-
return undefined;
|
|
689
|
-
}
|
|
690
|
-
if (decimalSeparator !== "" && groupSeparator !== "" && decimalSeparator === groupSeparator) {
|
|
691
|
-
return undefined;
|
|
692
|
-
}
|
|
693
|
-
const decimal = decimalSeparator === "" ? "." : decimalSeparator[0];
|
|
694
|
-
const group = groupSeparator === "" ? "" : groupSeparator[0];
|
|
695
|
-
const decimalIndex = decimal === "" ? -1 : core.indexOf(decimal);
|
|
696
|
-
if (decimalIndex !== -1 && core.indexOf(decimal, decimalIndex + 1) !== -1) {
|
|
697
|
-
return undefined;
|
|
698
|
-
}
|
|
699
|
-
let normalized = core;
|
|
700
|
-
if (group !== "") {
|
|
701
|
-
const groupAfterDecimal = decimalIndex === -1 ? -1 : normalized.indexOf(group, decimalIndex + decimal.length);
|
|
702
|
-
if (groupAfterDecimal !== -1) {
|
|
703
|
-
return undefined;
|
|
704
|
-
}
|
|
705
|
-
normalized = normalized.replaceAll(group, "");
|
|
706
|
-
}
|
|
707
|
-
if (decimal !== "." && decimal !== "") {
|
|
708
|
-
normalized = normalized.replace(decimal, ".");
|
|
709
|
-
}
|
|
710
|
-
if (normalized === "" || normalized === "." || normalized === "+" || normalized === "-") {
|
|
711
|
-
return undefined;
|
|
712
|
-
}
|
|
713
|
-
const parsed = Number(normalized);
|
|
714
|
-
if (!Number.isFinite(parsed)) {
|
|
715
|
-
return undefined;
|
|
716
|
-
}
|
|
717
|
-
return parsed / 100 ** percentCount;
|
|
718
|
-
}
|
|
719
214
|
function createReplaceBuiltin() {
|
|
720
215
|
return (...args) => {
|
|
721
216
|
const existingError = firstError(args);
|
|
@@ -790,82 +285,6 @@ function createReptBuiltin() {
|
|
|
790
285
|
return stringResult(repeated);
|
|
791
286
|
};
|
|
792
287
|
}
|
|
793
|
-
function escapeRegExp(value) {
|
|
794
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
795
|
-
}
|
|
796
|
-
function indexOfWithMode(text, delimiter, start, matchMode) {
|
|
797
|
-
if (matchMode === 1) {
|
|
798
|
-
return text.toLowerCase().indexOf(delimiter.toLowerCase(), start);
|
|
799
|
-
}
|
|
800
|
-
return text.indexOf(delimiter, start);
|
|
801
|
-
}
|
|
802
|
-
function lastIndexOfWithMode(text, delimiter, start, matchMode) {
|
|
803
|
-
if (matchMode === 1) {
|
|
804
|
-
return text.toLowerCase().lastIndexOf(delimiter.toLowerCase(), start);
|
|
805
|
-
}
|
|
806
|
-
return text.lastIndexOf(delimiter, start);
|
|
807
|
-
}
|
|
808
|
-
function hasSearchSyntax(pattern) {
|
|
809
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
810
|
-
const char = pattern[index];
|
|
811
|
-
if (char === "~") {
|
|
812
|
-
return true;
|
|
813
|
-
}
|
|
814
|
-
if (char === "*" || char === "?") {
|
|
815
|
-
return true;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
function buildSearchRegex(pattern) {
|
|
821
|
-
let source = "^";
|
|
822
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
823
|
-
const char = pattern[index];
|
|
824
|
-
if (char === "~") {
|
|
825
|
-
const next = pattern[index + 1];
|
|
826
|
-
if (next === undefined) {
|
|
827
|
-
source += escapeRegExp(char);
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
source += escapeRegExp(next);
|
|
831
|
-
index += 1;
|
|
832
|
-
}
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (char === "*") {
|
|
836
|
-
source += "[\\s\\S]*";
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
if (char === "?") {
|
|
840
|
-
source += "[\\s\\S]";
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
source += escapeRegExp(char);
|
|
844
|
-
}
|
|
845
|
-
return new RegExp(source, "i");
|
|
846
|
-
}
|
|
847
|
-
function findPosition(needle, haystack, start, caseSensitive, wildcardAware) {
|
|
848
|
-
const startIndex = start - 1;
|
|
849
|
-
if (needle === "") {
|
|
850
|
-
return start;
|
|
851
|
-
}
|
|
852
|
-
if (startIndex > haystack.length) {
|
|
853
|
-
return error(ErrorCode.Value);
|
|
854
|
-
}
|
|
855
|
-
if (!wildcardAware || !hasSearchSyntax(needle)) {
|
|
856
|
-
const normalizedHaystack = caseSensitive ? haystack : haystack.toLowerCase();
|
|
857
|
-
const normalizedNeedle = caseSensitive ? needle : needle.toLowerCase();
|
|
858
|
-
const found = normalizedHaystack.indexOf(normalizedNeedle, startIndex);
|
|
859
|
-
return found === -1 ? error(ErrorCode.Value) : found + 1;
|
|
860
|
-
}
|
|
861
|
-
const regex = buildSearchRegex(needle);
|
|
862
|
-
for (let index = startIndex; index <= haystack.length; index += 1) {
|
|
863
|
-
if (regex.test(haystack.slice(index))) {
|
|
864
|
-
return index + 1;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
return error(ErrorCode.Value);
|
|
868
|
-
}
|
|
869
288
|
function charCodeFromArgument(value) {
|
|
870
289
|
if (value === undefined) {
|
|
871
290
|
return error(ErrorCode.Value);
|
|
@@ -889,6 +308,33 @@ const textCoreBuiltins = createTextCoreBuiltins({
|
|
|
889
308
|
coerceText,
|
|
890
309
|
coerceNumber,
|
|
891
310
|
});
|
|
311
|
+
const textFormatBuiltins = createTextFormatBuiltins({
|
|
312
|
+
error,
|
|
313
|
+
stringResult,
|
|
314
|
+
numberResult,
|
|
315
|
+
firstError,
|
|
316
|
+
coerceText,
|
|
317
|
+
coerceNumber,
|
|
318
|
+
coerceInteger,
|
|
319
|
+
isErrorValue,
|
|
320
|
+
});
|
|
321
|
+
const textSearchBuiltins = createTextSearchBuiltins({
|
|
322
|
+
error,
|
|
323
|
+
stringResult,
|
|
324
|
+
numberResult,
|
|
325
|
+
booleanResult,
|
|
326
|
+
firstError,
|
|
327
|
+
coerceText,
|
|
328
|
+
coerceNumber,
|
|
329
|
+
coerceBoolean,
|
|
330
|
+
coerceInteger,
|
|
331
|
+
coercePositiveStart,
|
|
332
|
+
isErrorValue,
|
|
333
|
+
utf8Bytes,
|
|
334
|
+
findSubBytes,
|
|
335
|
+
bytePositionToCharPosition,
|
|
336
|
+
charPositionToBytePosition,
|
|
337
|
+
});
|
|
892
338
|
export const textBuiltins = {
|
|
893
339
|
LEN: (...args) => {
|
|
894
340
|
const existingError = firstError(args);
|
|
@@ -959,18 +405,9 @@ export const textBuiltins = {
|
|
|
959
405
|
}
|
|
960
406
|
return stringResult(String.fromCodePoint(integerCode));
|
|
961
407
|
},
|
|
962
|
-
TEXT: (...args) => {
|
|
963
|
-
const existingError = firstError(args);
|
|
964
|
-
if (existingError) {
|
|
965
|
-
return existingError;
|
|
966
|
-
}
|
|
967
|
-
const [value, formatValue] = args;
|
|
968
|
-
if (value === undefined || formatValue === undefined) {
|
|
969
|
-
return error(ErrorCode.Value);
|
|
970
|
-
}
|
|
971
|
-
return formatTextBuiltinValue(value, coerceText(formatValue));
|
|
972
|
-
},
|
|
973
408
|
...textCoreBuiltins,
|
|
409
|
+
...textFormatBuiltins,
|
|
410
|
+
...textSearchBuiltins,
|
|
974
411
|
LEFT: (...args) => {
|
|
975
412
|
const existingError = firstError(args);
|
|
976
413
|
if (existingError) {
|
|
@@ -1022,58 +459,6 @@ export const textBuiltins = {
|
|
|
1022
459
|
const text = coerceText(textValue);
|
|
1023
460
|
return stringResult(text.slice(start - 1, start - 1 + count));
|
|
1024
461
|
},
|
|
1025
|
-
FIND: (...args) => {
|
|
1026
|
-
const existingError = firstError(args);
|
|
1027
|
-
if (existingError) {
|
|
1028
|
-
return existingError;
|
|
1029
|
-
}
|
|
1030
|
-
const [findTextValue, withinTextValue, startValue] = args;
|
|
1031
|
-
if (findTextValue === undefined || withinTextValue === undefined) {
|
|
1032
|
-
return error(ErrorCode.Value);
|
|
1033
|
-
}
|
|
1034
|
-
const start = coercePositiveStart(startValue, 1);
|
|
1035
|
-
if (isErrorValue(start)) {
|
|
1036
|
-
return start;
|
|
1037
|
-
}
|
|
1038
|
-
const found = findPosition(coerceText(findTextValue), coerceText(withinTextValue), start, true, false);
|
|
1039
|
-
return isErrorValue(found) ? found : numberResult(found);
|
|
1040
|
-
},
|
|
1041
|
-
SEARCH: (...args) => {
|
|
1042
|
-
const existingError = firstError(args);
|
|
1043
|
-
if (existingError) {
|
|
1044
|
-
return existingError;
|
|
1045
|
-
}
|
|
1046
|
-
const [findTextValue, withinTextValue, startValue] = args;
|
|
1047
|
-
if (findTextValue === undefined || withinTextValue === undefined) {
|
|
1048
|
-
return error(ErrorCode.Value);
|
|
1049
|
-
}
|
|
1050
|
-
const start = coercePositiveStart(startValue, 1);
|
|
1051
|
-
if (isErrorValue(start)) {
|
|
1052
|
-
return start;
|
|
1053
|
-
}
|
|
1054
|
-
const found = findPosition(coerceText(findTextValue), coerceText(withinTextValue), start, false, true);
|
|
1055
|
-
return isErrorValue(found) ? found : numberResult(found);
|
|
1056
|
-
},
|
|
1057
|
-
SEARCHB: (...args) => {
|
|
1058
|
-
const existingError = firstError(args);
|
|
1059
|
-
if (existingError) {
|
|
1060
|
-
return existingError;
|
|
1061
|
-
}
|
|
1062
|
-
const [findTextValue, withinTextValue, startValue] = args;
|
|
1063
|
-
if (findTextValue === undefined || withinTextValue === undefined) {
|
|
1064
|
-
return error(ErrorCode.Value);
|
|
1065
|
-
}
|
|
1066
|
-
const text = coerceText(withinTextValue);
|
|
1067
|
-
const start = coercePositiveStart(startValue, 1);
|
|
1068
|
-
if (isErrorValue(start)) {
|
|
1069
|
-
return start;
|
|
1070
|
-
}
|
|
1071
|
-
if (start > utf8Bytes(text).length + 1) {
|
|
1072
|
-
return error(ErrorCode.Value);
|
|
1073
|
-
}
|
|
1074
|
-
const found = findPosition(coerceText(findTextValue), text, bytePositionToCharPosition(text, start), false, true);
|
|
1075
|
-
return isErrorValue(found) ? found : numberResult(charPositionToBytePosition(text, found));
|
|
1076
|
-
},
|
|
1077
462
|
ENCODEURL: (...args) => {
|
|
1078
463
|
const existingError = firstError(args);
|
|
1079
464
|
if (existingError) {
|
|
@@ -1085,27 +470,6 @@ export const textBuiltins = {
|
|
|
1085
470
|
}
|
|
1086
471
|
return stringResult(encodeURI(coerceText(value)));
|
|
1087
472
|
},
|
|
1088
|
-
FINDB: (...args) => {
|
|
1089
|
-
const existingError = firstError(args);
|
|
1090
|
-
if (existingError) {
|
|
1091
|
-
return existingError;
|
|
1092
|
-
}
|
|
1093
|
-
const [findTextValue, withinTextValue, startValue] = args;
|
|
1094
|
-
if (findTextValue === undefined || withinTextValue === undefined) {
|
|
1095
|
-
return error(ErrorCode.Value);
|
|
1096
|
-
}
|
|
1097
|
-
const start = coercePositiveStart(startValue, 1);
|
|
1098
|
-
if (isErrorValue(start)) {
|
|
1099
|
-
return start;
|
|
1100
|
-
}
|
|
1101
|
-
const findBytes = utf8Bytes(coerceText(findTextValue));
|
|
1102
|
-
const withinBytes = utf8Bytes(coerceText(withinTextValue));
|
|
1103
|
-
if (start > withinBytes.length + 1) {
|
|
1104
|
-
return error(ErrorCode.Value);
|
|
1105
|
-
}
|
|
1106
|
-
const found = findSubBytes(withinBytes, findBytes, start - 1);
|
|
1107
|
-
return found === -1 ? error(ErrorCode.Value) : numberResult(found + 1);
|
|
1108
|
-
},
|
|
1109
473
|
LEFTB: (...args) => {
|
|
1110
474
|
const existingError = firstError(args);
|
|
1111
475
|
if (existingError) {
|
|
@@ -1155,307 +519,6 @@ export const textBuiltins = {
|
|
|
1155
519
|
}
|
|
1156
520
|
return stringResult(rightBytes(coerceText(textValue), count));
|
|
1157
521
|
},
|
|
1158
|
-
VALUE: (...args) => {
|
|
1159
|
-
const existingError = firstError(args);
|
|
1160
|
-
if (existingError) {
|
|
1161
|
-
return existingError;
|
|
1162
|
-
}
|
|
1163
|
-
const [value] = args;
|
|
1164
|
-
if (value === undefined) {
|
|
1165
|
-
return error(ErrorCode.Value);
|
|
1166
|
-
}
|
|
1167
|
-
const coerced = coerceNumber(value);
|
|
1168
|
-
return coerced === undefined ? error(ErrorCode.Value) : numberResult(coerced);
|
|
1169
|
-
},
|
|
1170
|
-
NUMBERVALUE: (...args) => {
|
|
1171
|
-
const existingError = firstError(args);
|
|
1172
|
-
if (existingError) {
|
|
1173
|
-
return existingError;
|
|
1174
|
-
}
|
|
1175
|
-
const [textValue, decimalSeparatorValue, groupSeparatorValue] = args;
|
|
1176
|
-
if (textValue === undefined) {
|
|
1177
|
-
return error(ErrorCode.Value);
|
|
1178
|
-
}
|
|
1179
|
-
const text = coerceText(textValue);
|
|
1180
|
-
const decimalSeparator = decimalSeparatorValue === undefined ? "." : coerceText(decimalSeparatorValue);
|
|
1181
|
-
const groupSeparator = groupSeparatorValue === undefined ? "," : coerceText(groupSeparatorValue);
|
|
1182
|
-
const parsed = parseNumberValueText(text, decimalSeparator, groupSeparator);
|
|
1183
|
-
return parsed === undefined ? error(ErrorCode.Value) : numberResult(parsed);
|
|
1184
|
-
},
|
|
1185
|
-
VALUETOTEXT: (...args) => {
|
|
1186
|
-
const existingError = firstError(args);
|
|
1187
|
-
if (existingError) {
|
|
1188
|
-
return valueToTextResult(existingError, 0);
|
|
1189
|
-
}
|
|
1190
|
-
const [value, formatValue] = args;
|
|
1191
|
-
if (value === undefined) {
|
|
1192
|
-
return error(ErrorCode.Value);
|
|
1193
|
-
}
|
|
1194
|
-
const format = coerceInteger(formatValue, 0);
|
|
1195
|
-
if (isErrorValue(format)) {
|
|
1196
|
-
return format;
|
|
1197
|
-
}
|
|
1198
|
-
return valueToTextResult(value, format);
|
|
1199
|
-
},
|
|
1200
|
-
REGEXTEST: (...args) => {
|
|
1201
|
-
const existingError = firstError(args);
|
|
1202
|
-
if (existingError) {
|
|
1203
|
-
return existingError;
|
|
1204
|
-
}
|
|
1205
|
-
const [textValue, patternValue, caseSensitivityValue] = args;
|
|
1206
|
-
if (textValue === undefined || patternValue === undefined) {
|
|
1207
|
-
return error(ErrorCode.Value);
|
|
1208
|
-
}
|
|
1209
|
-
const caseSensitivity = coerceInteger(caseSensitivityValue, 0);
|
|
1210
|
-
if (isErrorValue(caseSensitivity) || (caseSensitivity !== 0 && caseSensitivity !== 1)) {
|
|
1211
|
-
return error(ErrorCode.Value);
|
|
1212
|
-
}
|
|
1213
|
-
const pattern = coerceText(patternValue);
|
|
1214
|
-
const regex = compileRegex(pattern, caseSensitivity);
|
|
1215
|
-
if (isRegexError(regex)) {
|
|
1216
|
-
return regex;
|
|
1217
|
-
}
|
|
1218
|
-
return booleanResult(regex.test(coerceText(textValue)));
|
|
1219
|
-
},
|
|
1220
|
-
REGEXREPLACE: (...args) => {
|
|
1221
|
-
const existingError = firstError(args);
|
|
1222
|
-
if (existingError) {
|
|
1223
|
-
return existingError;
|
|
1224
|
-
}
|
|
1225
|
-
const [textValue, patternValue, replacementValue, occurrenceValue, caseSensitivityValue] = args;
|
|
1226
|
-
if (textValue === undefined || patternValue === undefined || replacementValue === undefined) {
|
|
1227
|
-
return error(ErrorCode.Value);
|
|
1228
|
-
}
|
|
1229
|
-
const occurrence = coerceInteger(occurrenceValue, 0);
|
|
1230
|
-
const caseSensitivity = coerceInteger(caseSensitivityValue, 0);
|
|
1231
|
-
if (isErrorValue(occurrence) ||
|
|
1232
|
-
isErrorValue(caseSensitivity) ||
|
|
1233
|
-
(caseSensitivity !== 0 && caseSensitivity !== 1)) {
|
|
1234
|
-
return error(ErrorCode.Value);
|
|
1235
|
-
}
|
|
1236
|
-
const text = coerceText(textValue);
|
|
1237
|
-
const replacement = coerceText(replacementValue);
|
|
1238
|
-
const regex = compileRegex(coerceText(patternValue), caseSensitivity, true);
|
|
1239
|
-
if (isRegexError(regex)) {
|
|
1240
|
-
return regex;
|
|
1241
|
-
}
|
|
1242
|
-
if (occurrence === 0) {
|
|
1243
|
-
return stringResult(text.replace(regex, replacement));
|
|
1244
|
-
}
|
|
1245
|
-
const matches = [...text.matchAll(regex)];
|
|
1246
|
-
if (matches.length === 0) {
|
|
1247
|
-
return stringResult(text);
|
|
1248
|
-
}
|
|
1249
|
-
const targetIndex = occurrence > 0 ? occurrence - 1 : matches.length + occurrence;
|
|
1250
|
-
if (targetIndex < 0 || targetIndex >= matches.length) {
|
|
1251
|
-
return stringResult(text);
|
|
1252
|
-
}
|
|
1253
|
-
let currentIndex = -1;
|
|
1254
|
-
return stringResult(text.replace(regex, (match, ...rest) => {
|
|
1255
|
-
currentIndex += 1;
|
|
1256
|
-
if (currentIndex !== targetIndex) {
|
|
1257
|
-
return match;
|
|
1258
|
-
}
|
|
1259
|
-
const captures = rest
|
|
1260
|
-
.slice(0, -2)
|
|
1261
|
-
.map((value) => (typeof value === "string" ? value : undefined));
|
|
1262
|
-
return applyReplacementTemplate(replacement, match, captures);
|
|
1263
|
-
}));
|
|
1264
|
-
},
|
|
1265
|
-
REGEXEXTRACT: (...args) => {
|
|
1266
|
-
const existingError = firstError(args);
|
|
1267
|
-
if (existingError) {
|
|
1268
|
-
return existingError;
|
|
1269
|
-
}
|
|
1270
|
-
const [textValue, patternValue, returnModeValue, caseSensitivityValue] = args;
|
|
1271
|
-
if (textValue === undefined || patternValue === undefined) {
|
|
1272
|
-
return error(ErrorCode.Value);
|
|
1273
|
-
}
|
|
1274
|
-
const returnMode = coerceInteger(returnModeValue, 0);
|
|
1275
|
-
const caseSensitivity = coerceInteger(caseSensitivityValue, 0);
|
|
1276
|
-
if (isErrorValue(returnMode) ||
|
|
1277
|
-
isErrorValue(caseSensitivity) ||
|
|
1278
|
-
![0, 1, 2].includes(returnMode) ||
|
|
1279
|
-
(caseSensitivity !== 0 && caseSensitivity !== 1)) {
|
|
1280
|
-
return error(ErrorCode.Value);
|
|
1281
|
-
}
|
|
1282
|
-
const text = coerceText(textValue);
|
|
1283
|
-
const pattern = coerceText(patternValue);
|
|
1284
|
-
if (returnMode === 1) {
|
|
1285
|
-
const regex = compileRegex(pattern, caseSensitivity, true);
|
|
1286
|
-
if (isRegexError(regex)) {
|
|
1287
|
-
return regex;
|
|
1288
|
-
}
|
|
1289
|
-
const matches = [...text.matchAll(regex)].map((entry) => entry[0]);
|
|
1290
|
-
if (matches.length === 0) {
|
|
1291
|
-
return error(ErrorCode.NA);
|
|
1292
|
-
}
|
|
1293
|
-
return {
|
|
1294
|
-
kind: "array",
|
|
1295
|
-
rows: matches.length,
|
|
1296
|
-
cols: 1,
|
|
1297
|
-
values: matches.map((match) => stringResult(match)),
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
const regex = compileRegex(pattern, caseSensitivity, false);
|
|
1301
|
-
if (isRegexError(regex)) {
|
|
1302
|
-
return regex;
|
|
1303
|
-
}
|
|
1304
|
-
const match = text.match(regex);
|
|
1305
|
-
if (!match) {
|
|
1306
|
-
return error(ErrorCode.NA);
|
|
1307
|
-
}
|
|
1308
|
-
if (returnMode === 0) {
|
|
1309
|
-
return stringResult(match[0]);
|
|
1310
|
-
}
|
|
1311
|
-
const groups = match.slice(1);
|
|
1312
|
-
if (groups.length === 0) {
|
|
1313
|
-
return error(ErrorCode.NA);
|
|
1314
|
-
}
|
|
1315
|
-
return {
|
|
1316
|
-
kind: "array",
|
|
1317
|
-
rows: 1,
|
|
1318
|
-
cols: groups.length,
|
|
1319
|
-
values: groups.map((group) => stringResult(group ?? "")),
|
|
1320
|
-
};
|
|
1321
|
-
},
|
|
1322
|
-
TEXTBEFORE: (...args) => {
|
|
1323
|
-
const existingError = firstError(args);
|
|
1324
|
-
if (existingError) {
|
|
1325
|
-
return existingError;
|
|
1326
|
-
}
|
|
1327
|
-
const [textValue, delimiterValue, instanceValue, matchModeValue, matchEndValue, ifNotFoundValue,] = args;
|
|
1328
|
-
if (textValue === undefined || delimiterValue === undefined) {
|
|
1329
|
-
return error(ErrorCode.Value);
|
|
1330
|
-
}
|
|
1331
|
-
const text = coerceText(textValue);
|
|
1332
|
-
const delimiter = coerceText(delimiterValue);
|
|
1333
|
-
if (delimiter === "") {
|
|
1334
|
-
return error(ErrorCode.Value);
|
|
1335
|
-
}
|
|
1336
|
-
const instanceNumber = instanceValue === undefined ? 1 : coerceNumber(instanceValue);
|
|
1337
|
-
const matchMode = matchModeValue === undefined ? 0 : coerceNumber(matchModeValue);
|
|
1338
|
-
const matchEndNumber = matchEndValue === undefined ? 0 : coerceNumber(matchEndValue);
|
|
1339
|
-
if (instanceNumber === undefined ||
|
|
1340
|
-
matchMode === undefined ||
|
|
1341
|
-
matchEndNumber === undefined ||
|
|
1342
|
-
!Number.isInteger(instanceNumber) ||
|
|
1343
|
-
instanceNumber === 0 ||
|
|
1344
|
-
!Number.isInteger(matchMode) ||
|
|
1345
|
-
(matchMode !== 0 && matchMode !== 1)) {
|
|
1346
|
-
return error(ErrorCode.Value);
|
|
1347
|
-
}
|
|
1348
|
-
const matchEnd = matchEndNumber !== 0;
|
|
1349
|
-
if (instanceNumber > 0) {
|
|
1350
|
-
let searchFrom = 0;
|
|
1351
|
-
let found = -1;
|
|
1352
|
-
for (let count = 0; count < instanceNumber; count += 1) {
|
|
1353
|
-
found = indexOfWithMode(text, delimiter, searchFrom, matchMode);
|
|
1354
|
-
if (found === -1) {
|
|
1355
|
-
return ifNotFoundValue ?? error(ErrorCode.NA);
|
|
1356
|
-
}
|
|
1357
|
-
searchFrom = found + delimiter.length;
|
|
1358
|
-
}
|
|
1359
|
-
return stringResult(text.slice(0, found));
|
|
1360
|
-
}
|
|
1361
|
-
let searchFrom = text.length;
|
|
1362
|
-
let found = matchEnd ? text.length : -1;
|
|
1363
|
-
for (let count = 0; count < Math.abs(instanceNumber); count += 1) {
|
|
1364
|
-
found = lastIndexOfWithMode(text, delimiter, searchFrom, matchMode);
|
|
1365
|
-
if (found === -1) {
|
|
1366
|
-
return ifNotFoundValue ?? error(ErrorCode.NA);
|
|
1367
|
-
}
|
|
1368
|
-
searchFrom = found - 1;
|
|
1369
|
-
}
|
|
1370
|
-
return stringResult(text.slice(0, found));
|
|
1371
|
-
},
|
|
1372
|
-
TEXTAFTER: (...args) => {
|
|
1373
|
-
const existingError = firstError(args);
|
|
1374
|
-
if (existingError) {
|
|
1375
|
-
return existingError;
|
|
1376
|
-
}
|
|
1377
|
-
const [textValue, delimiterValue, instanceValue, matchModeValue, matchEndValue, ifNotFoundValue,] = args;
|
|
1378
|
-
if (textValue === undefined || delimiterValue === undefined) {
|
|
1379
|
-
return error(ErrorCode.Value);
|
|
1380
|
-
}
|
|
1381
|
-
const text = coerceText(textValue);
|
|
1382
|
-
const delimiter = coerceText(delimiterValue);
|
|
1383
|
-
if (delimiter === "") {
|
|
1384
|
-
return error(ErrorCode.Value);
|
|
1385
|
-
}
|
|
1386
|
-
const instanceNumber = instanceValue === undefined ? 1 : coerceNumber(instanceValue);
|
|
1387
|
-
const matchMode = matchModeValue === undefined ? 0 : coerceNumber(matchModeValue);
|
|
1388
|
-
const matchEndNumber = matchEndValue === undefined ? 0 : coerceNumber(matchEndValue);
|
|
1389
|
-
if (instanceNumber === undefined ||
|
|
1390
|
-
matchMode === undefined ||
|
|
1391
|
-
matchEndNumber === undefined ||
|
|
1392
|
-
!Number.isInteger(instanceNumber) ||
|
|
1393
|
-
instanceNumber === 0 ||
|
|
1394
|
-
!Number.isInteger(matchMode) ||
|
|
1395
|
-
(matchMode !== 0 && matchMode !== 1)) {
|
|
1396
|
-
return error(ErrorCode.Value);
|
|
1397
|
-
}
|
|
1398
|
-
const matchEnd = matchEndNumber !== 0;
|
|
1399
|
-
if (instanceNumber > 0) {
|
|
1400
|
-
let searchFrom = 0;
|
|
1401
|
-
let found = -1;
|
|
1402
|
-
for (let count = 0; count < instanceNumber; count += 1) {
|
|
1403
|
-
found = indexOfWithMode(text, delimiter, searchFrom, matchMode);
|
|
1404
|
-
if (found === -1) {
|
|
1405
|
-
return ifNotFoundValue ?? error(ErrorCode.NA);
|
|
1406
|
-
}
|
|
1407
|
-
searchFrom = found + delimiter.length;
|
|
1408
|
-
}
|
|
1409
|
-
return stringResult(text.slice(found + delimiter.length));
|
|
1410
|
-
}
|
|
1411
|
-
let searchFrom = text.length;
|
|
1412
|
-
let found = matchEnd ? text.length : -1;
|
|
1413
|
-
for (let count = 0; count < Math.abs(instanceNumber); count += 1) {
|
|
1414
|
-
found = lastIndexOfWithMode(text, delimiter, searchFrom, matchMode);
|
|
1415
|
-
if (found === -1) {
|
|
1416
|
-
return ifNotFoundValue ?? error(ErrorCode.NA);
|
|
1417
|
-
}
|
|
1418
|
-
searchFrom = found - 1;
|
|
1419
|
-
}
|
|
1420
|
-
const start = found + delimiter.length;
|
|
1421
|
-
return stringResult(text.slice(start));
|
|
1422
|
-
},
|
|
1423
|
-
TEXTJOIN: (...args) => {
|
|
1424
|
-
const existingError = firstError(args);
|
|
1425
|
-
if (existingError) {
|
|
1426
|
-
return existingError;
|
|
1427
|
-
}
|
|
1428
|
-
const [delimiterValue, ignoreEmptyValue, ...values] = args;
|
|
1429
|
-
if (delimiterValue === undefined || ignoreEmptyValue === undefined || values.length === 0) {
|
|
1430
|
-
return error(ErrorCode.Value);
|
|
1431
|
-
}
|
|
1432
|
-
const delimiter = coerceText(delimiterValue);
|
|
1433
|
-
const ignoreEmpty = coerceBoolean(ignoreEmptyValue, false);
|
|
1434
|
-
if (typeof ignoreEmpty !== "boolean") {
|
|
1435
|
-
return ignoreEmpty;
|
|
1436
|
-
}
|
|
1437
|
-
const valuesJoined = [];
|
|
1438
|
-
for (const value of values) {
|
|
1439
|
-
if (value === undefined) {
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
if (value.tag === ValueTag.Empty) {
|
|
1443
|
-
if (!ignoreEmpty) {
|
|
1444
|
-
valuesJoined.push("");
|
|
1445
|
-
}
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
if (value.tag === ValueTag.String && value.value === "" && !ignoreEmpty) {
|
|
1449
|
-
valuesJoined.push("");
|
|
1450
|
-
continue;
|
|
1451
|
-
}
|
|
1452
|
-
if (value.tag === ValueTag.String && value.value === "" && ignoreEmpty) {
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
valuesJoined.push(coerceText(value));
|
|
1456
|
-
}
|
|
1457
|
-
return stringResult(valuesJoined.join(delimiter));
|
|
1458
|
-
},
|
|
1459
522
|
REPLACE: createReplaceBuiltin(),
|
|
1460
523
|
REPLACEB: (...args) => {
|
|
1461
524
|
const existingError = firstError(args);
|