@bilig/formula 0.1.0 → 0.1.3

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.
Files changed (30) hide show
  1. package/dist/builtins/lookup-hypothesis-builtins.d.ts +4 -5
  2. package/dist/builtins/lookup-hypothesis-builtins.js +393 -10
  3. package/dist/builtins/lookup-hypothesis-builtins.js.map +1 -1
  4. package/dist/builtins/lookup.js +3 -387
  5. package/dist/builtins/lookup.js.map +1 -1
  6. package/dist/builtins/placeholder.d.ts +2 -2
  7. package/dist/builtins/text-core-builtins.d.ts +12 -0
  8. package/dist/builtins/text-core-builtins.js +409 -0
  9. package/dist/builtins/text-core-builtins.js.map +1 -0
  10. package/dist/builtins/text-format-builtins.d.ts +14 -0
  11. package/dist/builtins/text-format-builtins.js +537 -0
  12. package/dist/builtins/text-format-builtins.js.map +1 -0
  13. package/dist/builtins/text-search-builtins.d.ts +21 -0
  14. package/dist/builtins/text-search-builtins.js +469 -0
  15. package/dist/builtins/text-search-builtins.js.map +1 -0
  16. package/dist/builtins/text.js +41 -1368
  17. package/dist/builtins/text.js.map +1 -1
  18. package/dist/js-evaluator-array-special-calls.d.ts +28 -0
  19. package/dist/js-evaluator-array-special-calls.js +340 -0
  20. package/dist/js-evaluator-array-special-calls.js.map +1 -0
  21. package/dist/js-evaluator-context-special-calls.d.ts +24 -0
  22. package/dist/js-evaluator-context-special-calls.js +156 -0
  23. package/dist/js-evaluator-context-special-calls.js.map +1 -0
  24. package/dist/js-evaluator-types.d.ts +117 -0
  25. package/dist/js-evaluator-types.js +2 -0
  26. package/dist/js-evaluator-types.js.map +1 -0
  27. package/dist/js-evaluator.d.ts +3 -100
  28. package/dist/js-evaluator.js +42 -492
  29. package/dist/js-evaluator.js.map +1 -1
  30. package/package.json +6 -6
@@ -1,6 +1,8 @@
1
1
  import { ErrorCode, ValueTag } from "@bilig/protocol";
2
- import { excelSerialToDateParts } from "./datetime.js";
3
2
  import { createBlockedBuiltinMap, textPlaceholderBuiltinNames } from "./placeholder.js";
3
+ import { createTextCoreBuiltins } from "./text-core-builtins.js";
4
+ import { createTextFormatBuiltins } from "./text-format-builtins.js";
5
+ import { createTextSearchBuiltins } from "./text-search-builtins.js";
4
6
  function error(code) {
5
7
  return { tag: ValueTag.Error, code };
6
8
  }
@@ -209,572 +211,6 @@ function substituteText(text, oldText, newText, instance) {
209
211
  }
210
212
  return text;
211
213
  }
