@indodev/toolkit 0.3.4 → 0.4.1
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 +56 -164
- package/dist/datetime/index.cjs +442 -0
- package/dist/datetime/index.cjs.map +1 -0
- package/dist/datetime/index.d.cts +412 -0
- package/dist/datetime/index.d.ts +412 -0
- package/dist/datetime/index.js +422 -0
- package/dist/datetime/index.js.map +1 -0
- package/dist/index.cjs +504 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +486 -50
- package/dist/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 +36 -19
- package/LICENCE +0 -21
package/README.md
CHANGED
|
@@ -2,26 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
# @indodev/toolkit
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
[](https://github.com/choiruladamm/indo-dev-utils/actions)
|
|
6
|
+
[](https://npmjs.com/package/@indodev/toolkit)
|
|
7
|
+
[](https://typescriptlang.org/)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
9
|
|
|
9
10
|
</div>
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Building apps for Indonesia means dealing with NIK validation, phone number formatting, Rupiah display, and proper text handling. Instead of rewriting the same logic across projects, use battle-tested utilities that just work.
|
|
14
|
-
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- **NIK validation** - Verify Indonesian National Identity Numbers with province, date, and gender checks
|
|
18
|
-
- **Phone formatting** - Support for all major operators (Telkomsel, XL, Indosat, Smartfren, Axis) and 200+ area codes
|
|
19
|
-
- **Rupiah formatting** - Display currency with proper grammar rules (1,5 juta, not 1,0 juta)
|
|
20
|
-
- **Text utilities** - Smart capitalization, slug generation, abbreviation expansion, and string comparison with Indonesian language support
|
|
21
|
-
- **Terbilang converter** - Numbers to Indonesian words (1500000 → "satu juta lima ratus ribu rupiah")
|
|
22
|
-
- **Type-safe** - Full TypeScript support with proper type inference
|
|
23
|
-
- **Well-tested** - 1060+ test cases with 95%+ coverage
|
|
24
|
-
- **Zero dependencies** - Lightweight and tree-shakeable
|
|
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.
|
|
25
13
|
|
|
26
14
|
## Install
|
|
27
15
|
|
|
@@ -29,178 +17,82 @@ Building apps for Indonesia means dealing with NIK validation, phone number form
|
|
|
29
17
|
npm install @indodev/toolkit
|
|
30
18
|
```
|
|
31
19
|
|
|
32
|
-
##
|
|
20
|
+
## Usage
|
|
33
21
|
|
|
34
|
-
|
|
22
|
+
Generate an invoice with proper Rupiah formatting and terbilang:
|
|
35
23
|
|
|
36
24
|
```typescript
|
|
37
|
-
import {
|
|
25
|
+
import { formatRupiah, toWords, calculateTax } from '@indodev/toolkit/currency';
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
const items = [
|
|
28
|
+
{ name: 'Jasa Desain Website', qty: 1, price: 5000000 },
|
|
29
|
+
{ name: 'Hosting 1 Tahun', qty: 1, price: 1200000 },
|
|
30
|
+
];
|
|
41
31
|
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
console.log(info.gender); // 'male' or 'female'
|
|
32
|
+
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
|
|
33
|
+
const tax = calculateTax(subtotal, 0.11);
|
|
34
|
+
const total = subtotal + tax;
|
|
46
35
|
|
|
47
|
-
//
|
|
48
|
-
|
|
36
|
+
console.log(formatRupiah(subtotal)); // 'Rp 6.200.000'
|
|
37
|
+
console.log(formatRupiah(tax)); // 'Rp 682.000'
|
|
38
|
+
console.log(formatRupiah(total)); // 'Rp 6.882.000'
|
|
39
|
+
console.log(toWords(total)); // 'enam juta delapan ratus delapan puluh dua ribu rupiah'
|
|
49
40
|
```
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
Validate and parse an Indonesian NIK:
|
|
52
43
|
|
|
53
44
|
```typescript
|
|
54
|
-
import {
|
|
55
|
-
validatePhoneNumber,
|
|
56
|
-
formatPhoneNumber,
|
|
57
|
-
getOperator,
|
|
58
|
-
} from '@indodev/toolkit/phone';
|
|
59
|
-
|
|
60
|
-
// Validate and format
|
|
61
|
-
validatePhoneNumber('081234567890'); // true
|
|
62
|
-
formatPhoneNumber('081234567890', 'international'); // '+62 812-3456-7890'
|
|
63
|
-
|
|
64
|
-
// Detect operator
|
|
65
|
-
getOperator('081234567890'); // 'Telkomsel'
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Currency Formatting
|
|
45
|
+
import { validateNIK, parseNIK } from '@indodev/toolkit/nik';
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
import {
|
|
72
|
-
formatRupiah,
|
|
73
|
-
formatCompact,
|
|
74
|
-
toWords,
|
|
75
|
-
} from '@indodev/toolkit/currency';
|
|
76
|
-
|
|
77
|
-
// Standard format
|
|
78
|
-
formatRupiah(1500000); // 'Rp 1.500.000'
|
|
79
|
-
|
|
80
|
-
// Compact format (follows Indonesian grammar!)
|
|
81
|
-
formatCompact(1500000); // 'Rp 1,5 juta'
|
|
82
|
-
formatCompact(1000000); // 'Rp 1 juta' (not '1,0 juta')
|
|
47
|
+
validateNIK('3201234567890123'); // true
|
|
83
48
|
|
|
84
|
-
|
|
85
|
-
|
|
49
|
+
const info = parseNIK('3201234567890123');
|
|
50
|
+
// info.province.name → 'Jawa Barat'
|
|
51
|
+
// info.gender → 'male'
|
|
52
|
+
// info.birthDate → Date(1990-01-01)
|
|
86
53
|
```
|
|
87
54
|
|
|
88
|
-
|
|
55
|
+
Format phone numbers and mask sensitive data:
|
|
89
56
|
|
|
90
57
|
```typescript
|
|
91
|
-
import {
|
|
92
|
-
|
|
93
|
-
slugify,
|
|
94
|
-
expandAbbreviation,
|
|
95
|
-
truncate,
|
|
96
|
-
} from '@indodev/toolkit/text';
|
|
97
|
-
|
|
98
|
-
// Smart title case (respects Indonesian particles)
|
|
99
|
-
toTitleCase('buku panduan belajar di rumah');
|
|
100
|
-
// 'Buku Panduan Belajar di Rumah'
|
|
58
|
+
import { formatPhoneNumber } from '@indodev/toolkit/phone';
|
|
59
|
+
import { maskText, toTitleCase, slugify } from '@indodev/toolkit/text';
|
|
101
60
|
|
|
102
|
-
//
|
|
61
|
+
formatPhoneNumber('081234567890', 'international'); // '+62 812-3456-7890'
|
|
62
|
+
maskText('08123456789', { pattern: 'middle', visibleStart: 4, visibleEnd: 3 }); // '0812****789'
|
|
63
|
+
toTitleCase('pt bank central asia tbk'); // 'PT Bank Central Asia Tbk'
|
|
103
64
|
slugify('Pria & Wanita'); // 'pria-dan-wanita'
|
|
104
|
-
slugify('Hitam/Putih'); // 'hitam-atau-putih'
|
|
105
|
-
|
|
106
|
-
// Expand abbreviations
|
|
107
|
-
expandAbbreviation('Jl. Sudirman No. 45');
|
|
108
|
-
// 'Jalan Sudirman Nomor 45'
|
|
109
|
-
|
|
110
|
-
// Smart truncation
|
|
111
|
-
truncate('Ini adalah text yang sangat panjang', 20);
|
|
112
|
-
// 'Ini adalah text...'
|
|
113
65
|
```
|
|
114
66
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
### NIK Module
|
|
118
|
-
|
|
119
|
-
| Function | Description |
|
|
120
|
-
| ---------------------------- | ------------------------------------ |
|
|
121
|
-
| `validateNIK(nik)` | Check if NIK is valid |
|
|
122
|
-
| `parseNIK(nik)` | Extract province, birth date, gender |
|
|
123
|
-
| `formatNIK(nik, separator?)` | Format with separators |
|
|
124
|
-
| `maskNIK(nik, options?)` | Mask for privacy |
|
|
125
|
-
|
|
126
|
-
### Phone Module
|
|
127
|
-
|
|
128
|
-
| Function | Description |
|
|
129
|
-
| ---------------------------------- | ------------------------------------- |
|
|
130
|
-
| `validatePhoneNumber(phone)` | Validate Indonesian phone numbers |
|
|
131
|
-
| `formatPhoneNumber(phone, format)` | Format to international/national/e164 |
|
|
132
|
-
| `getOperator(phone)` | Detect operator (Telkomsel, XL, etc) |
|
|
133
|
-
| `parsePhoneNumber(phone)` | Get all phone info |
|
|
134
|
-
|
|
135
|
-
### Currency Module
|
|
136
|
-
|
|
137
|
-
| Function | Description |
|
|
138
|
-
| -------------------------------- | -------------------------------- |
|
|
139
|
-
| `formatRupiah(amount, options?)` | Standard Rupiah format |
|
|
140
|
-
| `formatCompact(amount)` | Compact format (1,5 juta) |
|
|
141
|
-
| `parseRupiah(formatted)` | Parse formatted string to number |
|
|
142
|
-
| `toWords(amount, options?)` | Convert to Indonesian words |
|
|
143
|
-
|
|
144
|
-
### Text Module
|
|
145
|
-
|
|
146
|
-
| Function | Description |
|
|
147
|
-
| -------------------------------------- | ----------------------------------------------- |
|
|
148
|
-
| `toTitleCase(text, options?)` | Smart capitalization with Indonesian rules |
|
|
149
|
-
| `slugify(text, options?)` | URL-friendly slugs with Indonesian conjunctions |
|
|
150
|
-
| `expandAbbreviation(text, options?)` | Expand Indonesian abbreviations (Jl., Bpk.) |
|
|
151
|
-
| `truncate(text, maxLength, options?)` | Smart text truncation at word boundaries |
|
|
152
|
-
| `compareStrings(str1, str2, options?)` | Robust string comparison |
|
|
153
|
-
| `sanitize(text, options?)` | Clean and normalize text |
|
|
154
|
-
|
|
155
|
-
## TypeScript Support
|
|
156
|
-
|
|
157
|
-
Full type inference out of the box:
|
|
67
|
+
Format dates with Indonesian locale:
|
|
158
68
|
|
|
159
69
|
```typescript
|
|
160
|
-
import
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
// ✅ Recommended: Import from submodules
|
|
172
|
-
import { formatRupiah } from '@indodev/toolkit/currency';
|
|
173
|
-
import { validateNIK } from '@indodev/toolkit/nik';
|
|
174
|
-
import { slugify } from '@indodev/toolkit/text';
|
|
175
|
-
|
|
176
|
-
// ⚠️ Works but imports everything
|
|
177
|
-
import { formatRupiah, validateNIK, slugify } from '@indodev/toolkit';
|
|
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'
|
|
178
79
|
```
|
|
179
80
|
|
|
180
|
-
##
|
|
181
|
-
|
|
182
|
-
| Module | Size (minified + gzipped) |
|
|
183
|
-
| --------- | ------------------------- |
|
|
184
|
-
| NIK | ~5 KB |
|
|
185
|
-
| Phone | ~12 KB |
|
|
186
|
-
| Currency | ~6 KB |
|
|
187
|
-
| Text | ~8 KB |
|
|
188
|
-
| **Total** | **~31 KB** |
|
|
189
|
-
|
|
190
|
-
## Requirements
|
|
191
|
-
|
|
192
|
-
- Node.js >= 18
|
|
193
|
-
- TypeScript >= 5.0 (optional)
|
|
194
|
-
|
|
195
|
-
## Documentation
|
|
196
|
-
|
|
197
|
-
- 📖 [Full Documentation](https://toolkit.adamm.cloud/docs)
|
|
198
|
-
- 🐛 [Report Issues](https://github.com/choiruladamm/indo-dev-utils/issues)
|
|
81
|
+
## Modules
|
|
199
82
|
|
|
200
|
-
|
|
83
|
+
| Module | Description |
|
|
84
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
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 |
|
|
88
|
+
| [NIK](https://toolkit.adamm.cloud/docs/identity/nik) | Validate, parse, and mask Indonesian National Identity Numbers |
|
|
89
|
+
| [NPWP](https://toolkit.adamm.cloud/docs/identity/npwp) | Validate and format Tax Identification Numbers |
|
|
90
|
+
| [Phone](https://toolkit.adamm.cloud/docs/contact/phone) | Format, validate, and detect mobile operators |
|
|
91
|
+
| [Email](https://toolkit.adamm.cloud/docs/contact/email) | Validate emails with disposable domain detection |
|
|
92
|
+
| [Plate](https://toolkit.adamm.cloud/docs/vehicles/plate) | Validate license plates with region detection |
|
|
93
|
+
| [VIN](https://toolkit.adamm.cloud/docs/vehicles/vin) | Validate Vehicle Identification Numbers (ISO 3779) |
|
|
201
94
|
|
|
202
|
-
MIT © [choiruladamm](https://github.com/choiruladamm)
|
|
203
95
|
|
|
204
|
-
|
|
96
|
+
Full docs, examples, and API reference at [toolkit.adamm.cloud](https://toolkit.adamm.cloud/docs)
|
|
205
97
|
|
|
206
|
-
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/datetime/types.ts
|
|
4
|
+
var InvalidDateError = class extends Error {
|
|
5
|
+
constructor(message = "Invalid date provided") {
|
|
6
|
+
super(message);
|
|
7
|
+
/** Error code for programmatic identification */
|
|
8
|
+
this.code = "INVALID_DATE";
|
|
9
|
+
this.name = "InvalidDateError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var InvalidDateRangeError = class extends Error {
|
|
13
|
+
constructor(message = "End date must be after start date") {
|
|
14
|
+
super(message);
|
|
15
|
+
/** Error code for programmatic identification */
|
|
16
|
+
this.code = "INVALID_DATE_RANGE";
|
|
17
|
+
this.name = "InvalidDateRangeError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/datetime/constants.ts
|
|
22
|
+
var MONTH_NAMES = [
|
|
23
|
+
"",
|
|
24
|
+
// Placeholder for 0-index
|
|
25
|
+
"Januari",
|
|
26
|
+
"Februari",
|
|
27
|
+
"Maret",
|
|
28
|
+
"April",
|
|
29
|
+
"Mei",
|
|
30
|
+
"Juni",
|
|
31
|
+
"Juli",
|
|
32
|
+
"Agustus",
|
|
33
|
+
"September",
|
|
34
|
+
"Oktober",
|
|
35
|
+
"November",
|
|
36
|
+
"Desember"
|
|
37
|
+
];
|
|
38
|
+
var MONTH_NAMES_SHORT = [
|
|
39
|
+
"",
|
|
40
|
+
// Placeholder for 0-index
|
|
41
|
+
"Jan",
|
|
42
|
+
"Feb",
|
|
43
|
+
"Mar",
|
|
44
|
+
"Apr",
|
|
45
|
+
"Mei",
|
|
46
|
+
"Jun",
|
|
47
|
+
"Jul",
|
|
48
|
+
"Agu",
|
|
49
|
+
"Sep",
|
|
50
|
+
"Okt",
|
|
51
|
+
"Nov",
|
|
52
|
+
"Des"
|
|
53
|
+
];
|
|
54
|
+
var DAY_NAMES = [
|
|
55
|
+
"Minggu",
|
|
56
|
+
"Senin",
|
|
57
|
+
"Selasa",
|
|
58
|
+
"Rabu",
|
|
59
|
+
"Kamis",
|
|
60
|
+
"Jumat",
|
|
61
|
+
"Sabtu"
|
|
62
|
+
];
|
|
63
|
+
var DAY_NAMES_SHORT = [
|
|
64
|
+
"Min",
|
|
65
|
+
"Sen",
|
|
66
|
+
"Sel",
|
|
67
|
+
"Rab",
|
|
68
|
+
"Kam",
|
|
69
|
+
"Jum",
|
|
70
|
+
"Sab"
|
|
71
|
+
];
|
|
72
|
+
var TIMEZONE_MAP = {
|
|
73
|
+
// UTC+7 - WIB
|
|
74
|
+
"Asia/Jakarta": "WIB",
|
|
75
|
+
"Asia/Pontianak": "WIB",
|
|
76
|
+
// UTC+8 - WITA
|
|
77
|
+
"Asia/Makassar": "WITA",
|
|
78
|
+
"Asia/Denpasar": "WITA",
|
|
79
|
+
"Asia/Manado": "WITA",
|
|
80
|
+
"Asia/Palu": "WITA",
|
|
81
|
+
// UTC+9 - WIT
|
|
82
|
+
"Asia/Jayapura": "WIT"
|
|
83
|
+
};
|
|
84
|
+
var VALID_UTC_OFFSETS = [7, 8, 9];
|
|
85
|
+
|
|
86
|
+
// src/datetime/calc.ts
|
|
87
|
+
function isLeapYear(year) {
|
|
88
|
+
if (!Number.isFinite(year) || !Number.isInteger(year)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
92
|
+
}
|
|
93
|
+
function daysInMonth(month, year) {
|
|
94
|
+
if (!Number.isFinite(month) || !Number.isInteger(month) || !Number.isFinite(year) || !Number.isInteger(year) || month < 1 || month > 12) {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if ([1, 3, 5, 7, 8, 10, 12].includes(month)) {
|
|
98
|
+
return 31;
|
|
99
|
+
}
|
|
100
|
+
if ([4, 6, 9, 11].includes(month)) {
|
|
101
|
+
return 30;
|
|
102
|
+
}
|
|
103
|
+
return isLeapYear(year) ? 29 : 28;
|
|
104
|
+
}
|
|
105
|
+
function isValidDate(date) {
|
|
106
|
+
return date instanceof Date && !Number.isNaN(date.getTime());
|
|
107
|
+
}
|
|
108
|
+
function isWeekend(date) {
|
|
109
|
+
const day = date.getDay();
|
|
110
|
+
return day === 0 || day === 6;
|
|
111
|
+
}
|
|
112
|
+
function isWorkingDay(date) {
|
|
113
|
+
const day = date.getDay();
|
|
114
|
+
return day >= 1 && day <= 5;
|
|
115
|
+
}
|
|
116
|
+
function normalizeDate(date) {
|
|
117
|
+
let result;
|
|
118
|
+
if (date instanceof Date) {
|
|
119
|
+
result = date;
|
|
120
|
+
} else if (typeof date === "number") {
|
|
121
|
+
result = new Date(date);
|
|
122
|
+
} else if (typeof date === "string") {
|
|
123
|
+
result = new Date(date);
|
|
124
|
+
} else {
|
|
125
|
+
throw new InvalidDateError("Date must be a Date, string, or number");
|
|
126
|
+
}
|
|
127
|
+
if (Number.isNaN(result.getTime())) {
|
|
128
|
+
throw new InvalidDateError(`Unable to parse date: ${String(date)}`);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function getAge(birthDate, options = {}) {
|
|
133
|
+
const birth = normalizeDate(birthDate);
|
|
134
|
+
const from = options.fromDate ? normalizeDate(options.fromDate) : /* @__PURE__ */ new Date();
|
|
135
|
+
if (birth.getTime() > from.getTime()) {
|
|
136
|
+
throw new InvalidDateError(
|
|
137
|
+
"Birth date cannot be in the future relative to fromDate"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
let years = from.getFullYear() - birth.getFullYear();
|
|
141
|
+
let months = from.getMonth() - birth.getMonth();
|
|
142
|
+
let days = from.getDate() - birth.getDate();
|
|
143
|
+
if (days < 0) {
|
|
144
|
+
months--;
|
|
145
|
+
const prevMonth = from.getMonth() === 0 ? 11 : from.getMonth() - 1;
|
|
146
|
+
const prevMonthYear = from.getMonth() === 0 ? from.getFullYear() - 1 : from.getFullYear();
|
|
147
|
+
days += daysInMonth(prevMonth + 1, prevMonthYear);
|
|
148
|
+
}
|
|
149
|
+
if (months < 0) {
|
|
150
|
+
years--;
|
|
151
|
+
months += 12;
|
|
152
|
+
}
|
|
153
|
+
const result = { years, months, days };
|
|
154
|
+
if (options.asString) {
|
|
155
|
+
return formatAgeString(result);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function formatAgeString(age) {
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (age.years > 0) {
|
|
162
|
+
parts.push(`${age.years} Tahun`);
|
|
163
|
+
}
|
|
164
|
+
if (age.months > 0) {
|
|
165
|
+
parts.push(`${age.months} Bulan`);
|
|
166
|
+
}
|
|
167
|
+
if (age.days > 0) {
|
|
168
|
+
parts.push(`${age.days} Hari`);
|
|
169
|
+
}
|
|
170
|
+
if (parts.length === 0) {
|
|
171
|
+
return "0 Hari";
|
|
172
|
+
}
|
|
173
|
+
return parts.join(" ");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/datetime/parse.ts
|
|
177
|
+
function parseDate(dateStr) {
|
|
178
|
+
const trimmed = dateStr.trim();
|
|
179
|
+
if (!trimmed) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (trimmed.includes(" ") || trimmed.includes(":")) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const parts = trimmed.split(/[-/.]/);
|
|
186
|
+
if (parts.length !== 3) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const nums = parts.map((p) => parseInt(p, 10));
|
|
190
|
+
if (nums.some((n) => Number.isNaN(n) || n < 0)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const [first, second, third] = nums;
|
|
194
|
+
let day;
|
|
195
|
+
let month;
|
|
196
|
+
let year;
|
|
197
|
+
if (first > 999 && first > 31) {
|
|
198
|
+
year = first;
|
|
199
|
+
month = second;
|
|
200
|
+
day = third;
|
|
201
|
+
} else {
|
|
202
|
+
if (third < 1e3) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
day = first;
|
|
206
|
+
month = second;
|
|
207
|
+
year = third;
|
|
208
|
+
}
|
|
209
|
+
if (month < 1 || month > 12) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (year < 1e3 || year > 9999) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const maxDays = daysInMonth(month, year);
|
|
216
|
+
if (maxDays === 0 || day < 1 || day > maxDays) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const date = new Date(year, month - 1, day);
|
|
220
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return date;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/datetime/format.ts
|
|
227
|
+
function normalizeDate2(date) {
|
|
228
|
+
let result;
|
|
229
|
+
if (date instanceof Date) {
|
|
230
|
+
result = date;
|
|
231
|
+
} else if (typeof date === "number") {
|
|
232
|
+
result = new Date(date);
|
|
233
|
+
} else if (typeof date === "string") {
|
|
234
|
+
result = new Date(date);
|
|
235
|
+
} else {
|
|
236
|
+
throw new InvalidDateError("Date must be a Date, string, or number");
|
|
237
|
+
}
|
|
238
|
+
if (Number.isNaN(result.getTime())) {
|
|
239
|
+
throw new InvalidDateError(`Unable to parse date: ${String(date)}`);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function formatDate(date, style = "long") {
|
|
244
|
+
const d = normalizeDate2(date);
|
|
245
|
+
const day = d.getDate();
|
|
246
|
+
const month = d.getMonth() + 1;
|
|
247
|
+
const year = d.getFullYear();
|
|
248
|
+
const dayOfWeek = d.getDay();
|
|
249
|
+
switch (style) {
|
|
250
|
+
case "full":
|
|
251
|
+
return `${DAY_NAMES[dayOfWeek]}, ${day} ${MONTH_NAMES[month]} ${year}`;
|
|
252
|
+
case "long":
|
|
253
|
+
return `${day} ${MONTH_NAMES[month]} ${year}`;
|
|
254
|
+
case "medium":
|
|
255
|
+
return `${day} ${MONTH_NAMES_SHORT[month]} ${year}`;
|
|
256
|
+
case "short": {
|
|
257
|
+
const dd = String(day).padStart(2, "0");
|
|
258
|
+
const mm = String(month).padStart(2, "0");
|
|
259
|
+
return `${dd}/${mm}/${year}`;
|
|
260
|
+
}
|
|
261
|
+
case "weekday":
|
|
262
|
+
return DAY_NAMES[dayOfWeek];
|
|
263
|
+
case "month":
|
|
264
|
+
return MONTH_NAMES[month];
|
|
265
|
+
default:
|
|
266
|
+
throw new InvalidDateError(`Unknown format style: ${style}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function formatDateRange(start, end, style = "long") {
|
|
270
|
+
const s = normalizeDate2(start);
|
|
271
|
+
const e = normalizeDate2(end);
|
|
272
|
+
if (e.getTime() < s.getTime()) {
|
|
273
|
+
throw new InvalidDateRangeError();
|
|
274
|
+
}
|
|
275
|
+
if (style === "short") {
|
|
276
|
+
return `${formatDate(s, "short")} - ${formatDate(e, "short")}`;
|
|
277
|
+
}
|
|
278
|
+
if (style === "full") {
|
|
279
|
+
return `${formatDate(s, "full")} - ${formatDate(e, "full")}`;
|
|
280
|
+
}
|
|
281
|
+
const sDay = s.getDate();
|
|
282
|
+
const eDay = e.getDate();
|
|
283
|
+
const sMonth = s.getMonth() + 1;
|
|
284
|
+
const eMonth = e.getMonth() + 1;
|
|
285
|
+
const sYear = s.getFullYear();
|
|
286
|
+
const eYear = e.getFullYear();
|
|
287
|
+
if (sDay === eDay && sMonth === eMonth && sYear === eYear) {
|
|
288
|
+
return formatDate(s, style);
|
|
289
|
+
}
|
|
290
|
+
if (sYear !== eYear) {
|
|
291
|
+
return `${formatDate(s, style)} - ${formatDate(e, style)}`;
|
|
292
|
+
}
|
|
293
|
+
if (sMonth !== eMonth) {
|
|
294
|
+
if (style === "long") {
|
|
295
|
+
return `${sDay} ${MONTH_NAMES[sMonth]} - ${eDay} ${MONTH_NAMES[eMonth]} ${eYear}`;
|
|
296
|
+
}
|
|
297
|
+
return `${sDay} ${MONTH_NAMES_SHORT[sMonth]} - ${eDay} ${MONTH_NAMES_SHORT[eMonth]} ${eYear}`;
|
|
298
|
+
}
|
|
299
|
+
if (style === "long") {
|
|
300
|
+
return `${sDay} - ${eDay} ${MONTH_NAMES[eMonth]} ${eYear}`;
|
|
301
|
+
}
|
|
302
|
+
return `${sDay} - ${eDay} ${MONTH_NAMES_SHORT[eMonth]} ${eYear}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/datetime/relative.ts
|
|
306
|
+
function normalizeDate3(date) {
|
|
307
|
+
let result;
|
|
308
|
+
if (date instanceof Date) {
|
|
309
|
+
result = date;
|
|
310
|
+
} else if (typeof date === "number") {
|
|
311
|
+
result = new Date(date);
|
|
312
|
+
} else if (typeof date === "string") {
|
|
313
|
+
result = new Date(date);
|
|
314
|
+
} else {
|
|
315
|
+
throw new InvalidDateError("Date must be a Date, string, or number");
|
|
316
|
+
}
|
|
317
|
+
if (Number.isNaN(result.getTime())) {
|
|
318
|
+
throw new InvalidDateError(`Unable to parse date: ${String(date)}`);
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
function toRelativeTime(date, baseDate = /* @__PURE__ */ new Date()) {
|
|
323
|
+
const d = normalizeDate3(date);
|
|
324
|
+
const base = normalizeDate3(baseDate);
|
|
325
|
+
const diffMs = d.getTime() - base.getTime();
|
|
326
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
327
|
+
const diffMin = Math.floor(diffMs / (1e3 * 60));
|
|
328
|
+
const diffHour = Math.floor(diffMs / (1e3 * 60 * 60));
|
|
329
|
+
const diffDay = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
330
|
+
if (diffMs === 0) {
|
|
331
|
+
return "Sekarang";
|
|
332
|
+
}
|
|
333
|
+
if (diffMs > 0) {
|
|
334
|
+
if (diffSec < 60) {
|
|
335
|
+
return "Baru saja";
|
|
336
|
+
}
|
|
337
|
+
if (diffMin < 60) {
|
|
338
|
+
return `${diffMin} menit lagi`;
|
|
339
|
+
}
|
|
340
|
+
if (diffHour < 24) {
|
|
341
|
+
return `${diffHour} jam lagi`;
|
|
342
|
+
}
|
|
343
|
+
if (diffHour < 48) {
|
|
344
|
+
return "Besok";
|
|
345
|
+
}
|
|
346
|
+
if (diffDay <= 30) {
|
|
347
|
+
return `${diffDay} hari lagi`;
|
|
348
|
+
}
|
|
349
|
+
return formatDate(d, "long");
|
|
350
|
+
}
|
|
351
|
+
const absDiffSec = Math.abs(diffSec);
|
|
352
|
+
const absDiffMin = Math.abs(diffMin);
|
|
353
|
+
const absDiffHour = Math.abs(diffHour);
|
|
354
|
+
const absDiffDay = Math.abs(diffDay);
|
|
355
|
+
if (absDiffSec < 60) {
|
|
356
|
+
return "Baru saja";
|
|
357
|
+
}
|
|
358
|
+
if (absDiffMin < 60) {
|
|
359
|
+
return `${absDiffMin} menit yang lalu`;
|
|
360
|
+
}
|
|
361
|
+
if (absDiffHour < 24) {
|
|
362
|
+
return `${absDiffHour} jam yang lalu`;
|
|
363
|
+
}
|
|
364
|
+
if (absDiffHour < 48) {
|
|
365
|
+
return "Kemarin";
|
|
366
|
+
}
|
|
367
|
+
if (absDiffDay <= 30) {
|
|
368
|
+
return `${absDiffDay} hari yang lalu`;
|
|
369
|
+
}
|
|
370
|
+
return formatDate(d, "long");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/datetime/timezone.ts
|
|
374
|
+
function getIndonesianTimezone(input) {
|
|
375
|
+
if (typeof input === "number") {
|
|
376
|
+
if (!Number.isFinite(input) || !Number.isInteger(input)) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
switch (input) {
|
|
380
|
+
case 7:
|
|
381
|
+
return "WIB";
|
|
382
|
+
case 8:
|
|
383
|
+
return "WITA";
|
|
384
|
+
case 9:
|
|
385
|
+
return "WIT";
|
|
386
|
+
default:
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (typeof input !== "string") {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const trimmed = input.trim();
|
|
394
|
+
const offsetMatch = trimmed.match(/^([+-])(\d{2}):?(\d{2})$/);
|
|
395
|
+
if (offsetMatch) {
|
|
396
|
+
const sign = offsetMatch[1];
|
|
397
|
+
const hours = parseInt(offsetMatch[2], 10);
|
|
398
|
+
const minutes = parseInt(offsetMatch[3], 10);
|
|
399
|
+
if (sign === "-") {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
if (minutes !== 0) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
switch (hours) {
|
|
406
|
+
case 7:
|
|
407
|
+
return "WIB";
|
|
408
|
+
case 8:
|
|
409
|
+
return "WITA";
|
|
410
|
+
case 9:
|
|
411
|
+
return "WIT";
|
|
412
|
+
default:
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (TIMEZONE_MAP[trimmed]) {
|
|
417
|
+
return TIMEZONE_MAP[trimmed];
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
exports.DAY_NAMES = DAY_NAMES;
|
|
423
|
+
exports.DAY_NAMES_SHORT = DAY_NAMES_SHORT;
|
|
424
|
+
exports.InvalidDateError = InvalidDateError;
|
|
425
|
+
exports.InvalidDateRangeError = InvalidDateRangeError;
|
|
426
|
+
exports.MONTH_NAMES = MONTH_NAMES;
|
|
427
|
+
exports.MONTH_NAMES_SHORT = MONTH_NAMES_SHORT;
|
|
428
|
+
exports.TIMEZONE_MAP = TIMEZONE_MAP;
|
|
429
|
+
exports.VALID_UTC_OFFSETS = VALID_UTC_OFFSETS;
|
|
430
|
+
exports.daysInMonth = daysInMonth;
|
|
431
|
+
exports.formatDate = formatDate;
|
|
432
|
+
exports.formatDateRange = formatDateRange;
|
|
433
|
+
exports.getAge = getAge;
|
|
434
|
+
exports.getIndonesianTimezone = getIndonesianTimezone;
|
|
435
|
+
exports.isLeapYear = isLeapYear;
|
|
436
|
+
exports.isValidDate = isValidDate;
|
|
437
|
+
exports.isWeekend = isWeekend;
|
|
438
|
+
exports.isWorkingDay = isWorkingDay;
|
|
439
|
+
exports.parseDate = parseDate;
|
|
440
|
+
exports.toRelativeTime = toRelativeTime;
|
|
441
|
+
//# sourceMappingURL=index.cjs.map
|
|
442
|
+
//# sourceMappingURL=index.cjs.map
|