@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.
@@ -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);