@bilig/formula 0.1.0 → 0.1.2

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,6 +1,7 @@
1
1
  import { ErrorCode, ValueTag } from "@bilig/protocol";
2
2
  import { excelSerialToDateParts } from "./datetime.js";
3
3
  import { createBlockedBuiltinMap, textPlaceholderBuiltinNames } from "./placeholder.js";
4
+ import { createTextCoreBuiltins } from "./text-core-builtins.js";
4
5
  function error(code) {
5
6
  return { tag: ValueTag.Error, code };
6
7
  }
@@ -715,66 +716,6 @@ function parseNumberValueText(input, decimalSeparator, groupSeparator) {
715
716
  }
716
717
  return parsed / 100 ** percentCount;
717
718
  }
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
719
  function createReplaceBuiltin() {
779
720
  return (...args) => {
780
721
  const existingError = firstError(args);
@@ -849,207 +790,6 @@ function createReptBuiltin() {
849
790
  return stringResult(repeated);
850
791
  };
851
792
  }
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
793
  function escapeRegExp(value) {
1054
794
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1055
795
  }
@@ -1141,6 +881,14 @@ function charCodeFromArgument(value) {
1141
881
  return integerCode;
1142
882
  }
1143
883
  const textPlaceholderBuiltins = createBlockedBuiltinMap(textPlaceholderBuiltinNames);
884
+ const textCoreBuiltins = createTextCoreBuiltins({
885
+ error,
886
+ stringResult,
887
+ booleanResult,
888
+ firstError,
889
+ coerceText,
890
+ coerceNumber,
891
+ });
1144
892
  export const textBuiltins = {
1145
893
  LEN: (...args) => {
1146
894
  const existingError = firstError(args);
@@ -1211,62 +959,6 @@ export const textBuiltins = {
1211
959
  }
1212
960
  return stringResult(String.fromCodePoint(integerCode));
1213
961
  },
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
962
  TEXT: (...args) => {
1271
963
  const existingError = firstError(args);
1272
964
  if (existingError) {
@@ -1278,56 +970,7 @@ export const textBuiltins = {
1278
970
  }
1279
971
  return formatTextBuiltinValue(value, coerceText(formatValue));
1280
972
  },
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
- },
973
+ ...textCoreBuiltins,
1331
974
  LEFT: (...args) => {
1332
975
  const existingError = firstError(args);
1333
976
  if (existingError) {
@@ -1379,39 +1022,6 @@ export const textBuiltins = {
1379
1022
  const text = coerceText(textValue);
1380
1023
  return stringResult(text.slice(start - 1, start - 1 + count));
1381
1024
  },
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
1025
  FIND: (...args) => {
1416
1026
  const existingError = firstError(args);
1417
1027
  if (existingError) {