@indodev/toolkit 0.4.0 → 0.4.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.
- package/README.md +19 -3
- package/dist/index.cjs +106 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +106 -82
- package/dist/index.js.map +1 -1
- package/dist/nik/index.cjs +51 -33
- package/dist/nik/index.cjs.map +1 -1
- package/dist/nik/index.d.cts +23 -1
- package/dist/nik/index.d.ts +23 -1
- package/dist/nik/index.js +51 -34
- package/dist/nik/index.js.map +1 -1
- package/dist/parse-BDfy3aQw.d.cts +542 -0
- package/dist/parse-BDfy3aQw.d.ts +542 -0
- package/dist/phone/index.cjs +82 -49
- package/dist/phone/index.cjs.map +1 -1
- package/dist/phone/index.d.cts +38 -485
- package/dist/phone/index.d.ts +38 -485
- package/dist/phone/index.js +80 -50
- package/dist/phone/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
TypeScript utilities for Indonesian data. Handles Rupiah formatting, terbilang, NIK validation, phone normalization, and text rules that generic libraries don't cover.
|
|
12
|
+
TypeScript utilities for Indonesian data. Handles Rupiah formatting, terbilang, NIK validation, phone normalization, date formatting, and text rules that generic libraries don't cover.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -64,12 +64,27 @@ toTitleCase('pt bank central asia tbk'); // 'PT Bank Central Asia Tbk'
|
|
|
64
64
|
slugify('Pria & Wanita'); // 'pria-dan-wanita'
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Format dates with Indonesian locale:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import {
|
|
71
|
+
formatDate,
|
|
72
|
+
parseDate,
|
|
73
|
+
toRelativeTime,
|
|
74
|
+
} from '@indodev/toolkit/datetime';
|
|
75
|
+
|
|
76
|
+
formatDate(new Date('2026-01-02'), 'long'); // '2 Januari 2026'
|
|
77
|
+
parseDate('02-01-2026'); // Date(2026, 0, 2)
|
|
78
|
+
toRelativeTime(new Date(Date.now() - 3600000)); // '1 jam yang lalu'
|
|
79
|
+
```
|
|
80
|
+
|
|
67
81
|
## Modules
|
|
68
82
|
|
|
69
83
|
| Module | Description |
|
|
70
84
|
| --------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
71
|
-
| [Currency](https://toolkit.adamm.cloud/docs/
|
|
72
|
-
| [Text](https://toolkit.adamm.cloud/docs/
|
|
85
|
+
| [Currency](https://toolkit.adamm.cloud/docs/utilities/currency) | Format Rupiah, terbilang, split amounts, percentages |
|
|
86
|
+
| [Text](https://toolkit.adamm.cloud/docs/utilities/text) | Title case, slugs, abbreviations, case conversion, masking |
|
|
87
|
+
| [DateTime](https://toolkit.adamm.cloud/docs/utilities/datetime) | Indonesian date formatting, relative time, age calculation |
|
|
73
88
|
| [NIK](https://toolkit.adamm.cloud/docs/identity/nik) | Validate, parse, and mask Indonesian National Identity Numbers |
|
|
74
89
|
| [NPWP](https://toolkit.adamm.cloud/docs/identity/npwp) | Validate and format Tax Identification Numbers |
|
|
75
90
|
| [Phone](https://toolkit.adamm.cloud/docs/contact/phone) | Format, validate, and detect mobile operators |
|
|
@@ -77,6 +92,7 @@ slugify('Pria & Wanita'); // 'pria-dan-wanita'
|
|
|
77
92
|
| [Plate](https://toolkit.adamm.cloud/docs/vehicles/plate) | Validate license plates with region detection |
|
|
78
93
|
| [VIN](https://toolkit.adamm.cloud/docs/vehicles/vin) | Validate Vehicle Identification Numbers (ISO 3779) |
|
|
79
94
|
|
|
95
|
+
|
|
80
96
|
Full docs, examples, and API reference at [toolkit.adamm.cloud](https://toolkit.adamm.cloud/docs)
|
|
81
97
|
|
|
82
98
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -59,53 +59,66 @@ var REGENCIES = {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
// src/nik/
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
const provinceCode = nik.substring(0, 2);
|
|
68
|
-
if (!PROVINCES[provinceCode]) {
|
|
69
|
-
return false;
|
|
62
|
+
// src/nik/utils/date.ts
|
|
63
|
+
function parseNIKDate(nik) {
|
|
64
|
+
if (nik.length !== 16) {
|
|
65
|
+
return null;
|
|
70
66
|
}
|
|
71
67
|
const yearStr = nik.substring(6, 8);
|
|
72
68
|
const monthStr = nik.substring(8, 10);
|
|
73
|
-
const
|
|
69
|
+
const dayEncodedStr = nik.substring(10, 12);
|
|
74
70
|
const year = parseInt(yearStr, 10);
|
|
71
|
+
if (isNaN(year)) return null;
|
|
75
72
|
const fullYear = year > 30 ? 1900 + year : 2e3 + year;
|
|
76
73
|
const month = parseInt(monthStr, 10);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
if (isNaN(month)) return null;
|
|
75
|
+
const dayEncoded = parseInt(dayEncodedStr, 10);
|
|
76
|
+
if (isNaN(dayEncoded)) return null;
|
|
77
|
+
const gender = dayEncoded > 40 ? "female" : "male";
|
|
78
|
+
const day = dayEncoded > 40 ? dayEncoded - 40 : dayEncoded;
|
|
79
|
+
return { year, fullYear, month, day, gender, dayEncoded };
|
|
80
|
+
}
|
|
81
|
+
function validateNIKDateComponents(year, month, day) {
|
|
82
|
+
if (month < 1 || month > 12) return false;
|
|
83
|
+
if (day < 1 || day > 31) return false;
|
|
84
|
+
const testDate = new Date(year, month - 1, day);
|
|
85
|
+
return testDate.getFullYear() === year && testDate.getMonth() === month - 1 && testDate.getDate() === day;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/nik/validate.ts
|
|
89
|
+
var NIK_PATTERN = /^\d{16}$/;
|
|
90
|
+
function validateNIK(nik) {
|
|
91
|
+
if (!NIK_PATTERN.test(nik)) {
|
|
92
|
+
return false;
|
|
80
93
|
}
|
|
81
|
-
|
|
94
|
+
const provinceCode = nik.substring(0, 2);
|
|
95
|
+
if (!PROVINCES[provinceCode]) {
|
|
82
96
|
return false;
|
|
83
97
|
}
|
|
84
|
-
|
|
98
|
+
const parsed = parseNIKDate(nik);
|
|
99
|
+
if (!parsed) {
|
|
85
100
|
return false;
|
|
86
101
|
}
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
102
|
+
const { fullYear, month, day } = parsed;
|
|
103
|
+
if (!validateNIKDateComponents(fullYear, month, day)) {
|
|
89
104
|
return false;
|
|
90
105
|
}
|
|
91
106
|
const now = /* @__PURE__ */ new Date();
|
|
92
|
-
if (
|
|
107
|
+
if (new Date(fullYear, month - 1, day) > now || fullYear < 1900) {
|
|
93
108
|
return false;
|
|
94
109
|
}
|
|
95
110
|
return true;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
// src/nik/parse.ts
|
|
114
|
+
var NIK_PATTERN2 = /^\d{16}$/;
|
|
99
115
|
function parseNIK(nik) {
|
|
100
|
-
if (
|
|
116
|
+
if (!NIK_PATTERN2.test(nik)) {
|
|
101
117
|
return null;
|
|
102
118
|
}
|
|
103
119
|
const provinceCode = nik.substring(0, 2);
|
|
104
120
|
const regencyCode = nik.substring(2, 4);
|
|
105
121
|
const districtCode = nik.substring(4, 6);
|
|
106
|
-
const yearStr = nik.substring(6, 8);
|
|
107
|
-
const monthStr = nik.substring(8, 10);
|
|
108
|
-
const dayStr = nik.substring(10, 12);
|
|
109
122
|
const serialNumber = nik.substring(12, 16);
|
|
110
123
|
const province = PROVINCES[provinceCode];
|
|
111
124
|
if (!province) {
|
|
@@ -113,21 +126,15 @@ function parseNIK(nik) {
|
|
|
113
126
|
}
|
|
114
127
|
const regencies = REGENCIES[provinceCode] || {};
|
|
115
128
|
const regency = regencies[regencyCode] || "Unknown";
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let gender = null;
|
|
120
|
-
if (day > 40) {
|
|
121
|
-
gender = "female";
|
|
122
|
-
day -= 40;
|
|
123
|
-
} else {
|
|
124
|
-
gender = "male";
|
|
129
|
+
const parsed = parseNIKDate(nik);
|
|
130
|
+
if (!parsed) {
|
|
131
|
+
return null;
|
|
125
132
|
}
|
|
126
|
-
const fullYear
|
|
127
|
-
|
|
128
|
-
if (birthDate.getFullYear() !== fullYear || birthDate.getMonth() !== month - 1 || birthDate.getDate() !== day) {
|
|
133
|
+
const { fullYear, month, day, gender } = parsed;
|
|
134
|
+
if (!validateNIKDateComponents(fullYear, month, day)) {
|
|
129
135
|
return null;
|
|
130
136
|
}
|
|
137
|
+
const birthDate = new Date(fullYear, month - 1, day);
|
|
131
138
|
return {
|
|
132
139
|
province: {
|
|
133
140
|
code: provinceCode,
|
|
@@ -717,7 +724,7 @@ function isMobileNumber(phone) {
|
|
|
717
724
|
let normalized;
|
|
718
725
|
if (cleaned.startsWith("+62")) {
|
|
719
726
|
normalized = "0" + cleaned.substring(3);
|
|
720
|
-
} else if (cleaned.startsWith("62")) {
|
|
727
|
+
} else if (cleaned.startsWith("62") && !cleaned.startsWith("620")) {
|
|
721
728
|
normalized = "0" + cleaned.substring(2);
|
|
722
729
|
} else {
|
|
723
730
|
normalized = cleaned;
|
|
@@ -731,19 +738,70 @@ function isLandlineNumber(phone) {
|
|
|
731
738
|
return !isMobileNumber(phone);
|
|
732
739
|
}
|
|
733
740
|
|
|
741
|
+
// src/phone/utils.ts
|
|
742
|
+
function normalizePhoneNumber(phone) {
|
|
743
|
+
if (!phone || typeof phone !== "string") {
|
|
744
|
+
return "";
|
|
745
|
+
}
|
|
746
|
+
if (phone.startsWith("+62")) {
|
|
747
|
+
return "0" + phone.substring(3);
|
|
748
|
+
}
|
|
749
|
+
if (phone.startsWith("62") && !phone.startsWith("620")) {
|
|
750
|
+
return "0" + phone.substring(2);
|
|
751
|
+
}
|
|
752
|
+
if (phone.startsWith("0")) {
|
|
753
|
+
return phone;
|
|
754
|
+
}
|
|
755
|
+
return "";
|
|
756
|
+
}
|
|
757
|
+
function normalizeToNational(phone) {
|
|
758
|
+
if (phone.startsWith("+62")) {
|
|
759
|
+
return "0" + phone.substring(3);
|
|
760
|
+
}
|
|
761
|
+
if (phone.startsWith("62") && !phone.startsWith("620")) {
|
|
762
|
+
return "0" + phone.substring(2);
|
|
763
|
+
}
|
|
764
|
+
if (phone.startsWith("0")) {
|
|
765
|
+
return phone;
|
|
766
|
+
}
|
|
767
|
+
return "";
|
|
768
|
+
}
|
|
769
|
+
function getLandlineRegion(phone) {
|
|
770
|
+
if (!phone || typeof phone !== "string") {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
const cleaned = phone.replace(/[^\d+]/g, "");
|
|
774
|
+
const normalized = normalizeToNational(cleaned);
|
|
775
|
+
if (!normalized || !normalized.startsWith("0")) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
if (normalized.startsWith("08")) {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
const areaCode4 = normalized.substring(0, 4);
|
|
782
|
+
if (AREA_CODES[areaCode4]) {
|
|
783
|
+
return AREA_CODES[areaCode4];
|
|
784
|
+
}
|
|
785
|
+
const areaCode3 = normalized.substring(0, 3);
|
|
786
|
+
if (AREA_CODES[areaCode3]) {
|
|
787
|
+
return AREA_CODES[areaCode3];
|
|
788
|
+
}
|
|
789
|
+
const areaCode2 = normalized.substring(0, 2);
|
|
790
|
+
if (AREA_CODES[areaCode2]) {
|
|
791
|
+
return AREA_CODES[areaCode2];
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
734
796
|
// src/phone/format.ts
|
|
735
797
|
function formatPhoneNumber(phone, format = "national") {
|
|
736
798
|
if (!validatePhoneNumber(phone)) {
|
|
737
799
|
return phone;
|
|
738
800
|
}
|
|
739
801
|
const cleaned = cleanPhoneNumber(phone);
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
} else if (cleaned.startsWith("62") && !cleaned.startsWith("620")) {
|
|
744
|
-
normalized = "0" + cleaned.substring(2);
|
|
745
|
-
} else {
|
|
746
|
-
normalized = cleaned;
|
|
802
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
803
|
+
if (!normalized) {
|
|
804
|
+
return phone;
|
|
747
805
|
}
|
|
748
806
|
switch (format) {
|
|
749
807
|
case "international":
|
|
@@ -762,7 +820,7 @@ function toInternational(phone) {
|
|
|
762
820
|
if (!cleaned) {
|
|
763
821
|
return phone;
|
|
764
822
|
}
|
|
765
|
-
const normalized =
|
|
823
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
766
824
|
if (!normalized) {
|
|
767
825
|
return phone;
|
|
768
826
|
}
|
|
@@ -788,7 +846,7 @@ function toNational(phone) {
|
|
|
788
846
|
if (!cleaned) {
|
|
789
847
|
return phone;
|
|
790
848
|
}
|
|
791
|
-
const normalized =
|
|
849
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
792
850
|
if (!normalized) {
|
|
793
851
|
return phone;
|
|
794
852
|
}
|
|
@@ -813,7 +871,7 @@ function toE164(phone) {
|
|
|
813
871
|
if (!cleaned) {
|
|
814
872
|
return phone;
|
|
815
873
|
}
|
|
816
|
-
const normalized =
|
|
874
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
817
875
|
if (!normalized) {
|
|
818
876
|
return phone;
|
|
819
877
|
}
|
|
@@ -825,16 +883,6 @@ function cleanPhoneNumber(phone) {
|
|
|
825
883
|
}
|
|
826
884
|
return phone.replace(/[^\d+]/g, "");
|
|
827
885
|
}
|
|
828
|
-
function normalizeToNational(phone) {
|
|
829
|
-
if (phone.startsWith("+62")) {
|
|
830
|
-
return "0" + phone.substring(3);
|
|
831
|
-
} else if (phone.startsWith("62") && !phone.startsWith("620")) {
|
|
832
|
-
return "0" + phone.substring(2);
|
|
833
|
-
} else if (phone.startsWith("0")) {
|
|
834
|
-
return phone;
|
|
835
|
-
}
|
|
836
|
-
return "";
|
|
837
|
-
}
|
|
838
886
|
function getAreaCodeLength(normalized) {
|
|
839
887
|
const fourDigitCode = normalized.substring(0, 5);
|
|
840
888
|
if (AREA_CODES[fourDigitCode]) {
|
|
@@ -860,7 +908,7 @@ function maskPhoneNumber(phone, options = {}) {
|
|
|
860
908
|
if (isInternational) {
|
|
861
909
|
toMask = cleaned;
|
|
862
910
|
} else {
|
|
863
|
-
const normalized =
|
|
911
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
864
912
|
toMask = normalized || cleaned;
|
|
865
913
|
}
|
|
866
914
|
if (toMask.length < 4) {
|
|
@@ -929,7 +977,7 @@ function parsePhoneNumber(phone) {
|
|
|
929
977
|
return null;
|
|
930
978
|
}
|
|
931
979
|
const cleaned = cleanPhoneNumber(phone);
|
|
932
|
-
const normalized =
|
|
980
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
933
981
|
if (!normalized) {
|
|
934
982
|
return null;
|
|
935
983
|
}
|
|
@@ -942,7 +990,7 @@ function parsePhoneNumber(phone) {
|
|
|
942
990
|
if (isMobile) {
|
|
943
991
|
operator = getOperator(normalized);
|
|
944
992
|
} else {
|
|
945
|
-
region =
|
|
993
|
+
region = getLandlineRegion(normalized);
|
|
946
994
|
}
|
|
947
995
|
return {
|
|
948
996
|
countryCode,
|
|
@@ -964,7 +1012,7 @@ function getOperator(phone) {
|
|
|
964
1012
|
return null;
|
|
965
1013
|
}
|
|
966
1014
|
const cleaned = cleanPhoneNumber(phone);
|
|
967
|
-
const normalized =
|
|
1015
|
+
const normalized = normalizePhoneNumber(cleaned);
|
|
968
1016
|
if (!normalized || normalized.length < 4) {
|
|
969
1017
|
return null;
|
|
970
1018
|
}
|
|
@@ -978,30 +1026,6 @@ function isProvider(phone, providerName) {
|
|
|
978
1026
|
}
|
|
979
1027
|
return operator.toLowerCase() === providerName.toLowerCase();
|
|
980
1028
|
}
|
|
981
|
-
function getRegion(phone) {
|
|
982
|
-
if (!phone.startsWith("0")) {
|
|
983
|
-
return null;
|
|
984
|
-
}
|
|
985
|
-
const areaCode4 = phone.substring(0, 4);
|
|
986
|
-
if (AREA_CODES[areaCode4]) {
|
|
987
|
-
return AREA_CODES[areaCode4];
|
|
988
|
-
}
|
|
989
|
-
const areaCode3 = phone.substring(0, 3);
|
|
990
|
-
if (AREA_CODES[areaCode3]) {
|
|
991
|
-
return AREA_CODES[areaCode3];
|
|
992
|
-
}
|
|
993
|
-
return null;
|
|
994
|
-
}
|
|
995
|
-
function normalizeToNational2(phone) {
|
|
996
|
-
if (phone.startsWith("+62")) {
|
|
997
|
-
return "0" + phone.substring(3);
|
|
998
|
-
} else if (phone.startsWith("62")) {
|
|
999
|
-
return "0" + phone.substring(2);
|
|
1000
|
-
} else if (phone.startsWith("0")) {
|
|
1001
|
-
return phone;
|
|
1002
|
-
}
|
|
1003
|
-
return "";
|
|
1004
|
-
}
|
|
1005
1029
|
|
|
1006
1030
|
// src/npwp/validate.ts
|
|
1007
1031
|
function validateNPWP(npwp) {
|