@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.
@@ -0,0 +1,422 @@
1
+ // src/datetime/types.ts
2
+ var InvalidDateError = class extends Error {
3
+ constructor(message = "Invalid date provided") {
4
+ super(message);
5
+ /** Error code for programmatic identification */
6
+ this.code = "INVALID_DATE";
7
+ this.name = "InvalidDateError";
8
+ }
9
+ };
10
+ var InvalidDateRangeError = class extends Error {
11
+ constructor(message = "End date must be after start date") {
12
+ super(message);
13
+ /** Error code for programmatic identification */
14
+ this.code = "INVALID_DATE_RANGE";
15
+ this.name = "InvalidDateRangeError";
16
+ }
17
+ };
18
+
19
+ // src/datetime/constants.ts
20
+ var MONTH_NAMES = [
21
+ "",
22
+ // Placeholder for 0-index
23
+ "Januari",
24
+ "Februari",
25
+ "Maret",
26
+ "April",
27
+ "Mei",
28
+ "Juni",
29
+ "Juli",
30
+ "Agustus",
31
+ "September",
32
+ "Oktober",
33
+ "November",
34
+ "Desember"
35
+ ];
36
+ var MONTH_NAMES_SHORT = [
37
+ "",
38
+ // Placeholder for 0-index
39
+ "Jan",
40
+ "Feb",
41
+ "Mar",
42
+ "Apr",
43
+ "Mei",
44
+ "Jun",
45
+ "Jul",
46
+ "Agu",
47
+ "Sep",
48
+ "Okt",
49
+ "Nov",
50
+ "Des"
51
+ ];
52
+ var DAY_NAMES = [
53
+ "Minggu",
54
+ "Senin",
55
+ "Selasa",
56
+ "Rabu",
57
+ "Kamis",
58
+ "Jumat",
59
+ "Sabtu"
60
+ ];
61
+ var DAY_NAMES_SHORT = [
62
+ "Min",
63
+ "Sen",
64
+ "Sel",
65
+ "Rab",
66
+ "Kam",
67
+ "Jum",
68
+ "Sab"
69
+ ];
70
+ var TIMEZONE_MAP = {
71
+ // UTC+7 - WIB
72
+ "Asia/Jakarta": "WIB",
73
+ "Asia/Pontianak": "WIB",
74
+ // UTC+8 - WITA
75
+ "Asia/Makassar": "WITA",
76
+ "Asia/Denpasar": "WITA",
77
+ "Asia/Manado": "WITA",
78
+ "Asia/Palu": "WITA",
79
+ // UTC+9 - WIT
80
+ "Asia/Jayapura": "WIT"
81
+ };
82
+ var VALID_UTC_OFFSETS = [7, 8, 9];
83
+
84
+ // src/datetime/calc.ts
85
+ function isLeapYear(year) {
86
+ if (!Number.isFinite(year) || !Number.isInteger(year)) {
87
+ return false;
88
+ }
89
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
90
+ }
91
+ function daysInMonth(month, year) {
92
+ if (!Number.isFinite(month) || !Number.isInteger(month) || !Number.isFinite(year) || !Number.isInteger(year) || month < 1 || month > 12) {
93
+ return 0;
94
+ }
95
+ if ([1, 3, 5, 7, 8, 10, 12].includes(month)) {
96
+ return 31;
97
+ }
98
+ if ([4, 6, 9, 11].includes(month)) {
99
+ return 30;
100
+ }
101
+ return isLeapYear(year) ? 29 : 28;
102
+ }
103
+ function isValidDate(date) {
104
+ return date instanceof Date && !Number.isNaN(date.getTime());
105
+ }
106
+ function isWeekend(date) {
107
+ const day = date.getDay();
108
+ return day === 0 || day === 6;
109
+ }
110
+ function isWorkingDay(date) {
111
+ const day = date.getDay();
112
+ return day >= 1 && day <= 5;
113
+ }
114
+ function normalizeDate(date) {
115
+ let result;
116
+ if (date instanceof Date) {
117
+ result = date;
118
+ } else if (typeof date === "number") {
119
+ result = new Date(date);
120
+ } else if (typeof date === "string") {
121
+ result = new Date(date);
122
+ } else {
123
+ throw new InvalidDateError("Date must be a Date, string, or number");
124
+ }
125
+ if (Number.isNaN(result.getTime())) {
126
+ throw new InvalidDateError(`Unable to parse date: ${String(date)}`);
127
+ }
128
+ return result;
129
+ }
130
+ function getAge(birthDate, options = {}) {
131
+ const birth = normalizeDate(birthDate);
132
+ const from = options.fromDate ? normalizeDate(options.fromDate) : /* @__PURE__ */ new Date();
133
+ if (birth.getTime() > from.getTime()) {
134
+ throw new InvalidDateError(
135
+ "Birth date cannot be in the future relative to fromDate"
136
+ );
137
+ }
138
+ let years = from.getFullYear() - birth.getFullYear();
139
+ let months = from.getMonth() - birth.getMonth();
140
+ let days = from.getDate() - birth.getDate();
141
+ if (days < 0) {
142
+ months--;
143
+ const prevMonth = from.getMonth() === 0 ? 11 : from.getMonth() - 1;
144
+ const prevMonthYear = from.getMonth() === 0 ? from.getFullYear() - 1 : from.getFullYear();
145
+ days += daysInMonth(prevMonth + 1, prevMonthYear);
146
+ }
147
+ if (months < 0) {
148
+ years--;
149
+ months += 12;
150
+ }
151
+ const result = { years, months, days };
152
+ if (options.asString) {
153
+ return formatAgeString(result);
154
+ }
155
+ return result;
156
+ }
157
+ function formatAgeString(age) {
158
+ const parts = [];
159
+ if (age.years > 0) {
160
+ parts.push(`${age.years} Tahun`);
161
+ }
162
+ if (age.months > 0) {
163
+ parts.push(`${age.months} Bulan`);
164
+ }
165
+ if (age.days > 0) {
166
+ parts.push(`${age.days} Hari`);
167
+ }
168
+ if (parts.length === 0) {
169
+ return "0 Hari";
170
+ }
171
+ return parts.join(" ");
172
+ }
173
+
174
+ // src/datetime/parse.ts
175
+ function parseDate(dateStr) {
176
+ const trimmed = dateStr.trim();
177
+ if (!trimmed) {
178
+ return null;
179
+ }
180
+ if (trimmed.includes(" ") || trimmed.includes(":")) {
181
+ return null;
182
+ }
183
+ const parts = trimmed.split(/[-/.]/);
184
+ if (parts.length !== 3) {
185
+ return null;
186
+ }
187
+ const nums = parts.map((p) => parseInt(p, 10));
188
+ if (nums.some((n) => Number.isNaN(n) || n < 0)) {
189
+ return null;
190
+ }
191
+ const [first, second, third] = nums;
192
+ let day;
193
+ let month;
194
+ let year;
195
+ if (first > 999 && first > 31) {
196
+ year = first;
197
+ month = second;
198
+ day = third;
199
+ } else {
200
+ if (third < 1e3) {
201
+ return null;
202
+ }
203
+ day = first;
204
+ month = second;
205
+ year = third;
206
+ }
207
+ if (month < 1 || month > 12) {
208
+ return null;
209
+ }
210
+ if (year < 1e3 || year > 9999) {
211
+ return null;
212
+ }
213
+ const maxDays = daysInMonth(month, year);
214
+ if (maxDays === 0 || day < 1 || day > maxDays) {
215
+ return null;
216
+ }
217
+ const date = new Date(year, month - 1, day);
218
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
219
+ return null;
220
+ }
221
+ return date;
222
+ }
223
+
224
+ // src/datetime/format.ts
225
+ function normalizeDate2(date) {
226
+ let result;
227
+ if (date instanceof Date) {
228
+ result = date;
229
+ } else if (typeof date === "number") {
230
+ result = new Date(date);
231
+ } else if (typeof date === "string") {
232
+ result = new Date(date);
233
+ } else {
234
+ throw new InvalidDateError("Date must be a Date, string, or number");
235
+ }
236
+ if (Number.isNaN(result.getTime())) {
237
+ throw new InvalidDateError(`Unable to parse date: ${String(date)}`);
238
+ }
239
+ return result;
240
+ }
241
+ function formatDate(date, style = "long") {
242
+ const d = normalizeDate2(date);
243
+ const day = d.getDate();
244
+ const month = d.getMonth() + 1;
245
+ const year = d.getFullYear();
246
+ const dayOfWeek = d.getDay();
247
+ switch (style) {
248
+ case "full":
249
+ return `${DAY_NAMES[dayOfWeek]}, ${day} ${MONTH_NAMES[month]} ${year}`;
250
+ case "long":
251
+ return `${day} ${MONTH_NAMES[month]} ${year}`;
252
+ case "medium":
253
+ return `${day} ${MONTH_NAMES_SHORT[month]} ${year}`;
254
+ case "short": {
255
+ const dd = String(day).padStart(2, "0");
256
+ const mm = String(month).padStart(2, "0");
257
+ return `${dd}/${mm}/${year}`;
258
+ }
259
+ case "weekday":
260
+ return DAY_NAMES[dayOfWeek];
261
+ case "month":
262
+ return MONTH_NAMES[month];
263
+ default:
264
+ throw new InvalidDateError(`Unknown format style: ${style}`);
265
+ }
266
+ }
267
+ function formatDateRange(start, end, style = "long") {
268
+ const s = normalizeDate2(start);
269
+ const e = normalizeDate2(end);
270
+ if (e.getTime() < s.getTime()) {
271
+ throw new InvalidDateRangeError();
272
+ }
273
+ if (style === "short") {
274
+ return `${formatDate(s, "short")} - ${formatDate(e, "short")}`;
275
+ }
276
+ if (style === "full") {
277
+ return `${formatDate(s, "full")} - ${formatDate(e, "full")}`;
278
+ }
279
+ const sDay = s.getDate();
280
+ const eDay = e.getDate();
281
+ const sMonth = s.getMonth() + 1;
282
+ const eMonth = e.getMonth() + 1;
283
+ const sYear = s.getFullYear();
284
+ const eYear = e.getFullYear();
285
+ if (sDay === eDay && sMonth === eMonth && sYear === eYear) {
286
+ return formatDate(s, style);
287
+ }
288
+ if (sYear !== eYear) {
289
+ return `${formatDate(s, style)} - ${formatDate(e, style)}`;
290
+ }
291
+ if (sMonth !== eMonth) {
292
+ if (style === "long") {
293
+ return `${sDay} ${MONTH_NAMES[sMonth]} - ${eDay} ${MONTH_NAMES[eMonth]} ${eYear}`;
294
+ }
295
+ return `${sDay} ${MONTH_NAMES_SHORT[sMonth]} - ${eDay} ${MONTH_NAMES_SHORT[eMonth]} ${eYear}`;
296
+ }
297
+ if (style === "long") {
298
+ return `${sDay} - ${eDay} ${MONTH_NAMES[eMonth]} ${eYear}`;
299
+ }
300
+ return `${sDay} - ${eDay} ${MONTH_NAMES_SHORT[eMonth]} ${eYear}`;
301
+ }
302
+
303
+ // src/datetime/relative.ts
304
+ function normalizeDate3(date) {
305
+ let result;
306
+ if (date instanceof Date) {
307
+ result = date;
308
+ } else if (typeof date === "number") {
309
+ result = new Date(date);
310
+ } else if (typeof date === "string") {
311
+ result = new Date(date);
312
+ } else {
313
+ throw new InvalidDateError("Date must be a Date, string, or number");
314
+ }
315
+ if (Number.isNaN(result.getTime())) {
316
+ throw new InvalidDateError(`Unable to parse date: ${String(date)}`);
317
+ }
318
+ return result;
319
+ }
320
+ function toRelativeTime(date, baseDate = /* @__PURE__ */ new Date()) {
321
+ const d = normalizeDate3(date);
322
+ const base = normalizeDate3(baseDate);
323
+ const diffMs = d.getTime() - base.getTime();
324
+ const diffSec = Math.floor(diffMs / 1e3);
325
+ const diffMin = Math.floor(diffMs / (1e3 * 60));
326
+ const diffHour = Math.floor(diffMs / (1e3 * 60 * 60));
327
+ const diffDay = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
328
+ if (diffMs === 0) {
329
+ return "Sekarang";
330
+ }
331
+ if (diffMs > 0) {
332
+ if (diffSec < 60) {
333
+ return "Baru saja";
334
+ }
335
+ if (diffMin < 60) {
336
+ return `${diffMin} menit lagi`;
337
+ }
338
+ if (diffHour < 24) {
339
+ return `${diffHour} jam lagi`;
340
+ }
341
+ if (diffHour < 48) {
342
+ return "Besok";
343
+ }
344
+ if (diffDay <= 30) {
345
+ return `${diffDay} hari lagi`;
346
+ }
347
+ return formatDate(d, "long");
348
+ }
349
+ const absDiffSec = Math.abs(diffSec);
350
+ const absDiffMin = Math.abs(diffMin);
351
+ const absDiffHour = Math.abs(diffHour);
352
+ const absDiffDay = Math.abs(diffDay);
353
+ if (absDiffSec < 60) {
354
+ return "Baru saja";
355
+ }
356
+ if (absDiffMin < 60) {
357
+ return `${absDiffMin} menit yang lalu`;
358
+ }
359
+ if (absDiffHour < 24) {
360
+ return `${absDiffHour} jam yang lalu`;
361
+ }
362
+ if (absDiffHour < 48) {
363
+ return "Kemarin";
364
+ }
365
+ if (absDiffDay <= 30) {
366
+ return `${absDiffDay} hari yang lalu`;
367
+ }
368
+ return formatDate(d, "long");
369
+ }
370
+
371
+ // src/datetime/timezone.ts
372
+ function getIndonesianTimezone(input) {
373
+ if (typeof input === "number") {
374
+ if (!Number.isFinite(input) || !Number.isInteger(input)) {
375
+ return null;
376
+ }
377
+ switch (input) {
378
+ case 7:
379
+ return "WIB";
380
+ case 8:
381
+ return "WITA";
382
+ case 9:
383
+ return "WIT";
384
+ default:
385
+ return null;
386
+ }
387
+ }
388
+ if (typeof input !== "string") {
389
+ return null;
390
+ }
391
+ const trimmed = input.trim();
392
+ const offsetMatch = trimmed.match(/^([+-])(\d{2}):?(\d{2})$/);
393
+ if (offsetMatch) {
394
+ const sign = offsetMatch[1];
395
+ const hours = parseInt(offsetMatch[2], 10);
396
+ const minutes = parseInt(offsetMatch[3], 10);
397
+ if (sign === "-") {
398
+ return null;
399
+ }
400
+ if (minutes !== 0) {
401
+ return null;
402
+ }
403
+ switch (hours) {
404
+ case 7:
405
+ return "WIB";
406
+ case 8:
407
+ return "WITA";
408
+ case 9:
409
+ return "WIT";
410
+ default:
411
+ return null;
412
+ }
413
+ }
414
+ if (TIMEZONE_MAP[trimmed]) {
415
+ return TIMEZONE_MAP[trimmed];
416
+ }
417
+ return null;
418
+ }
419
+
420
+ export { DAY_NAMES, DAY_NAMES_SHORT, InvalidDateError, InvalidDateRangeError, MONTH_NAMES, MONTH_NAMES_SHORT, TIMEZONE_MAP, VALID_UTC_OFFSETS, daysInMonth, formatDate, formatDateRange, getAge, getIndonesianTimezone, isLeapYear, isValidDate, isWeekend, isWorkingDay, parseDate, toRelativeTime };
421
+ //# sourceMappingURL=index.js.map
422
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/datetime/types.ts","../../src/datetime/constants.ts","../../src/datetime/calc.ts","../../src/datetime/parse.ts","../../src/datetime/format.ts","../../src/datetime/relative.ts","../../src/datetime/timezone.ts"],"names":["normalizeDate"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Custom error classes for datetime module\n *\n * @module datetime/types\n * @packageDocumentation\n */\n\n/**\n * Error thrown when an invalid date is provided to a function.\n * Extends native Error with a `code` property for programmatic error handling.\n *\n * @example\n * ```typescript\n * try {\n * formatDate('invalid');\n * } catch (error) {\n * if (error instanceof InvalidDateError) {\n * console.log(error.code); // 'INVALID_DATE'\n * }\n * }\n * ```\n */\nexport class InvalidDateError extends Error {\n /** Error code for programmatic identification */\n readonly code = 'INVALID_DATE' as const;\n\n constructor(message: string = 'Invalid date provided') {\n super(message);\n this.name = 'InvalidDateError';\n }\n}\n\n/**\n * Error thrown when an invalid date range is provided.\n * Extends native Error with a `code` property for programmatic error handling.\n *\n * @example\n * ```typescript\n * try {\n * formatDateRange(new Date('2026-01-05'), new Date('2026-01-01'));\n * } catch (error) {\n * if (error instanceof InvalidDateRangeError) {\n * console.log(error.code); // 'INVALID_DATE_RANGE'\n * }\n * }\n * ```\n */\nexport class InvalidDateRangeError extends Error {\n /** Error code for programmatic identification */\n readonly code = 'INVALID_DATE_RANGE' as const;\n\n constructor(message: string = 'End date must be after start date') {\n super(message);\n this.name = 'InvalidDateRangeError';\n }\n}\n\n/**\n * Date formatting style options\n */\nexport type DateStyle =\n | 'full'\n | 'long'\n | 'medium'\n | 'short'\n | 'weekday'\n | 'month';\n\n/**\n * Options for getAge function\n */\nexport interface AgeOptions {\n /**\n * Reference date to calculate age from.\n * Defaults to current date at function call time.\n * @defaultValue new Date()\n */\n fromDate?: Date | string | number;\n\n /**\n * Return age as formatted string instead of object.\n * @defaultValue false\n */\n asString?: boolean;\n}\n\n/**\n * Age calculation result object\n */\nexport interface AgeResult {\n /** Full years */\n years: number;\n /** Remaining months (0-11) */\n months: number;\n /** Remaining days (0-30) */\n days: number;\n}\n","/**\n * Constants for Indonesian datetime formatting\n *\n * @module datetime/constants\n * @packageDocumentation\n */\n\n/** Full Indonesian month names (1-indexed: index 0 = empty, 1 = Januari) */\nexport const MONTH_NAMES: readonly string[] = [\n '', // Placeholder for 0-index\n 'Januari',\n 'Februari',\n 'Maret',\n 'April',\n 'Mei',\n 'Juni',\n 'Juli',\n 'Agustus',\n 'September',\n 'Oktober',\n 'November',\n 'Desember',\n];\n\n/** Short Indonesian month names (3-letter abbreviation) */\nexport const MONTH_NAMES_SHORT: readonly string[] = [\n '', // Placeholder for 0-index\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'Mei',\n 'Jun',\n 'Jul',\n 'Agu',\n 'Sep',\n 'Okt',\n 'Nov',\n 'Des',\n];\n\n/** Full Indonesian day names */\nexport const DAY_NAMES: readonly string[] = [\n 'Minggu',\n 'Senin',\n 'Selasa',\n 'Rabu',\n 'Kamis',\n 'Jumat',\n 'Sabtu',\n];\n\n/** Short Indonesian day names (3-letter abbreviation) */\nexport const DAY_NAMES_SHORT: readonly string[] = [\n 'Min',\n 'Sen',\n 'Sel',\n 'Rab',\n 'Kam',\n 'Jum',\n 'Sab',\n];\n\n/** Mapping of IANA timezone names to Indonesian abbreviations */\nexport const TIMEZONE_MAP: Readonly<Record<string, 'WIB' | 'WITA' | 'WIT'>> = {\n // UTC+7 - WIB\n 'Asia/Jakarta': 'WIB',\n 'Asia/Pontianak': 'WIB',\n\n // UTC+8 - WITA\n 'Asia/Makassar': 'WITA',\n 'Asia/Denpasar': 'WITA',\n 'Asia/Manado': 'WITA',\n 'Asia/Palu': 'WITA',\n\n // UTC+9 - WIT\n 'Asia/Jayapura': 'WIT',\n};\n\n/** Valid UTC offset hours that map to Indonesian timezones */\nexport const VALID_UTC_OFFSETS: readonly number[] = [7, 8, 9];\n","/**\n * Date calculation utilities\n *\n * @module datetime/calc\n * @packageDocumentation\n */\n\nimport { InvalidDateError } from './types';\n\n/**\n * Check if a year is a leap year.\n *\n * A year is a leap year if:\n * - Divisible by 4, but not by 100, OR\n * - Divisible by 400\n *\n * @param year - The year to check\n * @returns `true` if leap year, `false` otherwise (including invalid inputs)\n *\n * @example\n * ```typescript\n * isLeapYear(2024); // true\n * isLeapYear(2023); // false\n * isLeapYear(1900); // false (divisible by 100 but not 400)\n * isLeapYear(2000); // true (divisible by 400)\n * isLeapYear(NaN); // false\n * isLeapYear(3.5); // false (non-integer)\n * ```\n */\nexport function isLeapYear(year: number): boolean {\n // Return false for non-finite, non-integer, or NaN values\n if (!Number.isFinite(year) || !Number.isInteger(year)) {\n return false;\n }\n\n return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\n/**\n * Get the number of days in a month.\n *\n * Accounts for leap years in February.\n *\n * @param month - Month number (1-12, 1-indexed)\n * @param year - Full year (e.g., 2026)\n * @returns Number of days in the month, or 0 for invalid inputs\n *\n * @example\n * ```typescript\n * daysInMonth(1, 2026); // 31 (January)\n * daysInMonth(2, 2024); // 29 (February, leap year)\n * daysInMonth(2, 2023); // 28 (February, non-leap year)\n * daysInMonth(4, 2026); // 30 (April)\n * daysInMonth(13, 2026); // 0 (invalid month)\n * daysInMonth(2, NaN); // 0 (invalid year)\n * ```\n */\nexport function daysInMonth(month: number, year: number): number {\n // Validate inputs\n if (\n !Number.isFinite(month) ||\n !Number.isInteger(month) ||\n !Number.isFinite(year) ||\n !Number.isInteger(year) ||\n month < 1 ||\n month > 12\n ) {\n return 0;\n }\n\n // Months with 31 days: Jan(1), Mar(3), May(5), Jul(7), Aug(8), Oct(10), Dec(12)\n if ([1, 3, 5, 7, 8, 10, 12].includes(month)) {\n return 31;\n }\n\n // Months with 30 days: Apr(4), Jun(6), Sep(9), Nov(11)\n if ([4, 6, 9, 11].includes(month)) {\n return 30;\n }\n\n // February - check for leap year\n return isLeapYear(year) ? 29 : 28;\n}\n\n/**\n * Type guard to check if a value is a valid Date object.\n *\n * Returns `true` only for Date instances that represent a valid date\n * (i.e., not `Invalid Date`). Returns `false` for null, undefined,\n * invalid dates, and non-Date values.\n *\n * @param date - Value to check\n * @returns `true` if valid Date object, `false` otherwise\n *\n * @example\n * ```typescript\n * isValidDate(new Date()); // true\n * isValidDate(new Date('invalid')); // false\n * isValidDate(null); // false\n * isValidDate(undefined); // false\n * isValidDate('2024-01-01'); // false (string, not Date)\n * isValidDate(1704067200000); // false (number, not Date)\n * ```\n */\nexport function isValidDate(date: unknown): date is Date {\n return date instanceof Date && !Number.isNaN(date.getTime());\n}\n\n/**\n * Check if a date falls on a weekend (Saturday or Sunday).\n *\n * Note: This only checks Saturday/Sunday and does not account for\n * industry-specific Saturday work schedules.\n *\n * @param date - Date object to check\n * @returns `true` if Saturday or Sunday, `false` otherwise\n *\n * @example\n * ```typescript\n * isWeekend(new Date('2026-01-03')); // true (Saturday)\n * isWeekend(new Date('2026-01-04')); // true (Sunday)\n * isWeekend(new Date('2026-01-05')); // false (Monday)\n * ```\n */\nexport function isWeekend(date: Date): boolean {\n const day = date.getDay();\n return day === 0 || day === 6; // Sunday = 0, Saturday = 6\n}\n\n/**\n * Check if a date falls on a working day (Monday-Friday).\n *\n * Note: This only checks Monday-Friday and does not account for\n * national holidays (holiday lists require periodic updates and\n * are not included per project mandates).\n *\n * @param date - Date object to check\n * @returns `true` if Monday-Friday, `false` otherwise\n *\n * @example\n * ```typescript\n * isWorkingDay(new Date('2026-01-05')); // true (Monday)\n * isWorkingDay(new Date('2026-01-03')); // false (Saturday)\n * isWorkingDay(new Date('2026-01-04')); // false (Sunday)\n * ```\n */\nexport function isWorkingDay(date: Date): boolean {\n const day = date.getDay();\n return day >= 1 && day <= 5; // Monday = 1 to Friday = 5\n}\n\n/**\n * Normalize various date input types to a Date object.\n *\n * @param date - Date input (Date, string, or number timestamp)\n * @returns Date object\n * @throws {InvalidDateError} If the input cannot be parsed to a valid date\n */\nfunction normalizeDate(date: Date | string | number): Date {\n let result: Date;\n\n if (date instanceof Date) {\n result = date;\n } else if (typeof date === 'number') {\n result = new Date(date);\n } else if (typeof date === 'string') {\n result = new Date(date);\n } else {\n throw new InvalidDateError('Date must be a Date, string, or number');\n }\n\n if (Number.isNaN(result.getTime())) {\n throw new InvalidDateError(`Unable to parse date: ${String(date)}`);\n }\n\n return result;\n}\n\n/**\n * Calculate age from a birth date.\n *\n * Accounts for leap years and month length variations.\n * Can return as an object { years, months, days } or as a formatted string.\n *\n * @param birthDate - Birth date (Date, string, or number timestamp)\n * @param options - Options for age calculation\n * @returns Age as object or formatted string (based on asString option)\n * @throws {InvalidDateError} If birthDate or fromDate is invalid\n *\n * @example\n * ```typescript\n * // Get age as object\n * getAge('1990-06-15'); // { years: 36, months: 9, days: 21 }\n * getAge(new Date('1990-06-15'), { fromDate: new Date('2024-06-15') });\n * // { years: 34, months: 0, days: 0 }\n *\n * // Get age as string\n * getAge('1990-06-15', { asString: true });\n * // '36 Tahun 9 Bulan 21 Hari'\n *\n * getAge(new Date('2020-01-01'), { fromDate: new Date('2020-01-15'), asString: true });\n * // '15 Hari'\n * ```\n */\nexport function getAge(\n birthDate: Date | string | number,\n options: { fromDate?: Date | string | number; asString?: boolean } = {}\n): { years: number; months: number; days: number } | string {\n const birth = normalizeDate(birthDate);\n const from = options.fromDate ? normalizeDate(options.fromDate) : new Date();\n\n // Validate: birth date cannot be in the future\n if (birth.getTime() > from.getTime()) {\n throw new InvalidDateError(\n 'Birth date cannot be in the future relative to fromDate'\n );\n }\n\n let years = from.getFullYear() - birth.getFullYear();\n let months = from.getMonth() - birth.getMonth();\n let days = from.getDate() - birth.getDate();\n\n // Adjust for negative days\n if (days < 0) {\n months--;\n // Get days in previous month\n const prevMonth = from.getMonth() === 0 ? 11 : from.getMonth() - 1;\n const prevMonthYear =\n from.getMonth() === 0 ? from.getFullYear() - 1 : from.getFullYear();\n days += daysInMonth(prevMonth + 1, prevMonthYear);\n }\n\n // Adjust for negative months\n if (months < 0) {\n years--;\n months += 12;\n }\n\n const result = { years, months, days };\n\n if (options.asString) {\n return formatAgeString(result);\n }\n\n return result;\n}\n\n/**\n * Format age object as Indonesian string.\n * Omits zero components.\n *\n * @param age - Age object with years, months, days\n * @returns Formatted age string\n */\nfunction formatAgeString(age: {\n years: number;\n months: number;\n days: number;\n}): string {\n const parts: string[] = [];\n\n if (age.years > 0) {\n parts.push(`${age.years} Tahun`);\n }\n\n if (age.months > 0) {\n parts.push(`${age.months} Bulan`);\n }\n\n if (age.days > 0) {\n parts.push(`${age.days} Hari`);\n }\n\n // Handle edge case: all zeros (same day)\n if (parts.length === 0) {\n return '0 Hari';\n }\n\n return parts.join(' ');\n}\n","/**\n * Date parsing utilities for Indonesian formats\n *\n * @module datetime/parse\n * @packageDocumentation\n */\n\nimport { daysInMonth } from './calc';\n\n/**\n * Parse a date string in Indonesian format (DD-MM-YYYY) or ISO format (YYYY-MM-DD).\n *\n * Strict parsing rules:\n * - Accepts delimiters: `/`, `-`, `.`\n * - DD-MM-YYYY format: Day first (1-31), 4-digit year required\n * - ISO auto-detection: If first segment is 4 digits AND > 31, treated as YYYY-MM-DD\n * - Leap year validation: Feb 29 is only valid in leap years\n * - 2-digit years NOT supported\n * - Time components NOT supported\n *\n * @param dateStr - Date string to parse\n * @returns Date object if valid, `null` if invalid\n *\n * @example\n * ```typescript\n * // Indonesian format (DD-MM-YYYY)\n * parseDate('02-01-2026'); // Date(2026, 0, 2) - Jan 2, 2026\n * parseDate('02/01/2026'); // Date(2026, 0, 2)\n * parseDate('02.01.2026'); // Date(2026, 0, 2)\n *\n * // ISO format auto-detected (YYYY-MM-DD)\n * parseDate('2026-01-02'); // Date(2026, 0, 2)\n *\n * // Invalid inputs return null\n * parseDate('29-02-2023'); // null (not a leap year)\n * parseDate('02-01-26'); // null (2-digit year)\n * parseDate('02-01-2026 14:30'); // null (time component)\n * parseDate('invalid'); // null\n * ```\n */\nexport function parseDate(dateStr: string): Date | null {\n // Trim whitespace\n const trimmed = dateStr.trim();\n\n // Reject if empty\n if (!trimmed) {\n return null;\n }\n\n // Reject if contains time component (has space or colon)\n if (trimmed.includes(' ') || trimmed.includes(':')) {\n return null;\n }\n\n // Split by supported delimiters: / - .\n const parts = trimmed.split(/[-/.]/);\n\n // Must have exactly 3 parts\n if (parts.length !== 3) {\n return null;\n }\n\n // Parse all segments as integers\n const nums = parts.map((p) => parseInt(p, 10));\n\n // Check for NaN or negative values\n if (nums.some((n) => Number.isNaN(n) || n < 0)) {\n return null;\n }\n\n // Check for 2-digit years (any segment < 100 after the first position)\n // First segment can be < 100 (day) or > 31 (year in ISO format)\n const [first, second, third] = nums;\n\n // Determine format: ISO (YYYY-MM-DD) or Indonesian (DD-MM-YYYY)\n let day: number;\n let month: number;\n let year: number;\n\n // ISO format detection: first segment is 4 digits AND > 31 (must be a year)\n if (first > 999 && first > 31) {\n // ISO format: YYYY-MM-DD\n year = first;\n month = second;\n day = third;\n } else {\n // Indonesian format: DD-MM-YYYY\n // Validate that third segment is 4 digits (year)\n if (third < 1000) {\n return null; // 2-digit year not supported\n }\n\n day = first;\n month = second;\n year = third;\n }\n\n // Validate month range (1-12)\n if (month < 1 || month > 12) {\n return null;\n }\n\n // Validate year is reasonable (positive and 4 digits)\n if (year < 1000 || year > 9999) {\n return null;\n }\n\n // Validate day against month length\n const maxDays = daysInMonth(month, year);\n if (maxDays === 0 || day < 1 || day > maxDays) {\n return null;\n }\n\n // Create date (month is 0-indexed in JS Date)\n const date = new Date(year, month - 1, day);\n\n // Final validation: ensure the date was created correctly\n // (catches edge cases like Feb 30 being converted to Mar 2)\n if (\n date.getFullYear() !== year ||\n date.getMonth() !== month - 1 ||\n date.getDate() !== day\n ) {\n return null;\n }\n\n return date;\n}\n","/**\n * Date formatting utilities for Indonesian locale\n *\n * @module datetime/format\n * @packageDocumentation\n */\n\nimport {\n InvalidDateError,\n InvalidDateRangeError,\n type DateStyle,\n} from './types';\nimport { MONTH_NAMES, MONTH_NAMES_SHORT, DAY_NAMES } from './constants';\n\n/**\n * Normalize various date input types to a Date object.\n *\n * @param date - Date input (Date, string, or number timestamp)\n * @returns Date object\n * @throws {InvalidDateError} If the input cannot be parsed to a valid date\n */\nfunction normalizeDate(date: Date | string | number): Date {\n let result: Date;\n\n if (date instanceof Date) {\n result = date;\n } else if (typeof date === 'number') {\n // Assume milliseconds timestamp\n result = new Date(date);\n } else if (typeof date === 'string') {\n result = new Date(date);\n } else {\n throw new InvalidDateError('Date must be a Date, string, or number');\n }\n\n // Validate the result\n if (Number.isNaN(result.getTime())) {\n throw new InvalidDateError(`Unable to parse date: ${String(date)}`);\n }\n\n return result;\n}\n\n/**\n * Format a date with Indonesian locale.\n *\n * @param date - Date to format (Date, string, or number timestamp in milliseconds)\n * @param style - Formatting style (default: 'long')\n * @returns Formatted date string\n * @throws {InvalidDateError} If the date is invalid\n *\n * @example\n * ```typescript\n * formatDate(new Date('2026-01-02'), 'full'); // 'Jumat, 2 Januari 2026'\n * formatDate(new Date('2026-01-02'), 'long'); // '2 Januari 2026'\n * formatDate(new Date('2026-01-02'), 'medium'); // '2 Jan 2026'\n * formatDate(new Date('2026-01-02'), 'short'); // '02/01/2026'\n * formatDate(new Date('2026-01-02'), 'weekday'); // 'Jumat'\n * formatDate(new Date('2026-01-02'), 'month'); // 'Januari'\n * ```\n */\nexport function formatDate(\n date: Date | string | number,\n style: DateStyle = 'long'\n): string {\n const d = normalizeDate(date);\n\n const day = d.getDate();\n const month = d.getMonth() + 1; // 1-indexed\n const year = d.getFullYear();\n const dayOfWeek = d.getDay(); // 0 = Sunday\n\n switch (style) {\n case 'full':\n return `${DAY_NAMES[dayOfWeek]}, ${day} ${MONTH_NAMES[month]} ${year}`;\n\n case 'long':\n return `${day} ${MONTH_NAMES[month]} ${year}`;\n\n case 'medium':\n return `${day} ${MONTH_NAMES_SHORT[month]} ${year}`;\n\n case 'short': {\n const dd = String(day).padStart(2, '0');\n const mm = String(month).padStart(2, '0');\n return `${dd}/${mm}/${year}`;\n }\n\n case 'weekday':\n return DAY_NAMES[dayOfWeek];\n\n case 'month':\n return MONTH_NAMES[month];\n\n default:\n throw new InvalidDateError(`Unknown format style: ${style}`);\n }\n}\n\n/**\n * Format a date range with Indonesian locale and smart redundancy removal.\n *\n * Removes redundant month/year information when dates share them.\n *\n * @param start - Start date\n * @param end - End date\n * @param style - Formatting style (default: 'long')\n * @returns Formatted date range string\n * @throws {InvalidDateError} If either date is invalid\n * @throws {InvalidDateRangeError} If end date is before start date\n *\n * @example\n * ```typescript\n * // Same day\n * formatDateRange(\n * new Date('2026-01-02'),\n * new Date('2026-01-02')\n * ); // '2 Januari 2026'\n *\n * // Same month & year\n * formatDateRange(\n * new Date('2026-01-02'),\n * new Date('2026-01-05')\n * ); // '2 - 5 Januari 2026'\n *\n * // Same year\n * formatDateRange(\n * new Date('2026-01-30'),\n * new Date('2026-02-02')\n * ); // '30 Januari - 2 Februari 2026'\n *\n * // Different year\n * formatDateRange(\n * new Date('2025-12-30'),\n * new Date('2026-01-02')\n * ); // '30 Desember 2025 - 2 Januari 2026'\n * ```\n */\nexport function formatDateRange(\n start: Date,\n end: Date,\n style: Exclude<DateStyle, 'weekday' | 'month'> = 'long'\n): string {\n const s = normalizeDate(start);\n const e = normalizeDate(end);\n\n // Validate range\n if (e.getTime() < s.getTime()) {\n throw new InvalidDateRangeError();\n }\n\n // Short style: no redundancy removal, always full format\n if (style === 'short') {\n return `${formatDate(s, 'short')} - ${formatDate(e, 'short')}`;\n }\n\n // Full style: full weekday + date for both\n if (style === 'full') {\n return `${formatDate(s, 'full')} - ${formatDate(e, 'full')}`;\n }\n\n // Extract components\n const sDay = s.getDate();\n const eDay = e.getDate();\n const sMonth = s.getMonth() + 1;\n const eMonth = e.getMonth() + 1;\n const sYear = s.getFullYear();\n const eYear = e.getFullYear();\n\n // Same day: show single date\n if (sDay === eDay && sMonth === eMonth && sYear === eYear) {\n return formatDate(s, style);\n }\n\n // Different year: full format for both\n if (sYear !== eYear) {\n return `${formatDate(s, style)} - ${formatDate(e, style)}`;\n }\n\n // Same year, different month: omit year from start date\n if (sMonth !== eMonth) {\n if (style === 'long') {\n return `${sDay} ${MONTH_NAMES[sMonth]} - ${eDay} ${MONTH_NAMES[eMonth]} ${eYear}`;\n }\n // medium style\n return `${sDay} ${MONTH_NAMES_SHORT[sMonth]} - ${eDay} ${MONTH_NAMES_SHORT[eMonth]} ${eYear}`;\n }\n\n // Same month & year: show range with single month/year\n if (style === 'long') {\n return `${sDay} - ${eDay} ${MONTH_NAMES[eMonth]} ${eYear}`;\n }\n // medium style\n return `${sDay} - ${eDay} ${MONTH_NAMES_SHORT[eMonth]} ${eYear}`;\n}\n","/**\n * Relative time formatting for Indonesian locale\n *\n * @module datetime/relative\n * @packageDocumentation\n */\n\nimport { InvalidDateError } from './types';\nimport { formatDate } from './format';\n\n/**\n * Normalize various date input types to a Date object.\n *\n * @param date - Date input (Date, string, or number timestamp)\n * @returns Date object\n * @throws {InvalidDateError} If the input cannot be parsed to a valid date\n */\nfunction normalizeDate(date: Date | string | number): Date {\n let result: Date;\n\n if (date instanceof Date) {\n result = date;\n } else if (typeof date === 'number') {\n result = new Date(date);\n } else if (typeof date === 'string') {\n result = new Date(date);\n } else {\n throw new InvalidDateError('Date must be a Date, string, or number');\n }\n\n if (Number.isNaN(result.getTime())) {\n throw new InvalidDateError(`Unable to parse date: ${String(date)}`);\n }\n\n return result;\n}\n\n/**\n * Format a date as relative time in Indonesian.\n *\n * Returns human-readable relative time like \"Baru saja\", \"X menit yang lalu\",\n * \"Kemarin\", or falls back to formatted date for older dates.\n *\n * @param date - Date to format (Date, string, or number timestamp in milliseconds)\n * @param baseDate - Reference date for comparison (default: current date)\n * @returns Relative time string in Indonesian\n * @throws {InvalidDateError} If either date is invalid\n *\n * @example\n * ```typescript\n * // Assuming today is 2026-01-02 12:00:00\n * toRelativeTime(new Date('2026-01-02 11:59:00')); // 'Baru saja'\n * toRelativeTime(new Date('2026-01-02 11:00:00')); // '1 jam yang lalu'\n * toRelativeTime(new Date('2026-01-01 12:00:00')); // 'Kemarin'\n * toRelativeTime(new Date('2025-12-30 12:00:00')); // '3 hari yang lalu'\n * toRelativeTime(new Date('2025-12-01 12:00:00')); // '1 Desember 2025'\n * ```\n */\nexport function toRelativeTime(\n date: Date | string | number,\n baseDate: Date = new Date()\n): string {\n const d = normalizeDate(date);\n const base = normalizeDate(baseDate);\n\n const diffMs = d.getTime() - base.getTime();\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffMs / (1000 * 60));\n const diffHour = Math.floor(diffMs / (1000 * 60 * 60));\n const diffDay = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\n // Exact match\n if (diffMs === 0) {\n return 'Sekarang';\n }\n\n // Future dates\n if (diffMs > 0) {\n // < 1 minute\n if (diffSec < 60) {\n return 'Baru saja';\n }\n\n // < 60 minutes\n if (diffMin < 60) {\n return `${diffMin} menit lagi`;\n }\n\n // < 24 hours\n if (diffHour < 24) {\n return `${diffHour} jam lagi`;\n }\n\n // 1 day (24-48 hours)\n if (diffHour < 48) {\n return 'Besok';\n }\n\n // 2-30 days\n if (diffDay <= 30) {\n return `${diffDay} hari lagi`;\n }\n\n // > 30 days: fallback to formatted date\n return formatDate(d, 'long');\n }\n\n // Past dates\n const absDiffSec = Math.abs(diffSec);\n const absDiffMin = Math.abs(diffMin);\n const absDiffHour = Math.abs(diffHour);\n const absDiffDay = Math.abs(diffDay);\n\n // < 1 minute\n if (absDiffSec < 60) {\n return 'Baru saja';\n }\n\n // < 60 minutes\n if (absDiffMin < 60) {\n return `${absDiffMin} menit yang lalu`;\n }\n\n // < 24 hours\n if (absDiffHour < 24) {\n return `${absDiffHour} jam yang lalu`;\n }\n\n // 1 day (24-48 hours)\n if (absDiffHour < 48) {\n return 'Kemarin';\n }\n\n // 2-30 days\n if (absDiffDay <= 30) {\n return `${absDiffDay} hari yang lalu`;\n }\n\n // > 30 days: fallback to formatted date\n return formatDate(d, 'long');\n}\n","/**\n * Timezone utilities for Indonesian locale\n *\n * @module datetime/timezone\n * @packageDocumentation\n */\n\nimport { TIMEZONE_MAP } from './constants';\n\n/**\n * Map IANA timezone names or UTC offsets to Indonesian abbreviations (WIB/WITA/WIT).\n *\n * Supported mappings:\n * - UTC+7 / Asia/Jakarta / Asia/Pontianak → \"WIB\"\n * - UTC+8 / Asia/Makassar / Asia/Denpasar → \"WITA\"\n * - UTC+9 / Asia/Jayapura → \"WIT\"\n *\n * @param input - IANA timezone name (case-sensitive), offset in hours, or offset string\n * @returns Indonesian timezone abbreviation or null if not Indonesian\n *\n * @example\n * ```typescript\n * // IANA timezone names\n * getIndonesianTimezone('Asia/Jakarta'); // 'WIB'\n * getIndonesianTimezone('Asia/Makassar'); // 'WITA'\n * getIndonesianTimezone('Asia/Jayapura'); // 'WIT'\n *\n * // Offset as number (hours)\n * getIndonesianTimezone(7); // 'WIB'\n * getIndonesianTimezone(8); // 'WITA'\n * getIndonesianTimezone(9); // 'WIT'\n *\n * // Offset as string\n * getIndonesianTimezone('+07:00'); // 'WIB'\n * getIndonesianTimezone('+0700'); // 'WIB'\n * getIndonesianTimezone('+08:00'); // 'WITA'\n *\n * // Non-Indonesian returns null\n * getIndonesianTimezone('America/New_York'); // null\n * getIndonesianTimezone(-5); // null\n * ```\n */\nexport function getIndonesianTimezone(\n input: string | number\n): 'WIB' | 'WITA' | 'WIT' | null {\n // Handle number input (offset in hours)\n if (typeof input === 'number') {\n if (!Number.isFinite(input) || !Number.isInteger(input)) {\n return null;\n }\n\n switch (input) {\n case 7:\n return 'WIB';\n case 8:\n return 'WITA';\n case 9:\n return 'WIT';\n default:\n return null;\n }\n }\n\n // Handle string input\n if (typeof input !== 'string') {\n return null;\n }\n\n const trimmed = input.trim();\n\n // Check if it's an offset string (+07:00, +0700, -05:00, etc.)\n const offsetMatch = trimmed.match(/^([+-])(\\d{2}):?(\\d{2})$/);\n if (offsetMatch) {\n const sign = offsetMatch[1];\n const hours = parseInt(offsetMatch[2], 10);\n const minutes = parseInt(offsetMatch[3], 10);\n\n // Indonesian timezones are all positive (UTC+)\n if (sign === '-') {\n return null;\n }\n\n // Validate format (must be exactly on the hour, no minutes)\n if (minutes !== 0) {\n return null;\n }\n\n // Map to Indonesian timezone\n switch (hours) {\n case 7:\n return 'WIB';\n case 8:\n return 'WITA';\n case 9:\n return 'WIT';\n default:\n return null;\n }\n }\n\n // Check if it's an IANA timezone name\n if (TIMEZONE_MAP[trimmed]) {\n return TIMEZONE_MAP[trimmed];\n }\n\n // Unknown timezone\n return null;\n}\n"]}