@f-o-t/datetime 0.1.0

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,694 @@
1
+ import { InvalidDateError, PluginError } from "../errors";
2
+ import { DateInputSchema } from "../schemas";
3
+ import type { DateInput, DateTimePlugin, TimeUnit } from "../types";
4
+
5
+ /**
6
+ * Core DateTime class that wraps the native JavaScript Date object
7
+ * Provides immutable operations and extensibility through plugins
8
+ */
9
+ export class DateTime {
10
+ /**
11
+ * Internal date storage
12
+ * @private
13
+ */
14
+ private readonly _date: Date;
15
+
16
+ /**
17
+ * Static registry of installed plugins
18
+ * @private
19
+ */
20
+ private static readonly plugins: Map<string, DateTimePlugin> = new Map();
21
+
22
+ /**
23
+ * Creates a new DateTime instance
24
+ * @param input - Date input (Date, ISO string, timestamp, DateTime, or undefined for current time)
25
+ * @throws {InvalidDateError} When input fails Zod validation
26
+ */
27
+ constructor(input?: DateInput) {
28
+ // If no input provided, use current time
29
+ if (input === undefined) {
30
+ this._date = new Date();
31
+ return;
32
+ }
33
+
34
+ // Special handling for NaN - allow it but create invalid date
35
+ if (typeof input === "number" && Number.isNaN(input)) {
36
+ this._date = new Date(NaN);
37
+ return;
38
+ }
39
+
40
+ // Special handling for Invalid Date objects - allow them through
41
+ if (input instanceof Date && Number.isNaN(input.getTime())) {
42
+ this._date = new Date(input.getTime());
43
+ return;
44
+ }
45
+
46
+ // Validate input with Zod schema
47
+ const validation = DateInputSchema.safeParse(input);
48
+ if (!validation.success) {
49
+ throw new InvalidDateError(
50
+ `Invalid date input: ${validation.error.message}`,
51
+ input,
52
+ );
53
+ }
54
+
55
+ // Convert input to Date
56
+ if (input instanceof Date) {
57
+ // Clone the date to ensure immutability
58
+ this._date = new Date(input.getTime());
59
+ } else if (typeof input === "string") {
60
+ // Parse ISO string or other date string
61
+ // This may result in an Invalid Date, which is allowed
62
+ this._date = new Date(input);
63
+ } else if (typeof input === "number") {
64
+ // Use timestamp
65
+ this._date = new Date(input);
66
+ } else if (this.isDateTimeInstance(input)) {
67
+ // Clone from another DateTime instance
68
+ this._date = new Date(input.valueOf());
69
+ } else {
70
+ // Should never reach here due to Zod validation, but TypeScript needs it
71
+ throw new InvalidDateError("Unsupported date input type", input);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Type guard to check if value is a DateTime instance
77
+ * @private
78
+ */
79
+ private isDateTimeInstance(val: unknown): val is DateTime {
80
+ return (
81
+ val !== null &&
82
+ typeof val === "object" &&
83
+ "toDate" in val &&
84
+ typeof val.toDate === "function"
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Checks if the date is valid
90
+ * @returns true if the date is valid, false otherwise
91
+ */
92
+ public isValid(): boolean {
93
+ return !Number.isNaN(this._date.getTime());
94
+ }
95
+
96
+ /**
97
+ * Returns the native JavaScript Date object (cloned for immutability)
98
+ * @returns A clone of the internal Date object
99
+ */
100
+ public toDate(): Date {
101
+ return new Date(this._date.getTime());
102
+ }
103
+
104
+ /**
105
+ * Returns ISO 8601 string representation
106
+ * @returns ISO 8601 formatted string
107
+ */
108
+ public toISO(): string {
109
+ return this._date.toISOString();
110
+ }
111
+
112
+ /**
113
+ * Returns the Unix timestamp in milliseconds
114
+ * @returns Unix timestamp
115
+ */
116
+ public valueOf(): number {
117
+ return this._date.getTime();
118
+ }
119
+
120
+ // ============================================
121
+ // Arithmetic Methods
122
+ // ============================================
123
+
124
+ /**
125
+ * Adds milliseconds to the date
126
+ * @param amount - Number of milliseconds to add (can be negative)
127
+ * @returns New DateTime instance with added milliseconds
128
+ */
129
+ public addMilliseconds(amount: number): DateTime {
130
+ return new DateTime(this.valueOf() + amount);
131
+ }
132
+
133
+ /**
134
+ * Adds seconds to the date
135
+ * @param amount - Number of seconds to add (can be negative)
136
+ * @returns New DateTime instance with added seconds
137
+ */
138
+ public addSeconds(amount: number): DateTime {
139
+ return this.addMilliseconds(amount * 1000);
140
+ }
141
+
142
+ /**
143
+ * Adds minutes to the date
144
+ * @param amount - Number of minutes to add (can be negative)
145
+ * @returns New DateTime instance with added minutes
146
+ */
147
+ public addMinutes(amount: number): DateTime {
148
+ return this.addMilliseconds(amount * 60 * 1000);
149
+ }
150
+
151
+ /**
152
+ * Adds hours to the date
153
+ * @param amount - Number of hours to add (can be negative)
154
+ * @returns New DateTime instance with added hours
155
+ */
156
+ public addHours(amount: number): DateTime {
157
+ return this.addMilliseconds(amount * 60 * 60 * 1000);
158
+ }
159
+
160
+ /**
161
+ * Adds days to the date
162
+ * @param amount - Number of days to add (can be negative)
163
+ * @returns New DateTime instance with added days
164
+ */
165
+ public addDays(amount: number): DateTime {
166
+ const newDate = this.toDate();
167
+ newDate.setDate(newDate.getDate() + amount);
168
+ return new DateTime(newDate);
169
+ }
170
+
171
+ /**
172
+ * Adds weeks to the date
173
+ * @param amount - Number of weeks to add (can be negative)
174
+ * @returns New DateTime instance with added weeks
175
+ */
176
+ public addWeeks(amount: number): DateTime {
177
+ return this.addDays(amount * 7);
178
+ }
179
+
180
+ /**
181
+ * Adds months to the date
182
+ * @param amount - Number of months to add (can be negative)
183
+ * @returns New DateTime instance with added months
184
+ */
185
+ public addMonths(amount: number): DateTime {
186
+ const newDate = this.toDate();
187
+ newDate.setMonth(newDate.getMonth() + amount);
188
+ return new DateTime(newDate);
189
+ }
190
+
191
+ /**
192
+ * Adds years to the date
193
+ * @param amount - Number of years to add (can be negative)
194
+ * @returns New DateTime instance with added years
195
+ */
196
+ public addYears(amount: number): DateTime {
197
+ const newDate = this.toDate();
198
+ newDate.setFullYear(newDate.getFullYear() + amount);
199
+ return new DateTime(newDate);
200
+ }
201
+
202
+ /**
203
+ * Subtracts milliseconds from the date
204
+ * @param amount - Number of milliseconds to subtract (can be negative)
205
+ * @returns New DateTime instance with subtracted milliseconds
206
+ */
207
+ public subtractMilliseconds(amount: number): DateTime {
208
+ return this.addMilliseconds(-amount);
209
+ }
210
+
211
+ /**
212
+ * Subtracts seconds from the date
213
+ * @param amount - Number of seconds to subtract (can be negative)
214
+ * @returns New DateTime instance with subtracted seconds
215
+ */
216
+ public subtractSeconds(amount: number): DateTime {
217
+ return this.addSeconds(-amount);
218
+ }
219
+
220
+ /**
221
+ * Subtracts minutes from the date
222
+ * @param amount - Number of minutes to subtract (can be negative)
223
+ * @returns New DateTime instance with subtracted minutes
224
+ */
225
+ public subtractMinutes(amount: number): DateTime {
226
+ return this.addMinutes(-amount);
227
+ }
228
+
229
+ /**
230
+ * Subtracts hours from the date
231
+ * @param amount - Number of hours to subtract (can be negative)
232
+ * @returns New DateTime instance with subtracted hours
233
+ */
234
+ public subtractHours(amount: number): DateTime {
235
+ return this.addHours(-amount);
236
+ }
237
+
238
+ /**
239
+ * Subtracts days from the date
240
+ * @param amount - Number of days to subtract (can be negative)
241
+ * @returns New DateTime instance with subtracted days
242
+ */
243
+ public subtractDays(amount: number): DateTime {
244
+ return this.addDays(-amount);
245
+ }
246
+
247
+ /**
248
+ * Subtracts weeks from the date
249
+ * @param amount - Number of weeks to subtract (can be negative)
250
+ * @returns New DateTime instance with subtracted weeks
251
+ */
252
+ public subtractWeeks(amount: number): DateTime {
253
+ return this.addWeeks(-amount);
254
+ }
255
+
256
+ /**
257
+ * Subtracts months from the date
258
+ * @param amount - Number of months to subtract (can be negative)
259
+ * @returns New DateTime instance with subtracted months
260
+ */
261
+ public subtractMonths(amount: number): DateTime {
262
+ return this.addMonths(-amount);
263
+ }
264
+
265
+ /**
266
+ * Subtracts years from the date
267
+ * @param amount - Number of years to subtract (can be negative)
268
+ * @returns New DateTime instance with subtracted years
269
+ */
270
+ public subtractYears(amount: number): DateTime {
271
+ return this.addYears(-amount);
272
+ }
273
+
274
+ // ============================================
275
+ // Comparison Methods
276
+ // ============================================
277
+
278
+ /**
279
+ * Checks if this date is before another date
280
+ * @param other - DateTime instance to compare against
281
+ * @returns true if this date is before the other date, false otherwise
282
+ */
283
+ public isBefore(other: DateTime): boolean {
284
+ return this.valueOf() < other.valueOf();
285
+ }
286
+
287
+ /**
288
+ * Checks if this date is after another date
289
+ * @param other - DateTime instance to compare against
290
+ * @returns true if this date is after the other date, false otherwise
291
+ */
292
+ public isAfter(other: DateTime): boolean {
293
+ return this.valueOf() > other.valueOf();
294
+ }
295
+
296
+ /**
297
+ * Checks if this date is the same as another date
298
+ * @param other - DateTime instance to compare against
299
+ * @returns true if this date is the same as the other date, false otherwise
300
+ */
301
+ public isSame(other: DateTime): boolean {
302
+ return this.valueOf() === other.valueOf();
303
+ }
304
+
305
+ /**
306
+ * Checks if this date is the same as or before another date
307
+ * @param other - DateTime instance to compare against
308
+ * @returns true if this date is the same as or before the other date, false otherwise
309
+ */
310
+ public isSameOrBefore(other: DateTime): boolean {
311
+ return this.valueOf() <= other.valueOf();
312
+ }
313
+
314
+ /**
315
+ * Checks if this date is the same as or after another date
316
+ * @param other - DateTime instance to compare against
317
+ * @returns true if this date is the same as or after the other date, false otherwise
318
+ */
319
+ public isSameOrAfter(other: DateTime): boolean {
320
+ return this.valueOf() >= other.valueOf();
321
+ }
322
+
323
+ /**
324
+ * Checks if this date is between two dates
325
+ * @param start - Start date of the range
326
+ * @param end - End date of the range
327
+ * @param inclusive - Whether to include the start and end dates in the comparison (default: false)
328
+ * @returns true if this date is between start and end, false otherwise
329
+ */
330
+ public isBetween(
331
+ start: DateTime,
332
+ end: DateTime,
333
+ inclusive = false,
334
+ ): boolean {
335
+ const thisTime = this.valueOf();
336
+ const startTime = start.valueOf();
337
+ const endTime = end.valueOf();
338
+
339
+ if (inclusive) {
340
+ return thisTime >= startTime && thisTime <= endTime;
341
+ }
342
+ return thisTime > startTime && thisTime < endTime;
343
+ }
344
+
345
+ // ============================================
346
+ // Getter Methods
347
+ // ============================================
348
+
349
+ /**
350
+ * Gets the UTC year
351
+ * @returns The year (e.g., 2024)
352
+ */
353
+ public year(): number {
354
+ return this._date.getUTCFullYear();
355
+ }
356
+
357
+ /**
358
+ * Gets the UTC month (0-indexed)
359
+ * @returns The month (0-11, where 0=January)
360
+ */
361
+ public month(): number {
362
+ return this._date.getUTCMonth();
363
+ }
364
+
365
+ /**
366
+ * Gets the UTC day of the month
367
+ * @returns The day of month (1-31)
368
+ */
369
+ public date(): number {
370
+ return this._date.getUTCDate();
371
+ }
372
+
373
+ /**
374
+ * Gets the UTC day of the week (0-indexed)
375
+ * @returns The day of week (0-6, where 0=Sunday)
376
+ */
377
+ public day(): number {
378
+ return this._date.getUTCDay();
379
+ }
380
+
381
+ /**
382
+ * Gets the UTC hour
383
+ * @returns The hour (0-23)
384
+ */
385
+ public hour(): number {
386
+ return this._date.getUTCHours();
387
+ }
388
+
389
+ /**
390
+ * Gets the UTC minute
391
+ * @returns The minute (0-59)
392
+ */
393
+ public minute(): number {
394
+ return this._date.getUTCMinutes();
395
+ }
396
+
397
+ /**
398
+ * Gets the UTC second
399
+ * @returns The second (0-59)
400
+ */
401
+ public second(): number {
402
+ return this._date.getUTCSeconds();
403
+ }
404
+
405
+ /**
406
+ * Gets the UTC millisecond
407
+ * @returns The millisecond (0-999)
408
+ */
409
+ public millisecond(): number {
410
+ return this._date.getUTCMilliseconds();
411
+ }
412
+
413
+ // ============================================
414
+ // Setter Methods
415
+ // ============================================
416
+
417
+ /**
418
+ * Sets the UTC year
419
+ * @param year - The year to set
420
+ * @returns New DateTime instance with updated year
421
+ */
422
+ public setYear(year: number): DateTime {
423
+ const newDate = this.toDate();
424
+ newDate.setUTCFullYear(year);
425
+ return new DateTime(newDate);
426
+ }
427
+
428
+ /**
429
+ * Sets the UTC month (0-indexed)
430
+ * @param month - The month to set (0-11, where 0=January)
431
+ * @returns New DateTime instance with updated month
432
+ */
433
+ public setMonth(month: number): DateTime {
434
+ const newDate = this.toDate();
435
+ newDate.setUTCMonth(month);
436
+ return new DateTime(newDate);
437
+ }
438
+
439
+ /**
440
+ * Sets the UTC day of the month
441
+ * @param date - The day to set (1-31)
442
+ * @returns New DateTime instance with updated date
443
+ */
444
+ public setDate(date: number): DateTime {
445
+ const newDate = this.toDate();
446
+ newDate.setUTCDate(date);
447
+ return new DateTime(newDate);
448
+ }
449
+
450
+ /**
451
+ * Sets the UTC hour
452
+ * @param hour - The hour to set (0-23)
453
+ * @returns New DateTime instance with updated hour
454
+ */
455
+ public setHour(hour: number): DateTime {
456
+ const newDate = this.toDate();
457
+ newDate.setUTCHours(hour);
458
+ return new DateTime(newDate);
459
+ }
460
+
461
+ /**
462
+ * Sets the UTC minute
463
+ * @param minute - The minute to set (0-59)
464
+ * @returns New DateTime instance with updated minute
465
+ */
466
+ public setMinute(minute: number): DateTime {
467
+ const newDate = this.toDate();
468
+ newDate.setUTCMinutes(minute);
469
+ return new DateTime(newDate);
470
+ }
471
+
472
+ /**
473
+ * Sets the UTC second
474
+ * @param second - The second to set (0-59)
475
+ * @returns New DateTime instance with updated second
476
+ */
477
+ public setSecond(second: number): DateTime {
478
+ const newDate = this.toDate();
479
+ newDate.setUTCSeconds(second);
480
+ return new DateTime(newDate);
481
+ }
482
+
483
+ /**
484
+ * Sets the UTC millisecond
485
+ * @param millisecond - The millisecond to set (0-999)
486
+ * @returns New DateTime instance with updated millisecond
487
+ */
488
+ public setMillisecond(millisecond: number): DateTime {
489
+ const newDate = this.toDate();
490
+ newDate.setUTCMilliseconds(millisecond);
491
+ return new DateTime(newDate);
492
+ }
493
+
494
+ // ============================================
495
+ // Start/End Methods
496
+ // ============================================
497
+
498
+ /**
499
+ * Returns a new DateTime at the start of the day (00:00:00.000)
500
+ * @returns New DateTime instance at start of day
501
+ */
502
+ public startOfDay(): DateTime {
503
+ const newDate = this.toDate();
504
+ newDate.setUTCHours(0, 0, 0, 0);
505
+ return new DateTime(newDate);
506
+ }
507
+
508
+ /**
509
+ * Returns a new DateTime at the end of the day (23:59:59.999)
510
+ * @returns New DateTime instance at end of day
511
+ */
512
+ public endOfDay(): DateTime {
513
+ const newDate = this.toDate();
514
+ newDate.setUTCHours(23, 59, 59, 999);
515
+ return new DateTime(newDate);
516
+ }
517
+
518
+ /**
519
+ * Returns a new DateTime at the start of the hour (XX:00:00.000)
520
+ * @returns New DateTime instance at start of hour
521
+ */
522
+ public startOfHour(): DateTime {
523
+ const newDate = this.toDate();
524
+ newDate.setUTCMinutes(0, 0, 0);
525
+ return new DateTime(newDate);
526
+ }
527
+
528
+ /**
529
+ * Returns a new DateTime at the end of the hour (XX:59:59.999)
530
+ * @returns New DateTime instance at end of hour
531
+ */
532
+ public endOfHour(): DateTime {
533
+ const newDate = this.toDate();
534
+ newDate.setUTCMinutes(59, 59, 999);
535
+ return new DateTime(newDate);
536
+ }
537
+
538
+ /**
539
+ * Returns a new DateTime at the start of the week (Sunday 00:00:00.000)
540
+ * @returns New DateTime instance at start of week
541
+ */
542
+ public startOfWeek(): DateTime {
543
+ const newDate = this.toDate();
544
+ const day = newDate.getUTCDay();
545
+ newDate.setUTCDate(newDate.getUTCDate() - day);
546
+ newDate.setUTCHours(0, 0, 0, 0);
547
+ return new DateTime(newDate);
548
+ }
549
+
550
+ /**
551
+ * Returns a new DateTime at the end of the week (Saturday 23:59:59.999)
552
+ * @returns New DateTime instance at end of week
553
+ */
554
+ public endOfWeek(): DateTime {
555
+ const newDate = this.toDate();
556
+ const day = newDate.getUTCDay();
557
+ newDate.setUTCDate(newDate.getUTCDate() + (6 - day));
558
+ newDate.setUTCHours(23, 59, 59, 999);
559
+ return new DateTime(newDate);
560
+ }
561
+
562
+ /**
563
+ * Returns a new DateTime at the start of the month (day 1, 00:00:00.000)
564
+ * @returns New DateTime instance at start of month
565
+ */
566
+ public startOfMonth(): DateTime {
567
+ const newDate = this.toDate();
568
+ newDate.setUTCDate(1);
569
+ newDate.setUTCHours(0, 0, 0, 0);
570
+ return new DateTime(newDate);
571
+ }
572
+
573
+ /**
574
+ * Returns a new DateTime at the end of the month (last day, 23:59:59.999)
575
+ * @returns New DateTime instance at end of month
576
+ */
577
+ public endOfMonth(): DateTime {
578
+ const newDate = this.toDate();
579
+ // Set to next month, day 0 (last day of current month)
580
+ newDate.setUTCMonth(newDate.getUTCMonth() + 1, 0);
581
+ newDate.setUTCHours(23, 59, 59, 999);
582
+ return new DateTime(newDate);
583
+ }
584
+
585
+ /**
586
+ * Returns a new DateTime at the start of the year (Jan 1, 00:00:00.000)
587
+ * @returns New DateTime instance at start of year
588
+ */
589
+ public startOfYear(): DateTime {
590
+ const newDate = this.toDate();
591
+ newDate.setUTCMonth(0, 1);
592
+ newDate.setUTCHours(0, 0, 0, 0);
593
+ return new DateTime(newDate);
594
+ }
595
+
596
+ /**
597
+ * Returns a new DateTime at the end of the year (Dec 31, 23:59:59.999)
598
+ * @returns New DateTime instance at end of year
599
+ */
600
+ public endOfYear(): DateTime {
601
+ const newDate = this.toDate();
602
+ newDate.setUTCMonth(11, 31);
603
+ newDate.setUTCHours(23, 59, 59, 999);
604
+ return new DateTime(newDate);
605
+ }
606
+
607
+ // ============================================
608
+ // Difference Method
609
+ // ============================================
610
+
611
+ /**
612
+ * Calculates the difference between this date and another date
613
+ * @param other - DateTime instance to compare against
614
+ * @param unit - Unit of measurement (default: 'millisecond')
615
+ * @returns The difference (can be negative)
616
+ */
617
+ public diff(other: DateTime, unit: TimeUnit = "millisecond"): number {
618
+ const diff = this.valueOf() - other.valueOf();
619
+
620
+ switch (unit) {
621
+ case "millisecond":
622
+ return diff;
623
+ case "second":
624
+ return diff / 1000;
625
+ case "minute":
626
+ return diff / (1000 * 60);
627
+ case "hour":
628
+ return diff / (1000 * 60 * 60);
629
+ case "day":
630
+ return diff / (1000 * 60 * 60 * 24);
631
+ case "week":
632
+ return diff / (1000 * 60 * 60 * 24 * 7);
633
+ case "month": {
634
+ // For months, we calculate based on the actual month difference
635
+ const thisDate = this.toDate();
636
+ const otherDate = other.toDate();
637
+ const yearDiff =
638
+ thisDate.getUTCFullYear() - otherDate.getUTCFullYear();
639
+ const monthDiff = thisDate.getUTCMonth() - otherDate.getUTCMonth();
640
+ return yearDiff * 12 + monthDiff;
641
+ }
642
+ case "year": {
643
+ // For years, we calculate based on milliseconds
644
+ // Average year length accounting for leap years
645
+ return diff / (1000 * 60 * 60 * 24 * 365.25);
646
+ }
647
+ default:
648
+ return diff;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Registers a plugin to extend DateTime functionality
654
+ * @param plugin - The plugin to register
655
+ * @param options - Optional plugin configuration
656
+ * @throws {PluginError} When plugin with same name already exists
657
+ */
658
+ public static extend(
659
+ plugin: DateTimePlugin,
660
+ options?: Record<string, unknown>,
661
+ ): void {
662
+ // Check if plugin already registered
663
+ if (DateTime.plugins.has(plugin.name)) {
664
+ throw new PluginError(
665
+ `Plugin ${plugin.name} is already registered`,
666
+ plugin.name,
667
+ );
668
+ }
669
+
670
+ // Register plugin
671
+ DateTime.plugins.set(plugin.name, plugin);
672
+
673
+ // Call plugin install function
674
+ plugin.install(DateTime as any, options);
675
+ }
676
+
677
+ /**
678
+ * Checks if a plugin is registered
679
+ * @param name - Plugin name
680
+ * @returns true if plugin is registered, false otherwise
681
+ */
682
+ public static hasPlugin(name: string): boolean {
683
+ return DateTime.plugins.has(name);
684
+ }
685
+
686
+ /**
687
+ * Gets a registered plugin
688
+ * @param name - Plugin name
689
+ * @returns The plugin if found, undefined otherwise
690
+ */
691
+ public static getPlugin(name: string): DateTimePlugin | undefined {
692
+ return DateTime.plugins.get(name);
693
+ }
694
+ }