@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,412 @@
1
+ /**
2
+ * Custom error classes for datetime module
3
+ *
4
+ * @module datetime/types
5
+ * @packageDocumentation
6
+ */
7
+ /**
8
+ * Error thrown when an invalid date is provided to a function.
9
+ * Extends native Error with a `code` property for programmatic error handling.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * try {
14
+ * formatDate('invalid');
15
+ * } catch (error) {
16
+ * if (error instanceof InvalidDateError) {
17
+ * console.log(error.code); // 'INVALID_DATE'
18
+ * }
19
+ * }
20
+ * ```
21
+ */
22
+ declare class InvalidDateError extends Error {
23
+ /** Error code for programmatic identification */
24
+ readonly code: "INVALID_DATE";
25
+ constructor(message?: string);
26
+ }
27
+ /**
28
+ * Error thrown when an invalid date range is provided.
29
+ * Extends native Error with a `code` property for programmatic error handling.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * try {
34
+ * formatDateRange(new Date('2026-01-05'), new Date('2026-01-01'));
35
+ * } catch (error) {
36
+ * if (error instanceof InvalidDateRangeError) {
37
+ * console.log(error.code); // 'INVALID_DATE_RANGE'
38
+ * }
39
+ * }
40
+ * ```
41
+ */
42
+ declare class InvalidDateRangeError extends Error {
43
+ /** Error code for programmatic identification */
44
+ readonly code: "INVALID_DATE_RANGE";
45
+ constructor(message?: string);
46
+ }
47
+ /**
48
+ * Date formatting style options
49
+ */
50
+ type DateStyle = 'full' | 'long' | 'medium' | 'short' | 'weekday' | 'month';
51
+ /**
52
+ * Options for getAge function
53
+ */
54
+ interface AgeOptions {
55
+ /**
56
+ * Reference date to calculate age from.
57
+ * Defaults to current date at function call time.
58
+ * @defaultValue new Date()
59
+ */
60
+ fromDate?: Date | string | number;
61
+ /**
62
+ * Return age as formatted string instead of object.
63
+ * @defaultValue false
64
+ */
65
+ asString?: boolean;
66
+ }
67
+ /**
68
+ * Age calculation result object
69
+ */
70
+ interface AgeResult {
71
+ /** Full years */
72
+ years: number;
73
+ /** Remaining months (0-11) */
74
+ months: number;
75
+ /** Remaining days (0-30) */
76
+ days: number;
77
+ }
78
+
79
+ /**
80
+ * Constants for Indonesian datetime formatting
81
+ *
82
+ * @module datetime/constants
83
+ * @packageDocumentation
84
+ */
85
+ /** Full Indonesian month names (1-indexed: index 0 = empty, 1 = Januari) */
86
+ declare const MONTH_NAMES: readonly string[];
87
+ /** Short Indonesian month names (3-letter abbreviation) */
88
+ declare const MONTH_NAMES_SHORT: readonly string[];
89
+ /** Full Indonesian day names */
90
+ declare const DAY_NAMES: readonly string[];
91
+ /** Short Indonesian day names (3-letter abbreviation) */
92
+ declare const DAY_NAMES_SHORT: readonly string[];
93
+ /** Mapping of IANA timezone names to Indonesian abbreviations */
94
+ declare const TIMEZONE_MAP: Readonly<Record<string, 'WIB' | 'WITA' | 'WIT'>>;
95
+ /** Valid UTC offset hours that map to Indonesian timezones */
96
+ declare const VALID_UTC_OFFSETS: readonly number[];
97
+
98
+ /**
99
+ * Date calculation utilities
100
+ *
101
+ * @module datetime/calc
102
+ * @packageDocumentation
103
+ */
104
+ /**
105
+ * Check if a year is a leap year.
106
+ *
107
+ * A year is a leap year if:
108
+ * - Divisible by 4, but not by 100, OR
109
+ * - Divisible by 400
110
+ *
111
+ * @param year - The year to check
112
+ * @returns `true` if leap year, `false` otherwise (including invalid inputs)
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * isLeapYear(2024); // true
117
+ * isLeapYear(2023); // false
118
+ * isLeapYear(1900); // false (divisible by 100 but not 400)
119
+ * isLeapYear(2000); // true (divisible by 400)
120
+ * isLeapYear(NaN); // false
121
+ * isLeapYear(3.5); // false (non-integer)
122
+ * ```
123
+ */
124
+ declare function isLeapYear(year: number): boolean;
125
+ /**
126
+ * Get the number of days in a month.
127
+ *
128
+ * Accounts for leap years in February.
129
+ *
130
+ * @param month - Month number (1-12, 1-indexed)
131
+ * @param year - Full year (e.g., 2026)
132
+ * @returns Number of days in the month, or 0 for invalid inputs
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * daysInMonth(1, 2026); // 31 (January)
137
+ * daysInMonth(2, 2024); // 29 (February, leap year)
138
+ * daysInMonth(2, 2023); // 28 (February, non-leap year)
139
+ * daysInMonth(4, 2026); // 30 (April)
140
+ * daysInMonth(13, 2026); // 0 (invalid month)
141
+ * daysInMonth(2, NaN); // 0 (invalid year)
142
+ * ```
143
+ */
144
+ declare function daysInMonth(month: number, year: number): number;
145
+ /**
146
+ * Type guard to check if a value is a valid Date object.
147
+ *
148
+ * Returns `true` only for Date instances that represent a valid date
149
+ * (i.e., not `Invalid Date`). Returns `false` for null, undefined,
150
+ * invalid dates, and non-Date values.
151
+ *
152
+ * @param date - Value to check
153
+ * @returns `true` if valid Date object, `false` otherwise
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * isValidDate(new Date()); // true
158
+ * isValidDate(new Date('invalid')); // false
159
+ * isValidDate(null); // false
160
+ * isValidDate(undefined); // false
161
+ * isValidDate('2024-01-01'); // false (string, not Date)
162
+ * isValidDate(1704067200000); // false (number, not Date)
163
+ * ```
164
+ */
165
+ declare function isValidDate(date: unknown): date is Date;
166
+ /**
167
+ * Check if a date falls on a weekend (Saturday or Sunday).
168
+ *
169
+ * Note: This only checks Saturday/Sunday and does not account for
170
+ * industry-specific Saturday work schedules.
171
+ *
172
+ * @param date - Date object to check
173
+ * @returns `true` if Saturday or Sunday, `false` otherwise
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * isWeekend(new Date('2026-01-03')); // true (Saturday)
178
+ * isWeekend(new Date('2026-01-04')); // true (Sunday)
179
+ * isWeekend(new Date('2026-01-05')); // false (Monday)
180
+ * ```
181
+ */
182
+ declare function isWeekend(date: Date): boolean;
183
+ /**
184
+ * Check if a date falls on a working day (Monday-Friday).
185
+ *
186
+ * Note: This only checks Monday-Friday and does not account for
187
+ * national holidays (holiday lists require periodic updates and
188
+ * are not included per project mandates).
189
+ *
190
+ * @param date - Date object to check
191
+ * @returns `true` if Monday-Friday, `false` otherwise
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * isWorkingDay(new Date('2026-01-05')); // true (Monday)
196
+ * isWorkingDay(new Date('2026-01-03')); // false (Saturday)
197
+ * isWorkingDay(new Date('2026-01-04')); // false (Sunday)
198
+ * ```
199
+ */
200
+ declare function isWorkingDay(date: Date): boolean;
201
+ /**
202
+ * Calculate age from a birth date.
203
+ *
204
+ * Accounts for leap years and month length variations.
205
+ * Can return as an object { years, months, days } or as a formatted string.
206
+ *
207
+ * @param birthDate - Birth date (Date, string, or number timestamp)
208
+ * @param options - Options for age calculation
209
+ * @returns Age as object or formatted string (based on asString option)
210
+ * @throws {InvalidDateError} If birthDate or fromDate is invalid
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // Get age as object
215
+ * getAge('1990-06-15'); // { years: 36, months: 9, days: 21 }
216
+ * getAge(new Date('1990-06-15'), { fromDate: new Date('2024-06-15') });
217
+ * // { years: 34, months: 0, days: 0 }
218
+ *
219
+ * // Get age as string
220
+ * getAge('1990-06-15', { asString: true });
221
+ * // '36 Tahun 9 Bulan 21 Hari'
222
+ *
223
+ * getAge(new Date('2020-01-01'), { fromDate: new Date('2020-01-15'), asString: true });
224
+ * // '15 Hari'
225
+ * ```
226
+ */
227
+ declare function getAge(birthDate: Date | string | number, options?: {
228
+ fromDate?: Date | string | number;
229
+ asString?: boolean;
230
+ }): {
231
+ years: number;
232
+ months: number;
233
+ days: number;
234
+ } | string;
235
+
236
+ /**
237
+ * Date parsing utilities for Indonesian formats
238
+ *
239
+ * @module datetime/parse
240
+ * @packageDocumentation
241
+ */
242
+ /**
243
+ * Parse a date string in Indonesian format (DD-MM-YYYY) or ISO format (YYYY-MM-DD).
244
+ *
245
+ * Strict parsing rules:
246
+ * - Accepts delimiters: `/`, `-`, `.`
247
+ * - DD-MM-YYYY format: Day first (1-31), 4-digit year required
248
+ * - ISO auto-detection: If first segment is 4 digits AND > 31, treated as YYYY-MM-DD
249
+ * - Leap year validation: Feb 29 is only valid in leap years
250
+ * - 2-digit years NOT supported
251
+ * - Time components NOT supported
252
+ *
253
+ * @param dateStr - Date string to parse
254
+ * @returns Date object if valid, `null` if invalid
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * // Indonesian format (DD-MM-YYYY)
259
+ * parseDate('02-01-2026'); // Date(2026, 0, 2) - Jan 2, 2026
260
+ * parseDate('02/01/2026'); // Date(2026, 0, 2)
261
+ * parseDate('02.01.2026'); // Date(2026, 0, 2)
262
+ *
263
+ * // ISO format auto-detected (YYYY-MM-DD)
264
+ * parseDate('2026-01-02'); // Date(2026, 0, 2)
265
+ *
266
+ * // Invalid inputs return null
267
+ * parseDate('29-02-2023'); // null (not a leap year)
268
+ * parseDate('02-01-26'); // null (2-digit year)
269
+ * parseDate('02-01-2026 14:30'); // null (time component)
270
+ * parseDate('invalid'); // null
271
+ * ```
272
+ */
273
+ declare function parseDate(dateStr: string): Date | null;
274
+
275
+ /**
276
+ * Date formatting utilities for Indonesian locale
277
+ *
278
+ * @module datetime/format
279
+ * @packageDocumentation
280
+ */
281
+
282
+ /**
283
+ * Format a date with Indonesian locale.
284
+ *
285
+ * @param date - Date to format (Date, string, or number timestamp in milliseconds)
286
+ * @param style - Formatting style (default: 'long')
287
+ * @returns Formatted date string
288
+ * @throws {InvalidDateError} If the date is invalid
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * formatDate(new Date('2026-01-02'), 'full'); // 'Jumat, 2 Januari 2026'
293
+ * formatDate(new Date('2026-01-02'), 'long'); // '2 Januari 2026'
294
+ * formatDate(new Date('2026-01-02'), 'medium'); // '2 Jan 2026'
295
+ * formatDate(new Date('2026-01-02'), 'short'); // '02/01/2026'
296
+ * formatDate(new Date('2026-01-02'), 'weekday'); // 'Jumat'
297
+ * formatDate(new Date('2026-01-02'), 'month'); // 'Januari'
298
+ * ```
299
+ */
300
+ declare function formatDate(date: Date | string | number, style?: DateStyle): string;
301
+ /**
302
+ * Format a date range with Indonesian locale and smart redundancy removal.
303
+ *
304
+ * Removes redundant month/year information when dates share them.
305
+ *
306
+ * @param start - Start date
307
+ * @param end - End date
308
+ * @param style - Formatting style (default: 'long')
309
+ * @returns Formatted date range string
310
+ * @throws {InvalidDateError} If either date is invalid
311
+ * @throws {InvalidDateRangeError} If end date is before start date
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * // Same day
316
+ * formatDateRange(
317
+ * new Date('2026-01-02'),
318
+ * new Date('2026-01-02')
319
+ * ); // '2 Januari 2026'
320
+ *
321
+ * // Same month & year
322
+ * formatDateRange(
323
+ * new Date('2026-01-02'),
324
+ * new Date('2026-01-05')
325
+ * ); // '2 - 5 Januari 2026'
326
+ *
327
+ * // Same year
328
+ * formatDateRange(
329
+ * new Date('2026-01-30'),
330
+ * new Date('2026-02-02')
331
+ * ); // '30 Januari - 2 Februari 2026'
332
+ *
333
+ * // Different year
334
+ * formatDateRange(
335
+ * new Date('2025-12-30'),
336
+ * new Date('2026-01-02')
337
+ * ); // '30 Desember 2025 - 2 Januari 2026'
338
+ * ```
339
+ */
340
+ declare function formatDateRange(start: Date, end: Date, style?: Exclude<DateStyle, 'weekday' | 'month'>): string;
341
+
342
+ /**
343
+ * Relative time formatting for Indonesian locale
344
+ *
345
+ * @module datetime/relative
346
+ * @packageDocumentation
347
+ */
348
+ /**
349
+ * Format a date as relative time in Indonesian.
350
+ *
351
+ * Returns human-readable relative time like "Baru saja", "X menit yang lalu",
352
+ * "Kemarin", or falls back to formatted date for older dates.
353
+ *
354
+ * @param date - Date to format (Date, string, or number timestamp in milliseconds)
355
+ * @param baseDate - Reference date for comparison (default: current date)
356
+ * @returns Relative time string in Indonesian
357
+ * @throws {InvalidDateError} If either date is invalid
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * // Assuming today is 2026-01-02 12:00:00
362
+ * toRelativeTime(new Date('2026-01-02 11:59:00')); // 'Baru saja'
363
+ * toRelativeTime(new Date('2026-01-02 11:00:00')); // '1 jam yang lalu'
364
+ * toRelativeTime(new Date('2026-01-01 12:00:00')); // 'Kemarin'
365
+ * toRelativeTime(new Date('2025-12-30 12:00:00')); // '3 hari yang lalu'
366
+ * toRelativeTime(new Date('2025-12-01 12:00:00')); // '1 Desember 2025'
367
+ * ```
368
+ */
369
+ declare function toRelativeTime(date: Date | string | number, baseDate?: Date): string;
370
+
371
+ /**
372
+ * Timezone utilities for Indonesian locale
373
+ *
374
+ * @module datetime/timezone
375
+ * @packageDocumentation
376
+ */
377
+ /**
378
+ * Map IANA timezone names or UTC offsets to Indonesian abbreviations (WIB/WITA/WIT).
379
+ *
380
+ * Supported mappings:
381
+ * - UTC+7 / Asia/Jakarta / Asia/Pontianak → "WIB"
382
+ * - UTC+8 / Asia/Makassar / Asia/Denpasar → "WITA"
383
+ * - UTC+9 / Asia/Jayapura → "WIT"
384
+ *
385
+ * @param input - IANA timezone name (case-sensitive), offset in hours, or offset string
386
+ * @returns Indonesian timezone abbreviation or null if not Indonesian
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * // IANA timezone names
391
+ * getIndonesianTimezone('Asia/Jakarta'); // 'WIB'
392
+ * getIndonesianTimezone('Asia/Makassar'); // 'WITA'
393
+ * getIndonesianTimezone('Asia/Jayapura'); // 'WIT'
394
+ *
395
+ * // Offset as number (hours)
396
+ * getIndonesianTimezone(7); // 'WIB'
397
+ * getIndonesianTimezone(8); // 'WITA'
398
+ * getIndonesianTimezone(9); // 'WIT'
399
+ *
400
+ * // Offset as string
401
+ * getIndonesianTimezone('+07:00'); // 'WIB'
402
+ * getIndonesianTimezone('+0700'); // 'WIB'
403
+ * getIndonesianTimezone('+08:00'); // 'WITA'
404
+ *
405
+ * // Non-Indonesian returns null
406
+ * getIndonesianTimezone('America/New_York'); // null
407
+ * getIndonesianTimezone(-5); // null
408
+ * ```
409
+ */
410
+ declare function getIndonesianTimezone(input: string | number): 'WIB' | 'WITA' | 'WIT' | null;
411
+
412
+ export { type AgeOptions, type AgeResult, DAY_NAMES, DAY_NAMES_SHORT, type DateStyle, InvalidDateError, InvalidDateRangeError, MONTH_NAMES, MONTH_NAMES_SHORT, TIMEZONE_MAP, VALID_UTC_OFFSETS, daysInMonth, formatDate, formatDateRange, getAge, getIndonesianTimezone, isLeapYear, isValidDate, isWeekend, isWorkingDay, parseDate, toRelativeTime };