212
- function regexFlags(caseSensitivity, global = false) {
213
- return `${global ? "g" : ""}${caseSensitivity === 1 ? "i" : ""}`;
214
- }
215
- function compileRegex(pattern, caseSensitivity, global = false) {
216
- try {
217
- return new RegExp(pattern, regexFlags(caseSensitivity, global));
218
- }
219
- catch {
220
- return error(ErrorCode.Value);
221
- }
222
- }
223
- function isRegexError(value) {
224
- return !(value instanceof RegExp);
225
- }
226
- function applyReplacementTemplate(template, match, captures) {
227
- return template.replace(/\$(\$|&|[0-9]{1,2})/g, (_whole, token) => {
228
- if (token === "$") {
229
- return "$";
230
- }
231
- if (token === "&") {
232
- return match;
233
- }
234
- const index = Number(token);
235
- if (!Number.isInteger(index) || index <= 0) {
236
- return "";
237
- }
238
- return captures[index - 1] ?? "";
239
- });
240
- }
241
- function valueToTextResult(value, format) {
242
- if (format !== 0 && format !== 1) {
243
- return error(ErrorCode.Value);
244
- }
245
- switch (value.tag) {
246
- case ValueTag.Empty:
247
- return stringResult("");
248
- case ValueTag.Number:
249
- return stringResult(String(value.value));
250
- case ValueTag.Boolean:
251
- return stringResult(value.value ? "TRUE" : "FALSE");
252
- case ValueTag.String:
253
- return stringResult(format === 1 ? JSON.stringify(value.value) : value.value);
254
- case ValueTag.Error: {
255
- const label = value.code === ErrorCode.Div0
256
- ? "#DIV/0!"
257
- : value.code === ErrorCode.Ref
258
- ? "#REF!"
259
- : value.code === ErrorCode.Value
260
- ? "#VALUE!"
261
- : value.code === ErrorCode.Name
262
- ? "#NAME?"
263
- : value.code === ErrorCode.NA
264
- ? "#N/A"
265
- : value.code === ErrorCode.Cycle
266
- ? "#CYCLE!"
267
- : value.code === ErrorCode.Spill
268
- ? "#SPILL!"
269
- : value.code === ErrorCode.Blocked
270
- ? "#BLOCKED!"
271
- : "#ERROR!";
272
- return stringResult(label);
273
- }
274
- }
275
- }
276
- const shortMonthNames = [
277
- "Jan",
278
- "Feb",
279
- "Mar",
280
- "Apr",
281
- "May",
282
- "Jun",
283
- "Jul",
284
- "Aug",
285
- "Sep",
286
- "Oct",
287
- "Nov",
288
- "Dec",
289
- ];
290
- const fullMonthNames = [
291
- "January",
292
- "February",
293
- "March",
294
- "April",
295
- "May",
296
- "June",
297
- "July",
298
- "August",
299
- "September",
300
- "October",
301
- "November",
302
- "December",
303
- ];
304
- const shortWeekdayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
305
- const fullWeekdayNames = [
306
- "Sunday",
307
- "Monday",
308
- "Tuesday",
309
- "Wednesday",
310
- "Thursday",
311
- "Friday",
312
- "Saturday",
313
- ];
314
- function splitFormatSections(format) {
315
- const sections = [];
316
- let current = "";
317
- let inQuotes = false;
318
- let bracketDepth = 0;
319
- let escaped = false;
320
- for (let index = 0; index < format.length; index += 1) {
321
- const char = format[index];
322
- if (escaped) {
323
- current += char;
324
- escaped = false;
325
- continue;
326
- }
327
- if (char === "\\") {
328
- current += char;
329
- escaped = true;
330
- continue;
331
- }
332
- if (char === '"') {
333
- current += char;
334
- inQuotes = !inQuotes;
335
- continue;
336
- }
337
- if (!inQuotes && char === "[") {
338
- bracketDepth += 1;
339
- current += char;
340
- continue;
341
- }
342
- if (!inQuotes && char === "]" && bracketDepth > 0) {
343
- bracketDepth -= 1;
344
- current += char;
345
- continue;
346
- }
347
- if (!inQuotes && bracketDepth === 0 && char === ";") {
348
- sections.push(current);
349
- current = "";
350
- continue;
351
- }
352
- current += char;
353
- }
354
- sections.push(current);
355
- return sections;
356
- }
357
- function stripFormatDecorations(section) {
358
- let output = "";
359
- let inQuotes = false;
360
- for (let index = 0; index < section.length; index += 1) {
361
- const char = section[index];
362
- if (inQuotes) {
363
- if (char === '"') {
364
- inQuotes = false;
365
- }
366
- else {
367
- output += char;
368
- }
369
- continue;
370
- }
371
- if (char === '"') {
372
- inQuotes = true;
373
- continue;
374
- }
375
- if (char === "\\") {
376
- output += section[index + 1] ?? "";
377
- index += 1;
378
- continue;
379
- }
380
- if (char === "_") {
381
- output += " ";
382
- index += 1;
383
- continue;
384
- }
385
- if (char === "*") {
386
- index += 1;
387
- continue;
388
- }
389
- if (char === "[") {
390
- const end = section.indexOf("]", index + 1);
391
- if (end === -1) {
392
- continue;
393
- }
394
- index = end;
395
- continue;
396
- }
397
- output += char;
398
- }
399
- return output;
400
- }
401
- function formatThousandsText(integerPart) {
402
- return integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
403
- }
404
- function zeroPadText(value, width) {
405
- return String(Math.trunc(Math.abs(value))).padStart(width, "0");
406
- }
407
- function roundToDigits(value, digits) {
408
- if (!Number.isFinite(value)) {
409
- return Number.NaN;
410
- }
411
- const factor = 10 ** Math.max(0, digits);
412
- return Math.round((value + Number.EPSILON) * factor) / factor;
413
- }
414
- function excelSecondOfDay(serial) {
415
- if (!Number.isFinite(serial)) {
416
- return undefined;
417
- }
418
- const whole = Math.floor(serial);
419
- let fraction = serial - whole;
420
- if (fraction < 0) {
421
- fraction += 1;
422
- }
423
- let seconds = Math.floor(fraction * 86_400 + 1e-9);
424
- if (seconds >= 86_400) {
425
- seconds = 0;
426
- }
427
- return seconds;
428
- }
429
- function excelWeekdayIndex(serial) {
430
- if (!Number.isFinite(serial)) {
431
- return undefined;
432
- }
433
- const whole = Math.floor(serial);
434
- if (whole < 0) {
435
- return undefined;
436
- }
437
- const adjustedWhole = whole < 60 ? whole : whole - 1;
438
- return ((adjustedWhole % 7) + 7) % 7;
439
- }
440
- function isDateTimeFormat(section) {
441
- const cleaned = stripFormatDecorations(section).toUpperCase();
442
- return (cleaned.includes("AM/PM") ||
443
- cleaned.includes("A/P") ||
444
- /[YDSH]/.test(cleaned) ||
445
- /(^|[^0#?])M+([^0#?]|$)/.test(cleaned));
446
- }
447
- function isTextFormat(section) {
448
- return stripFormatDecorations(section).includes("@");
449
- }
450
- function chooseFormatSection(value, formatText) {
451
- const sections = splitFormatSections(formatText);
452
- if (value.tag === ValueTag.String) {
453
- return { section: sections[3] ?? sections[0] ?? "", autoNegative: false };
454
- }
455
- const numeric = coerceNumber(value);
456
- if (numeric === undefined) {
457
- return error(ErrorCode.Value);
458
- }
459
- if (numeric < 0) {
460
- if (sections[1] !== undefined) {
461
- return { section: sections[1], numeric: -numeric, autoNegative: false };
462
- }
463
- return { section: sections[0] ?? "", numeric: -numeric, autoNegative: true };
464
- }
465
- if (numeric === 0 && sections[2] !== undefined) {
466
- return { section: sections[2], numeric, autoNegative: false };
467
- }
468
- return { section: sections[0] ?? "", numeric, autoNegative: false };
469
- }
470
- function formatTextSectionValue(value, section) {
471
- const cleaned = stripFormatDecorations(section);
472
- return cleaned.includes("@") ? cleaned.replace(/@/g, value) : cleaned;
473
- }
474
- function tokenizeDateTimeFormat(section) {
475
- const cleaned = stripFormatDecorations(section);
476
- const tokens = [];
477
- let index = 0;
478
- while (index < cleaned.length) {
479
- const remainder = cleaned.slice(index);
480
- const upperRemainder = remainder.toUpperCase();
481
- if (upperRemainder.startsWith("AM/PM")) {
482
- tokens.push({ kind: "ampm", text: cleaned.slice(index, index + 5) });
483
- index += 5;
484
- continue;
485
- }
486
- if (upperRemainder.startsWith("A/P")) {
487
- tokens.push({ kind: "ampm", text: cleaned.slice(index, index + 3) });
488
- index += 3;
489
- continue;
490
- }
491
- const char = cleaned[index];
492
- const lower = char.toLowerCase();
493
- if ("ymdhms".includes(lower)) {
494
- let end = index + 1;
495
- while (end < cleaned.length && cleaned[end].toLowerCase() === lower) {
496
- end += 1;
497
- }
498
- const tokenText = cleaned.slice(index, end);
499
- const baseKind = lower === "y"
500
- ? "year"
501
- : lower === "d"
502
- ? "day"
503
- : lower === "h"
504
- ? "hour"
505
- : lower === "s"
506
- ? "second"
507
- : "month";
508
- tokens.push({ kind: baseKind, text: tokenText });
509
- index = end;
510
- continue;
511
- }
512
- tokens.push({ kind: "literal", text: char });
513
- index += 1;
514
- }
515
- return tokens.map((token, tokenIndex, allTokens) => {
516
- if (token.kind !== "month") {
517
- return token;
518
- }
519
- const previous = allTokens.slice(0, tokenIndex).findLast((entry) => entry.kind !== "literal");
520
- const next = allTokens.slice(tokenIndex + 1).find((entry) => entry.kind !== "literal");
521
- if (previous?.kind === "hour" || previous?.kind === "minute" || next?.kind === "second") {
522
- return { kind: "minute", text: token.text };
523
- }
524
- return token;
525
- });
526
- }
527
- function formatAmPmToken(token, hour) {
528
- const isPm = hour >= 12;
529
- const upper = token.toUpperCase();
530
- if (upper === "A/P") {
531
- const letter = isPm ? "P" : "A";
532
- return token === token.toLowerCase() ? letter.toLowerCase() : letter;
533
- }
534
- if (token === token.toLowerCase()) {
535
- return isPm ? "pm" : "am";
536
- }
537
- return isPm ? "PM" : "AM";
538
- }
539
- function formatDateTimeSectionValue(serial, section) {
540
- const dateParts = excelSerialToDateParts(serial);
541
- const weekdayIndex = excelWeekdayIndex(serial);
542
- const secondOfDay = excelSecondOfDay(serial);
543
- if (!dateParts || weekdayIndex === undefined || secondOfDay === undefined) {
544
- return undefined;
545
- }
546
- const hour24 = Math.floor(secondOfDay / 3600);
547
- const minute = Math.floor((secondOfDay % 3600) / 60);
548
- const second = secondOfDay % 60;
549
- const tokens = tokenizeDateTimeFormat(section);
550
- const hasAmPm = tokens.some((token) => token.kind === "ampm");
551
- return tokens
552
- .map((token) => {
553
- switch (token.kind) {
554
- case "literal":
555
- return token.text;
556
- case "year":
557
- return token.text.length === 2
558
- ? zeroPadText(dateParts.year % 100, 2)
559
- : String(dateParts.year).padStart(Math.max(4, token.text.length), "0");
560
- case "month":
561
- return token.text.length === 1
562
- ? String(dateParts.month)
563
- : token.text.length === 2
564
- ? zeroPadText(dateParts.month, 2)
565
- : token.text.length === 3
566
- ? shortMonthNames[dateParts.month - 1]
567
- : fullMonthNames[dateParts.month - 1];
568
- case "minute":
569
- return token.text.length >= 2 ? zeroPadText(minute, 2) : String(minute);
570
- case "day":
571
- return token.text.length === 1
572
- ? String(dateParts.day)
573
- : token.text.length === 2
574
- ? zeroPadText(dateParts.day, 2)
575
- : token.text.length === 3
576
- ? shortWeekdayNames[weekdayIndex]
577
- : fullWeekdayNames[weekdayIndex];
578
- case "hour": {
579
- const normalizedHour = hasAmPm ? ((hour24 + 11) % 12) + 1 : hour24;
580
- return token.text.length >= 2 ? zeroPadText(normalizedHour, 2) : String(normalizedHour);
581
- }
582
- case "second":
583
- return token.text.length >= 2 ? zeroPadText(second, 2) : String(second);
584
- case "ampm":
585
- return formatAmPmToken(token.text, hour24);
586
- }
587
- })
588
- .join("");
589
- }
590
- function trimOptionalFractionDigits(fraction, minDigits) {
591
- let trimmed = fraction;
592
- while (trimmed.length > minDigits && trimmed.endsWith("0")) {
593
- trimmed = trimmed.slice(0, -1);
594
- }
595
- return trimmed;
596
- }
597
- function formatScientificSection(value, core) {
598
- const exponentIndex = core.search(/[Ee][+-]/);
599
- const mantissaPattern = core.slice(0, exponentIndex);
600
- const exponentPattern = core.slice(exponentIndex + 2);
601
- const mantissaParts = mantissaPattern.split(".");
602
- const fractionPattern = mantissaParts[1] ?? "";
603
- const maxFractionDigits = (fractionPattern.match(/[0#?]/g) ?? []).length;
604
- const minFractionDigits = (fractionPattern.match(/0/g) ?? []).length;
605
- const [mantissaRaw = "0", exponentRaw] = value.toExponential(maxFractionDigits).split("e");
606
- let [integerPart = "0", fractionPart = ""] = mantissaRaw.split(".");
607
- fractionPart = trimOptionalFractionDigits(fractionPart, minFractionDigits);
608
- const exponentValue = Number(exponentRaw ?? 0);
609
- const exponentText = String(Math.abs(exponentValue)).padStart(exponentPattern.length, "0");
610
- return `${integerPart}${fractionPart === "" ? "" : `.${fractionPart}`}E${exponentValue < 0 ? "-" : "+"}${exponentText}`;
611
- }
612
- function formatNumericSectionValue(value, section, autoNegative) {
613
- const cleaned = stripFormatDecorations(section);
614
- if (!/[0#?]/.test(cleaned)) {
615
- return autoNegative && !cleaned.startsWith("-") ? `-${cleaned}` : cleaned;
616
- }
617
- const firstPlaceholder = cleaned.search(/[0#?]/);
618
- let lastPlaceholder = -1;
619
- for (let index = cleaned.length - 1; index >= 0; index -= 1) {
620
- if (/[0#?]/.test(cleaned[index])) {
621
- lastPlaceholder = index;
622
- break;
623
- }
624
- }
625
- const prefix = cleaned.slice(0, firstPlaceholder);
626
- const core = cleaned.slice(firstPlaceholder, lastPlaceholder + 1);
627
- const suffix = cleaned.slice(lastPlaceholder + 1);
628
- const percentCount = (cleaned.match(/%/g) ?? []).length;
629
- const scaledValue = Math.abs(value) * 100 ** percentCount;
630
- let numericText = "";
631
- if (/[Ee][+-]/.test(core)) {
632
- numericText = formatScientificSection(scaledValue, core);
633
- }
634
- else {
635
- const decimalIndex = core.indexOf(".");
636
- const integerPattern = (decimalIndex === -1 ? core : core.slice(0, decimalIndex)).replaceAll(",", "");
637
- const fractionPattern = decimalIndex === -1 ? "" : core.slice(decimalIndex + 1);
638
- const maxFractionDigits = (fractionPattern.match(/[0#?]/g) ?? []).length;
639
- const minFractionDigits = (fractionPattern.match(/0/g) ?? []).length;
640
- const minIntegerDigits = (integerPattern.match(/0/g) ?? []).length;
641
- const roundedValue = roundToDigits(scaledValue, maxFractionDigits);
642
- const fixed = roundedValue.toFixed(maxFractionDigits);
643
- let [integerPart = "0", fractionPart = ""] = fixed.split(".");
644
- if (integerPart.length < minIntegerDigits) {
645
- integerPart = integerPart.padStart(minIntegerDigits, "0");
646
- }
647
- if (core.includes(",")) {
648
- integerPart = formatThousandsText(integerPart);
649
- }
650
- fractionPart = trimOptionalFractionDigits(fractionPart, minFractionDigits);
651
- numericText = `${integerPart}${fractionPart === "" ? "" : `.${fractionPart}`}`;
652
- }
653
- const combined = `${prefix}${numericText}${suffix}`;
654
- return autoNegative && !combined.startsWith("-") ? `-${combined}` : combined;
655
- }
656
- function formatTextBuiltinValue(value, formatText) {
657
- const chosen = chooseFormatSection(value, formatText);
658
- if ("tag" in chosen) {
659
- return chosen;
660
- }
661
- const { section, numeric, autoNegative } = chosen;
662
- if (value.tag === ValueTag.String) {
663
- const cleaned = stripFormatDecorations(section);
664
- if (isTextFormat(section) || !/[0#?YMDHS]/i.test(cleaned)) {
665
- return stringResult(formatTextSectionValue(value.value, section));
666
- }
667
- return error(ErrorCode.Value);
668
- }
669
- if (numeric === undefined) {
670
- return error(ErrorCode.Value);
671
- }
672
- if (isDateTimeFormat(section)) {
673
- const formatted = formatDateTimeSectionValue(numeric, section);
674
- return formatted === undefined ? error(ErrorCode.Value) : stringResult(formatted);
675
- }
676
- return stringResult(formatNumericSectionValue(numeric, section, autoNegative));
677
- }
678
- function parseNumberValueText(input, decimalSeparator, groupSeparator) {
679
- const compact = input.replaceAll(/\s+/g, "");
680
- if (compact === "") {
681
- return 0;
682
- }
683
- const percentMatch = compact.match(/%+$/);
684
- const percentCount = percentMatch?.[0].length ?? 0;
685
- const core = percentCount === 0 ? compact : compact.slice(0, -percentCount);
686
- if (core.includes("%")) {
687
- return undefined;
688
- }
689
- if (decimalSeparator !== "" && groupSeparator !== "" && decimalSeparator === groupSeparator) {
690
- return undefined;
691
- }
692
- const decimal = decimalSeparator === "" ? "." : decimalSeparator[0];
693
- const group = groupSeparator === "" ? "" : groupSeparator[0];
694
- const decimalIndex = decimal === "" ? -1 : core.indexOf(decimal);
695
- if (decimalIndex !== -1 && core.indexOf(decimal, decimalIndex + 1) !== -1) {
696
- return undefined;
697
- }
698
- let normalized = core;
699
- if (group !== "") {
700
- const groupAfterDecimal = decimalIndex === -1 ? -1 : normalized.indexOf(group, decimalIndex + decimal.length);
701
- if (groupAfterDecimal !== -1) {
702
- return undefined;
703
- }
704
- normalized = normalized.replaceAll(group, "");
705
- }
706
- if (decimal !== "." && decimal !== "") {
707
- normalized = normalized.replace(decimal, ".");
708
- }
709
- if (normalized === "" || normalized === "." || normalized === "+" || normalized === "-") {
710
- return undefined;
711
- }
712
- const parsed = Number(normalized);
713
- if (!Number.isFinite(parsed)) {
714
- return undefined;
715
- }
716
- return parsed / 100 ** percentCount;
717
- }
718
- const bahtDigitWords = ["ศูนย์", "หนึ่ง", "สอง", "สาม", "สี่", "ห้า", "หก", "เจ็ด", "แปด", "เก้า"];
719
- const bahtScaleWords = ["", "สิบ", "ร้อย", "พัน", "หมื่น", "แสน"];
720
- const maxBahtTextSatang = Number.MAX_SAFE_INTEGER;
721
- function bahtSegmentText(digits) {
722
- const normalized = digits.replace(/^0+(?=\d)/u, "");
723
- if (normalized === "" || /^0+$/u.test(normalized)) {
724
- return "";
725
- }
726
- let output = "";
727
- let hasHigherNonZero = false;
728
- const length = normalized.length;
729
- for (let index = 0; index < length; index += 1) {
730
- const digit = normalized.charCodeAt(index) - 48;
731
- if (digit === 0) {
732
- continue;
733
- }
734
- const position = length - index - 1;
735
- if (position === 0) {
736
- output += digit === 1 && hasHigherNonZero ? "เอ็ด" : bahtDigitWords[digit];
737
- }
738
- else if (position === 1) {
739
- output += digit === 1 ? "สิบ" : digit === 2 ? "ยี่สิบ" : `${bahtDigitWords[digit]}สิบ`;
740
- }
741
- else {
742
- output += `${bahtDigitWords[digit]}${bahtScaleWords[position]}`;
743
- }
744
- hasHigherNonZero = true;
745
- }
746
- return output;
747
- }
748
- function bahtIntegerText(digits) {
749
- const normalized = digits.replace(/^0+(?=\d)/u, "");
750
- if (normalized === "" || /^0+$/u.test(normalized)) {
751
- return bahtDigitWords[0];
752
- }
753
- if (normalized.length > 6) {
754
- const head = bahtIntegerText(normalized.slice(0, -6));
755
- const tail = bahtSegmentText(normalized.slice(-6));
756
- return `${head}ล้าน${tail}`;
757
- }
758
- return bahtSegmentText(normalized) || bahtDigitWords[0];
759
- }
760
- function bahtTextFromNumber(value) {
761
- if (!Number.isFinite(value)) {
762
- return error(ErrorCode.Value);
763
- }
764
- const absolute = Math.abs(value);
765
- const scaled = Math.round(absolute * 100);
766
- if (!Number.isSafeInteger(scaled) || scaled > maxBahtTextSatang) {
767
- return error(ErrorCode.Value);
768
- }
769
- const baht = Math.trunc(scaled / 100);
770
- const satang = scaled % 100;
771
- const prefix = value < 0 ? "ลบ" : "";
772
- const bahtText = bahtIntegerText(String(baht));
773
- if (satang === 0) {
774
- return stringResult(`${prefix}${bahtText}บาทถ้วน`);
775
- }
776
- return stringResult(`${prefix}${bahtText}บาท${bahtSegmentText(String(satang))}สตางค์`);
777
- }
778
214
  function createReplaceBuiltin() {
779
215
  return (...args) => {
780
216
  const existingError = firstError(args);
@@ -849,283 +285,6 @@ function createReptBuiltin() {
849
285
  return stringResult(repeated);
850
286
  };
851
287
  }
852
- function excelTrim(input) {
853
- let start = 0;
854
- let end = input.length;
855
- while (start < end && input.charCodeAt(start) === 32) {
856
- start += 1;
857
- }
858
- while (end > start && input.charCodeAt(end - 1) === 32) {
859
- end -= 1;
860
- }
861
- return input.slice(start, end).replace(/ {2,}/g, " ");
862
- }
863
- function stripControlCharacters(input) {
864
- let output = "";
865
- for (let index = 0; index < input.length; index += 1) {
866
- const char = input.charCodeAt(index);
867
- if ((char >= 0 && char <= 31) || char === 127) {
868
- continue;
869
- }
870
- output += input[index] ?? "";
871
- }
872
- return output;
873
- }
874
- const halfWidthKanaToFullWidthMap = new Map([
875
- ["。", "。"],
876
- ["「", "「"],
877
- ["」", "」"],
878
- ["、", "、"],
879
- ["・", "・"],
880
- ["ヲ", "ヲ"],
881
- ["ァ", "ァ"],
882
- ["ィ", "ィ"],
883
- ["ゥ", "ゥ"],
884
- ["ェ", "ェ"],
885
- ["ォ", "ォ"],
886
- ["ャ", "ャ"],
887
- ["ュ", "ュ"],
888
- ["ョ", "ョ"],
889
- ["ッ", "ッ"],
890
- ["ー", "ー"],
891
- ["ア", "ア"],
892
- ["イ", "イ"],
893
- ["ウ", "ウ"],
894
- ["エ", "エ"],
895
- ["オ", "オ"],
896
- ["カ", "カ"],
897
- ["キ", "キ"],
898
- ["ク", "ク"],
899
- ["ケ", "ケ"],
900
- ["コ", "コ"],
901
- ["サ", "サ"],
902
- ["シ", "シ"],
903
- ["ス", "ス"],
904
- ["セ", "セ"],
905
- ["ソ", "ソ"],
906
- ["タ", "タ"],
907
- ["チ", "チ"],
908
- ["ツ", "ツ"],
909
- ["テ", "テ"],
910
- ["ト", "ト"],
911
- ["ナ", "ナ"],
912
- ["ニ", "ニ"],
913
- ["ヌ", "ヌ"],
914
- ["ネ", "ネ"],
915
- ["ノ", "ノ"],
916
- ["ハ", "ハ"],
917
- ["ヒ", "ヒ"],
918
- ["フ", "フ"],
919
- ["ヘ", "ヘ"],
920
- ["ホ", "ホ"],
921
- ["マ", "マ"],
922
- ["ミ", "ミ"],
923
- ["ム", "ム"],
924
- ["メ", "メ"],
925
- ["モ", "モ"],
926
- ["ヤ", "ヤ"],
927
- ["ユ", "ユ"],
928
- ["ヨ", "ヨ"],
929
- ["ラ", "ラ"],
930
- ["リ", "リ"],
931
- ["ル", "ル"],
932
- ["レ", "レ"],
933
- ["ロ", "ロ"],
934
- ["ワ", "ワ"],
935
- ["ン", "ン"],
936
- ]);
937
- const halfWidthVoicedKanaToFullWidthMap = new Map([
938
- ["ヴ", "ヴ"],
939
- ["ガ", "ガ"],
940
- ["ギ", "ギ"],
941
- ["グ", "グ"],
942
- ["ゲ", "ゲ"],
943
- ["ゴ", "ゴ"],
944
- ["ザ", "ザ"],
945
- ["ジ", "ジ"],
946
- ["ズ", "ズ"],
947
- ["ゼ", "ゼ"],
948
- ["ゾ", "ゾ"],
949
- ["ダ", "ダ"],
950
- ["ヂ", "ヂ"],
951
- ["ヅ", "ヅ"],
952
- ["デ", "デ"],
953
- ["ド", "ド"],
954
- ["バ", "バ"],
955
- ["ビ", "ビ"],
956
- ["ブ", "ブ"],
957
- ["ベ", "ベ"],
958
- ["ボ", "ボ"],
959
- ["パ", "パ"],
960
- ["ピ", "ピ"],
961
- ["プ", "プ"],
962
- ["ペ", "ペ"],
963
- ["ポ", "ポ"],
964
- ]);
965
- const fullWidthKanaToHalfWidthMap = new Map([
966
- ...[...halfWidthKanaToFullWidthMap.entries()].map(([half, full]) => [full, half]),
967
- ...[...halfWidthVoicedKanaToFullWidthMap.entries()].map(([half, full]) => [full, half]),
968
- ]);
969
- function isLeadingSurrogate(codeUnit) {
970
- return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
971
- }
972
- function isTrailingSurrogate(codeUnit) {
973
- return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
974
- }
975
- function toJapaneseFullWidth(input) {
976
- let output = "";
977
- for (let index = 0; index < input.length; index += 1) {
978
- const char = input[index];
979
- const code = input.charCodeAt(index);
980
- if (isLeadingSurrogate(code) && index + 1 < input.length) {
981
- const nextCode = input.charCodeAt(index + 1);
982
- if (isTrailingSurrogate(nextCode)) {
983
- output += input.slice(index, index + 2);
984
- index += 1;
985
- continue;
986
- }
987
- }
988
- if (code === 0x20) {
989
- output += "\u3000";
990
- continue;
991
- }
992
- if (code >= 0x21 && code <= 0x7e) {
993
- output += String.fromCharCode(code + 0xfee0);
994
- continue;
995
- }
996
- const next = input[index + 1];
997
- if (next !== undefined) {
998
- const voiced = halfWidthVoicedKanaToFullWidthMap.get(char + next);
999
- if (voiced !== undefined) {
1000
- output += voiced;
1001
- index += 1;
1002
- continue;
1003
- }
1004
- }
1005
- const mapped = halfWidthKanaToFullWidthMap.get(char);
1006
- output += mapped ?? char;
1007
- }
1008
- return output;
1009
- }
1010
- function toJapaneseHalfWidth(input) {
1011
- let output = "";
1012
- for (let index = 0; index < input.length; index += 1) {
1013
- const char = input[index];
1014
- const code = input.charCodeAt(index);
1015
- if (isLeadingSurrogate(code) && index + 1 < input.length) {
1016
- const nextCode = input.charCodeAt(index + 1);
1017
- if (isTrailingSurrogate(nextCode)) {
1018
- output += input.slice(index, index + 2);
1019
- index += 1;
1020
- continue;
1021
- }
1022
- }
1023
- if (code === 0x3000) {
1024
- output += " ";
1025
- continue;
1026
- }
1027
- if (code >= 0xff01 && code <= 0xff5e) {
1028
- output += String.fromCharCode(code - 0xfee0);
1029
- continue;
1030
- }
1031
- const mapped = fullWidthKanaToHalfWidthMap.get(char);
1032
- output += mapped ?? char;
1033
- }
1034
- return output;
1035
- }
1036
- function toTitleCase(input) {
1037
- let result = "";
1038
- let capitalizeNext = true;
1039
- for (let index = 0; index < input.length; index += 1) {
1040
- const char = input[index] ?? "";
1041
- const code = char.charCodeAt(0);
1042
- const isAlpha = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
1043
- if (!isAlpha) {
1044
- capitalizeNext = true;
1045
- result += char;
1046
- continue;
1047
- }
1048
- result += capitalizeNext ? char.toUpperCase() : char.toLowerCase();
1049
- capitalizeNext = false;
1050
- }
1051
- return result;
1052
- }
1053
- function escapeRegExp(value) {
1054
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1055
- }
1056
- function indexOfWithMode(text, delimiter, start, matchMode) {
1057
- if (matchMode === 1) {
1058
- return text.toLowerCase().indexOf(delimiter.toLowerCase(), start);
1059
- }
1060
- return text.indexOf(delimiter, start);
1061
- }
1062
- function lastIndexOfWithMode(text, delimiter, start, matchMode) {
1063
- if (matchMode === 1) {
1064
- return text.toLowerCase().lastIndexOf(delimiter.toLowerCase(), start);
1065
- }
1066
- return text.lastIndexOf(delimiter, start);
1067
- }
1068
- function hasSearchSyntax(pattern) {
1069
- for (let index = 0; index < pattern.length; index += 1) {
1070
- const char = pattern[index];
1071
- if (char === "~") {
1072
- return true;
1073
- }
1074
- if (char === "*" || char === "?") {
1075
- return true;
1076
- }
1077
- }
1078
- return false;
1079
- }
1080
- function buildSearchRegex(pattern) {
1081
- let source = "^";
1082
- for (let index = 0; index < pattern.length; index += 1) {
1083
- const char = pattern[index];
1084
- if (char === "~") {
1085
- const next = pattern[index + 1];
1086
- if (next === undefined) {
1087
- source += escapeRegExp(char);
1088
- }
1089
- else {
1090
- source += escapeRegExp(next);
1091
- index += 1;
1092
- }
1093
- continue;
1094
- }
1095
- if (char === "*") {
1096
- source += "[\\s\\S]*";
1097
- continue;
1098
- }
1099
- if (char === "?") {
1100
- source += "[\\s\\S]";
1101
- continue;
1102
- }
1103
- source += escapeRegExp(char);
1104
- }
1105
- return new RegExp(source, "i");
1106
- }
1107
- function findPosition(needle, haystack, start, caseSensitive, wildcardAware) {
1108
- const startIndex = start - 1;
1109
- if (needle === "") {
1110
- return start;
1111
- }
1112
- if (startIndex > haystack.length) {
1113
- return error(ErrorCode.Value);
1114
- }
1115
- if (!wildcardAware || !hasSearchSyntax(needle)) {
1116
- const normalizedHaystack = caseSensitive ? haystack : haystack.toLowerCase();
1117
- const normalizedNeedle = caseSensitive ? needle : needle.toLowerCase();
1118
- const found = normalizedHaystack.indexOf(normalizedNeedle, startIndex);
1119
- return found === -1 ? error(ErrorCode.Value) : found + 1;
1120
- }
1121
- const regex = buildSearchRegex(needle);
1122
- for (let index = startIndex; index <= haystack.length; index += 1) {
1123
- if (regex.test(haystack.slice(index))) {
1124
- return index + 1;
1125
- }
1126
- }
1127
- return error(ErrorCode.Value);
1128
- }
1129
288
  function charCodeFromArgument(value) {
1130
289
  if (value === undefined) {
1131
290
  return error(ErrorCode.Value);
@@ -1141,6 +300,41 @@ function charCodeFromArgument(value) {
1141
300
  return integerCode;
1142
301
  }
1143
302
  const textPlaceholderBuiltins = createBlockedBuiltinMap(textPlaceholderBuiltinNames);
303
+ const textCoreBuiltins = createTextCoreBuiltins({
304
+ error,
305
+ stringResult,
306
+ booleanResult,
307
+ firstError,
308
+ coerceText,
309
+ coerceNumber,
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
+ });
1144
338
  export const textBuiltins = {
1145
339
  LEN: (...args) => {
1146
340
  const existingError = firstError(args);
@@ -1211,123 +405,9 @@ export const textBuiltins = {
1211
405
  }
1212
406
  return stringResult(String.fromCodePoint(integerCode));
1213
407
  },
1214
- CLEAN: (...args) => {
1215
- const existingError = firstError(args);
1216
- if (existingError) {
1217
- return existingError;
1218
- }
1219
- const [textValue] = args;
1220
- if (textValue === undefined) {
1221
- return error(ErrorCode.Value);
1222
- }
1223
- return stringResult(stripControlCharacters(coerceText(textValue)));
1224
- },
1225
- ASC: (...args) => {
1226
- const existingError = firstError(args);
1227
- if (existingError) {
1228
- return existingError;
1229
- }
1230
- const [textValue] = args;
1231
- if (textValue === undefined) {
1232
- return error(ErrorCode.Value);
1233
- }
1234
- return stringResult(toJapaneseHalfWidth(coerceText(textValue)));
1235
- },
1236
- JIS: (...args) => {
1237
- const existingError = firstError(args);
1238
- if (existingError) {
1239
- return existingError;
1240
- }
1241
- const [textValue] = args;
1242
- if (textValue === undefined) {
1243
- return error(ErrorCode.Value);
1244
- }
1245
- return stringResult(toJapaneseFullWidth(coerceText(textValue)));
1246
- },
1247
- DBCS: (...args) => {
1248
- const existingError = firstError(args);
1249
- if (existingError) {
1250
- return existingError;
1251
- }
1252
- const [textValue] = args;
1253
- if (textValue === undefined) {
1254
- return error(ErrorCode.Value);
1255
- }
1256
- return stringResult(toJapaneseFullWidth(coerceText(textValue)));
1257
- },
1258
- BAHTTEXT: (...args) => {
1259
- const existingError = firstError(args);
1260
- if (existingError) {
1261
- return existingError;
1262
- }
1263
- const [value] = args;
1264
- if (value === undefined) {
1265
- return error(ErrorCode.Value);
1266
- }
1267
- const numeric = coerceNumber(value);
1268
- return numeric === undefined ? error(ErrorCode.Value) : bahtTextFromNumber(numeric);
1269
- },
1270
- TEXT: (...args) => {
1271
- const existingError = firstError(args);
1272
- if (existingError) {
1273
- return existingError;
1274
- }
1275
- const [value, formatValue] = args;
1276
- if (value === undefined || formatValue === undefined) {
1277
- return error(ErrorCode.Value);
1278
- }
1279
- return formatTextBuiltinValue(value, coerceText(formatValue));
1280
- },
1281
- PHONETIC: (...args) => {
1282
- const existingError = firstError(args);
1283
- if (existingError) {
1284
- return existingError;
1285
- }
1286
- const [value] = args;
1287
- if (value === undefined) {
1288
- return error(ErrorCode.Value);
1289
- }
1290
- return stringResult(coerceText(value));
1291
- },
1292
- CONCATENATE: (...args) => {
1293
- const existingError = firstError(args);
1294
- if (existingError) {
1295
- return existingError;
1296
- }
1297
- if (args.length === 0) {
1298
- return error(ErrorCode.Value);
1299
- }
1300
- return stringResult(args.map(coerceText).join(""));
1301
- },
1302
- CONCAT: (...args) => {
1303
- const existingError = firstError(args);
1304
- if (existingError) {
1305
- return existingError;
1306
- }
1307
- return stringResult(args.map(coerceText).join(""));
1308
- },
1309
- PROPER: (...args) => {
1310
- const existingError = firstError(args);
1311
- if (existingError) {
1312
- return existingError;
1313
- }
1314
- const [textValue] = args;
1315
- if (textValue === undefined) {
1316
- return error(ErrorCode.Value);
1317
- }
1318
- return stringResult(toTitleCase(coerceText(textValue)));
1319
- },
1320
- EXACT: (...args) => {
1321
- const existingError = firstError(args);
1322
- if (existingError) {
1323
- return existingError;
1324
- }
1325
- const [leftValue, rightValue] = args;
1326
- if (leftValue === undefined || rightValue === undefined) {
1327
- return error(ErrorCode.Value);
1328
- }
1329
- return { tag: ValueTag.Boolean, value: coerceText(leftValue) === coerceText(rightValue) };
1330
- },
408
+ ...textCoreBuiltins,
409
+ ...textFormatBuiltins,
410
+ ...textSearchBuiltins,
1331
411
  LEFT: (...args) => {
1332
412
  const existingError = firstError(args);
1333
413
  if (existingError) {
@@ -1379,91 +459,6 @@ export const textBuiltins = {
1379
459
  const text = coerceText(textValue);
1380
460
  return stringResult(text.slice(start - 1, start - 1 + count));
1381
461
  },
1382
- TRIM: (...args) => {
1383
- const existingError = firstError(args);
1384
- if (existingError) {
1385
- return existingError;
1386
- }
1387
- const [value] = args;
1388
- if (value === undefined) {
1389
- return error(ErrorCode.Value);
1390
- }
1391
- return stringResult(excelTrim(coerceText(value)));
1392
- },
1393
- UPPER: (...args) => {
1394
- const existingError = firstError(args);
1395
- if (existingError) {
1396
- return existingError;
1397
- }
1398
- const [value] = args;
1399
- if (value === undefined) {
1400
- return error(ErrorCode.Value);
1401
- }
1402
- return stringResult(coerceText(value).toUpperCase());
1403
- },
1404
- LOWER: (...args) => {
1405
- const existingError = firstError(args);
1406
- if (existingError) {
1407
- return existingError;
1408
- }
1409
- const [value] = args;
1410
- if (value === undefined) {
1411
- return error(ErrorCode.Value);
1412
- }
1413
- return stringResult(coerceText(value).toLowerCase());
1414
- },
1415
- FIND: (...args) => {
1416
- const existingError = firstError(args);
1417
- if (existingError) {
1418
- return existingError;
1419
- }
1420
- const [findTextValue, withinTextValue, startValue] = args;
1421
- if (findTextValue === undefined || withinTextValue === undefined) {
1422
- return error(ErrorCode.Value);
1423
- }
1424
- const start = coercePositiveStart(startValue, 1);
1425
- if (isErrorValue(start)) {
1426
- return start;
1427
- }
1428
- const found = findPosition(coerceText(findTextValue), coerceText(withinTextValue), start, true, false);
1429
- return isErrorValue(found) ? found : numberResult(found);
1430
- },
1431
- SEARCH: (...args) => {
1432
- const existingError = firstError(args);
1433
- if (existingError) {
1434
- return existingError;
1435
- }
1436
- const [findTextValue, withinTextValue, startValue] = args;
1437
- if (findTextValue === undefined || withinTextValue === undefined) {
1438
- return error(ErrorCode.Value);
1439
- }
1440
- const start = coercePositiveStart(startValue, 1);
1441
- if (isErrorValue(start)) {
1442
- return start;
1443
- }
1444
- const found = findPosition(coerceText(findTextValue), coerceText(withinTextValue), start, false, true);
1445
- return isErrorValue(found) ? found : numberResult(found);
1446
- },
1447
- SEARCHB: (...args) => {
1448
- const existingError = firstError(args);
1449
- if (existingError) {
1450
- return existingError;
1451
- }
1452
- const [findTextValue, withinTextValue, startValue] = args;
1453
- if (findTextValue === undefined || withinTextValue === undefined) {
1454
- return error(ErrorCode.Value);
1455
- }
1456
- const text = coerceText(withinTextValue);
1457
- const start = coercePositiveStart(startValue, 1);
1458
- if (isErrorValue(start)) {
1459
- return start;
1460
- }
1461
- if (start > utf8Bytes(text).length + 1) {
1462
- return error(ErrorCode.Value);
1463
- }
1464
- const found = findPosition(coerceText(findTextValue), text, bytePositionToCharPosition(text, start), false, true);
1465
- return isErrorValue(found) ? found : numberResult(charPositionToBytePosition(text, found));
1466
- },
1467
462
  ENCODEURL: (...args) => {
1468
463
  const existingError = firstError(args);
1469
464
  if (existingError) {
@@ -1475,27 +470,6 @@ export const textBuiltins = {
1475
470
  }
1476
471
  return stringResult(encodeURI(coerceText(value)));
1477
472
  },
1478
- FINDB: (...args) => {
1479
- const existingError = firstError(args);
1480
- if (existingError) {
1481
- return existingError;
1482
- }
1483
- const [findTextValue, withinTextValue, startValue] = args;
1484
- if (findTextValue === undefined || withinTextValue === undefined) {
1485
- return error(ErrorCode.Value);
1486
- }
1487
- const start = coercePositiveStart(startValue, 1);
1488
- if (isErrorValue(start)) {
1489
- return start;
1490
- }
1491
- const findBytes = utf8Bytes(coerceText(findTextValue));
1492
- const withinBytes = utf8Bytes(coerceText(withinTextValue));
1493
- if (start > withinBytes.length + 1) {
1494
- return error(ErrorCode.Value);
1495
- }
1496
- const found = findSubBytes(withinBytes, findBytes, start - 1);
1497
- return found === -1 ? error(ErrorCode.Value) : numberResult(found + 1);
1498
- },
1499
473
  LEFTB: (...args) => {
1500
474
  const existingError = firstError(args);
1501
475
  if (existingError) {
@@ -1545,307 +519,6 @@ export const textBuiltins = {
1545
519
  }
1546
520
  return stringResult(rightBytes(coerceText(textValue), count));
1547
521
  },
1548
- VALUE: (...args) => {
1549
- const existingError = firstError(args);
1550
- if (existingError) {
1551
- return existingError;
1552
- }
1553
- const [value] = args;
1554
- if (value === undefined) {
1555
- return error(ErrorCode.Value);
1556
- }
1557
- const coerced = coerceNumber(value);
1558
- return coerced === undefined ? error(ErrorCode.Value) : numberResult(coerced);
1559
- },
1560
- NUMBERVALUE: (...args) => {
1561
- const existingError = firstError(args);
1562
- if (existingError) {
1563
- return existingError;
1564
- }
1565
- const [textValue, decimalSeparatorValue, groupSeparatorValue] = args;
1566
- if (textValue === undefined) {
1567
- return error(ErrorCode.Value);
1568
- }
1569
- const text = coerceText(textValue);
1570
- const decimalSeparator = decimalSeparatorValue === undefined ? "." : coerceText(decimalSeparatorValue);
1571
- const groupSeparator = groupSeparatorValue === undefined ? "," : coerceText(groupSeparatorValue);
1572
- const parsed = parseNumberValueText(text, decimalSeparator, groupSeparator);
1573
- return parsed === undefined ? error(ErrorCode.Value) : numberResult(parsed);
1574
- },
1575
- VALUETOTEXT: (...args) => {
1576
- const existingError = firstError(args);
1577
- if (existingError) {
1578
- return valueToTextResult(existingError, 0);
1579
- }
1580
- const [value, formatValue] = args;
1581
- if (value === undefined) {
1582
- return error(ErrorCode.Value);
1583
- }
1584
- const format = coerceInteger(formatValue, 0);
1585
- if (isErrorValue(format)) {
1586
- return format;
1587
- }
1588
- return valueToTextResult(value, format);
1589
- },
1590
- REGEXTEST: (...args) => {
1591
- const existingError = firstError(args);
1592
- if (existingError) {
1593
- return existingError;
1594
- }
1595
- const [textValue, patternValue, caseSensitivityValue] = args;
1596
- if (textValue === undefined || patternValue === undefined) {
1597
- return error(ErrorCode.Value);
1598
- }
1599
- const caseSensitivity = coerceInteger(caseSensitivityValue, 0);
1600
- if (isErrorValue(caseSensitivity) || (caseSensitivity !== 0 && caseSensitivity !== 1)) {
1601
- return error(ErrorCode.Value);
1602
- }
1603
- const pattern = coerceText(patternValue);
1604
- const regex = compileRegex(pattern, caseSensitivity);
1605
- if (isRegexError(regex)) {
1606
- return regex;
1607
- }
1608
- return booleanResult(regex.test(coerceText(textValue)));
1609
- },
1610
- REGEXREPLACE: (...args) => {
1611
- const existingError = firstError(args);
1612
- if (existingError) {
1613
- return existingError;
1614
- }
1615
- const [textValue, patternValue, replacementValue, occurrenceValue, caseSensitivityValue] = args;
1616
- if (textValue === undefined || patternValue === undefined || replacementValue === undefined) {
1617
- return error(ErrorCode.Value);
1618
- }
1619
- const occurrence = coerceInteger(occurrenceValue, 0);
1620
- const caseSensitivity = coerceInteger(caseSensitivityValue, 0);
1621
- if (isErrorValue(occurrence) ||
1622
- isErrorValue(caseSensitivity) ||
1623
- (caseSensitivity !== 0 && caseSensitivity !== 1)) {
1624
- return error(ErrorCode.Value);
1625
- }
1626
- const text = coerceText(textValue);
1627
- const replacement = coerceText(replacementValue);
1628
- const regex = compileRegex(coerceText(patternValue), caseSensitivity, true);
1629
- if (isRegexError(regex)) {
1630
- return regex;
1631
- }
1632
- if (occurrence === 0) {
1633
- return stringResult(text.replace(regex, replacement));
1634
- }
1635
- const matches = [...text.matchAll(regex)];
1636
- if (matches.length === 0) {
1637
- return stringResult(text);
1638
- }
1639
- const targetIndex = occurrence > 0 ? occurrence - 1 : matches.length + occurrence;
1640
- if (targetIndex < 0 || targetIndex >= matches.length) {
1641
- return stringResult(text);
1642
- }
1643
- let currentIndex = -1;
1644
- return stringResult(text.replace(regex, (match, ...rest) => {
1645
- currentIndex += 1;
1646
- if (currentIndex !== targetIndex) {
1647
- return match;
1648
- }
1649
- const captures = rest
1650
- .slice(0, -2)
1651
- .map((value) => (typeof value === "string" ? value : undefined));
1652
- return applyReplacementTemplate(replacement, match, captures);
1653
- }));
1654
- },
1655
- REGEXEXTRACT: (...args) => {
1656
- const existingError = firstError(args);
1657
- if (existingError) {
1658
- return existingError;
1659
- }
1660
- const [textValue, patternValue, returnModeValue, caseSensitivityValue] = args;
1661
- if (textValue === undefined || patternValue === undefined) {
1662
- return error(ErrorCode.Value);
1663
- }
1664
- const returnMode = coerceInteger(returnModeValue, 0);
1665
- const caseSensitivity = coerceInteger(caseSensitivityValue, 0);
1666
- if (isErrorValue(returnMode) ||
1667
- isErrorValue(caseSensitivity) ||
1668
- ![0, 1, 2].includes(returnMode) ||
1669
- (caseSensitivity !== 0 && caseSensitivity !== 1)) {
1670
- return error(ErrorCode.Value);
1671
- }
1672
- const text = coerceText(textValue);
1673
- const pattern = coerceText(patternValue);
1674
- if (returnMode === 1) {
1675
- const regex = compileRegex(pattern, caseSensitivity, true);
1676
- if (isRegexError(regex)) {
1677
- return regex;
1678
- }
1679
- const matches = [...text.matchAll(regex)].map((entry) => entry[0]);
1680
- if (matches.length === 0) {
1681
- return error(ErrorCode.NA);
1682
- }
1683
- return {
1684
- kind: "array",
1685
- rows: matches.length,
1686
- cols: 1,
1687
- values: matches.map((match) => stringResult(match)),
1688
- };
1689
- }
1690
- const regex = compileRegex(pattern, caseSensitivity, false);
1691
- if (isRegexError(regex)) {
1692
- return regex;
1693
- }
1694
- const match = text.match(regex);
1695
- if (!match) {
1696
- return error(ErrorCode.NA);
1697
- }
1698
- if (returnMode === 0) {
1699
- return stringResult(match[0]);
1700
- }
1701
- const groups = match.slice(1);
1702
- if (groups.length === 0) {
1703
- return error(ErrorCode.NA);
1704
- }
1705
- return {
1706
- kind: "array",
1707
- rows: 1,
1708
- cols: groups.length,
1709
- values: groups.map((group) => stringResult(group ?? "")),
1710
- };
1711
- },
1712
- TEXTBEFORE: (...args) => {
1713
- const existingError = firstError(args);
1714
- if (existingError) {
1715
- return existingError;
1716
- }
1717
- const [textValue, delimiterValue, instanceValue, matchModeValue, matchEndValue, ifNotFoundValue,] = args;
1718
- if (textValue === undefined || delimiterValue === undefined) {
1719
- return error(ErrorCode.Value);
1720
- }
1721
- const text = coerceText(textValue);
1722
- const delimiter = coerceText(delimiterValue);
1723
- if (delimiter === "") {
1724
- return error(ErrorCode.Value);
1725
- }
1726
- const instanceNumber = instanceValue === undefined ? 1 : coerceNumber(instanceValue);
1727
- const matchMode = matchModeValue === undefined ? 0 : coerceNumber(matchModeValue);
1728
- const matchEndNumber = matchEndValue === undefined ? 0 : coerceNumber(matchEndValue);
1729
- if (instanceNumber === undefined ||
1730
- matchMode === undefined ||
1731
- matchEndNumber === undefined ||
1732
- !Number.isInteger(instanceNumber) ||
1733
- instanceNumber === 0 ||
1734
- !Number.isInteger(matchMode) ||
1735
- (matchMode !== 0 && matchMode !== 1)) {
1736
- return error(ErrorCode.Value);
1737
- }
1738
- const matchEnd = matchEndNumber !== 0;
1739
- if (instanceNumber > 0) {
1740
- let searchFrom = 0;
1741
- let found = -1;
1742
- for (let count = 0; count < instanceNumber; count += 1) {
1743
- found = indexOfWithMode(text, delimiter, searchFrom, matchMode);
1744
- if (found === -1) {
1745
- return ifNotFoundValue ?? error(ErrorCode.NA);
1746
- }
1747
- searchFrom = found + delimiter.length;
1748
- }
1749
- return stringResult(text.slice(0, found));
1750
- }
1751
- let searchFrom = text.length;
1752
- let found = matchEnd ? text.length : -1;
1753
- for (let count = 0; count < Math.abs(instanceNumber); count += 1) {
1754
- found = lastIndexOfWithMode(text, delimiter, searchFrom, matchMode);
1755
- if (found === -1) {
1756
- return ifNotFoundValue ?? error(ErrorCode.NA);
1757
- }
1758
- searchFrom = found - 1;
1759
- }
1760
- return stringResult(text.slice(0, found));
1761
- },
1762
- TEXTAFTER: (...args) => {
1763
- const existingError = firstError(args);
1764
- if (existingError) {
1765
- return existingError;
1766
- }
1767
- const [textValue, delimiterValue, instanceValue, matchModeValue, matchEndValue, ifNotFoundValue,] = args;
1768
- if (textValue === undefined || delimiterValue === undefined) {
1769
- return error(ErrorCode.Value);
1770
- }
1771
- const text = coerceText(textValue);
1772
- const delimiter = coerceText(delimiterValue);
1773
- if (delimiter === "") {
1774
- return error(ErrorCode.Value);
1775
- }
1776
- const instanceNumber = instanceValue === undefined ? 1 : coerceNumber(instanceValue);
1777
- const matchMode = matchModeValue === undefined ? 0 : coerceNumber(matchModeValue);
1778
- const matchEndNumber = matchEndValue === undefined ? 0 : coerceNumber(matchEndValue);
1779
- if (instanceNumber === undefined ||
1780
- matchMode === undefined ||
1781
- matchEndNumber === undefined ||
1782
- !Number.isInteger(instanceNumber) ||
1783
- instanceNumber === 0 ||
1784
- !Number.isInteger(matchMode) ||
1785
- (matchMode !== 0 && matchMode !== 1)) {
1786
- return error(ErrorCode.Value);
1787
- }
1788
- const matchEnd = matchEndNumber !== 0;
1789
- if (instanceNumber > 0) {
1790
- let searchFrom = 0;
1791
- let found = -1;
1792
- for (let count = 0; count < instanceNumber; count += 1) {
1793
- found = indexOfWithMode(text, delimiter, searchFrom, matchMode);
1794
- if (found === -1) {
1795
- return ifNotFoundValue ?? error(ErrorCode.NA);
1796
- }
1797
- searchFrom = found + delimiter.length;
1798
- }
1799
- return stringResult(text.slice(found + delimiter.length));
1800
- }
1801
- let searchFrom = text.length;
1802
- let found = matchEnd ? text.length : -1;
1803
- for (let count = 0; count < Math.abs(instanceNumber); count += 1) {
1804
- found = lastIndexOfWithMode(text, delimiter, searchFrom, matchMode);
1805
- if (found === -1) {
1806
- return ifNotFoundValue ?? error(ErrorCode.NA);
1807
- }
1808
- searchFrom = found - 1;
1809
- }
1810
- const start = found + delimiter.length;
1811
- return stringResult(text.slice(start));
1812
- },
1813
- TEXTJOIN: (...args) => {
1814
- const existingError = firstError(args);
1815
- if (existingError) {
1816
- return existingError;
1817
- }
1818
- const [delimiterValue, ignoreEmptyValue, ...values] = args;
1819
- if (delimiterValue === undefined || ignoreEmptyValue === undefined || values.length === 0) {
1820
- return error(ErrorCode.Value);
1821
- }
1822
- const delimiter = coerceText(delimiterValue);
1823
- const ignoreEmpty = coerceBoolean(ignoreEmptyValue, false);
1824
- if (typeof ignoreEmpty !== "boolean") {
1825
- return ignoreEmpty;
1826
- }
1827
- const valuesJoined = [];
1828
- for (const value of values) {
1829
- if (value === undefined) {
1830
- continue;
1831
- }
1832
- if (value.tag === ValueTag.Empty) {
1833
- if (!ignoreEmpty) {
1834
- valuesJoined.push("");
1835
- }
1836
- continue;
1837
- }
1838
- if (value.tag === ValueTag.String && value.value === "" && !ignoreEmpty) {
1839
- valuesJoined.push("");
1840
- continue;
1841
- }
1842
- if (value.tag === ValueTag.String && value.value === "" && ignoreEmpty) {
1843
- continue;
1844
- }
1845
- valuesJoined.push(coerceText(value));
1846
- }
1847
- return stringResult(valuesJoined.join(delimiter));
1848
- },
1849
522
  REPLACE: createReplaceBuiltin(),
1850
523
  REPLACEB: (...args) => {
1851
524
  const existingError = firstError(args);