@nivinjoseph/n-date 1.0.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.
Files changed (43) hide show
  1. package/.editorconfig +14 -0
  2. package/.vscode/launch.json +56 -0
  3. package/.vscode/settings.json +111 -0
  4. package/.vscode/tasks.json +12 -0
  5. package/.yarn/releases/yarn-4.14.1.cjs +940 -0
  6. package/.yarnrc.yml +8 -0
  7. package/LICENSE +21 -0
  8. package/README.md +21 -0
  9. package/dist/date-time-format.d.ts +11 -0
  10. package/dist/date-time-format.d.ts.map +1 -0
  11. package/dist/date-time-format.js +11 -0
  12. package/dist/date-time-format.js.map +1 -0
  13. package/dist/date-time-span.d.ts +70 -0
  14. package/dist/date-time-span.d.ts.map +1 -0
  15. package/dist/date-time-span.js +122 -0
  16. package/dist/date-time-span.js.map +1 -0
  17. package/dist/date-time.d.ts +391 -0
  18. package/dist/date-time.d.ts.map +1 -0
  19. package/dist/date-time.js +753 -0
  20. package/dist/date-time.js.map +1 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/tsconfig.json +13 -0
  26. package/docs/README.md +33 -0
  27. package/docs/date-time-span.md +72 -0
  28. package/docs/date-time.md +169 -0
  29. package/docs/formats.md +56 -0
  30. package/docs/getting-started.md +151 -0
  31. package/eslint.config.js +596 -0
  32. package/package.json +57 -0
  33. package/src/date-time-format.ts +35 -0
  34. package/src/date-time-span.ts +130 -0
  35. package/src/date-time.ts +950 -0
  36. package/src/index.ts +3 -0
  37. package/test/date-time-comparison.test.ts +1579 -0
  38. package/test/date-time-create.test.ts +1147 -0
  39. package/test/date-time-math.test.ts +324 -0
  40. package/test/date-time-properties.test.ts +200 -0
  41. package/test/date-time-utility.test.ts +432 -0
  42. package/test/date-time-validations.test.ts +521 -0
  43. package/tsconfig.json +31 -0
@@ -0,0 +1,950 @@
1
+ import { given } from "@nivinjoseph/n-defensive";
2
+ import { DateTime as LuxonDateTime, Interval as LuxonInterval } from "luxon";
3
+ import { Serializable, serialize, Duration, Schema, TypeHelper } from "@nivinjoseph/n-util";
4
+ import { DateTimeFormat, DateTimeFormat_DEFAULT, DateTimeFormatExt } from "./date-time-format.js";
5
+
6
+ /**
7
+ * A robust date and time handling system with timezone support.
8
+ * This class provides comprehensive functionality for date/time manipulation, comparison, and formatting.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const now = DateTime.now("UTC");
13
+ * const future = now.addTime(Duration.fromHours(2));
14
+ * const isAfter = future.isAfter(now);
15
+ * ```
16
+ */
17
+ @serialize("Ndate")
18
+ export class DateTime extends Serializable<DateTimeSchema>
19
+ {
20
+ private static _fixedNow: number | null = null;
21
+ private static _relativeNow: { baseTimestamp: number; baseRealTime: number; } | null = null;
22
+
23
+ private readonly _value: string;
24
+ private readonly _zone: string;
25
+ private readonly _dateTime: LuxonDateTime;
26
+ private readonly _timestamp: number;
27
+ private readonly _dateCode: string;
28
+ private readonly _timeCode: string;
29
+ private readonly _dateValue: string;
30
+ private readonly _timeValue: string;
31
+
32
+
33
+ /**
34
+ * Gets the system's local timezone.
35
+ *
36
+ * @returns The local timezone identifier.
37
+ */
38
+ public static get currentZone(): string { return LuxonDateTime.local().zoneName; }
39
+
40
+ /**
41
+ * Gets the formatted date and time string.
42
+ */
43
+ @serialize
44
+ public get value(): string { return this._value; }
45
+
46
+ /**
47
+ * Gets the timezone identifier.
48
+ */
49
+ @serialize
50
+ public get zone(): string { return this._zone; }
51
+
52
+ /**
53
+ * Gets the Unix timestamp in seconds.
54
+ */
55
+ public get timestamp(): number { return this._timestamp; }
56
+
57
+ /**
58
+ * Gets the date code in YYYYMMDD format.
59
+ */
60
+ public get dateCode(): string { return this._dateCode; }
61
+
62
+ /**
63
+ * Gets the time code in HHMMSS format.
64
+ */
65
+ public get timeCode(): string { return this._timeCode; }
66
+
67
+ /**
68
+ * Gets the date value in YYYY-MM-DD format.
69
+ */
70
+ public get dateValue(): string { return this._dateValue; }
71
+
72
+ /**
73
+ * Gets the time value in HH:mm:ss format.
74
+ */
75
+ public get timeValue(): string { return this._timeValue; }
76
+
77
+ /**
78
+ * Gets whether this DateTime is in the past.
79
+ */
80
+ public get isPast(): boolean { return this.isBefore(DateTime.now()); }
81
+
82
+ /**
83
+ * Gets whether this DateTime is in the future.
84
+ */
85
+ public get isFuture(): boolean { return this.isAfter(DateTime.now()); }
86
+
87
+ /**
88
+ * Creates a new DateTime instance.
89
+ *
90
+ * @param data - The DateTime data containing value and zone.
91
+ * @throws Error if the value or zone is invalid.
92
+ */
93
+ public constructor(data: DateTimeSchema)
94
+ {
95
+ super(data);
96
+
97
+ let { value, zone } = data;
98
+
99
+ given(value, "value").ensureHasValue().ensureIsString();
100
+ value = value.trim()
101
+ .split("")
102
+ .take(DateTimeFormat.yearMonthDayHourMinuteSecond.length)
103
+ .join("");
104
+
105
+ if (value.matchesFormat("####"))
106
+ value = `${value}-01`; // MM
107
+ if (value.matchesFormat("####-##"))
108
+ value = `${value}-01`; // dd
109
+ if (value.matchesFormat("####-##-##"))
110
+ value = `${value} 00`; // HH
111
+ if (value.matchesFormat("####-##-## ##"))
112
+ value = `${value}:00`; // mm
113
+ if (value.matchesFormat("####-##-## ##:##"))
114
+ value = `${value}:00`; // ss
115
+
116
+ given(value, "value")
117
+ .ensure(
118
+ t => t.matchesFormat("####-##-## ##:##:##"),
119
+ "Invalid format"
120
+ );
121
+
122
+ const [date, time] = value.split(" ");
123
+
124
+ const dateSplit = date.split("-");
125
+ // const _year = Number.parseInt(dateSplit[0]);
126
+ const month = Number.parseInt(dateSplit[1]);
127
+ const day = Number.parseInt(dateSplit[2]);
128
+
129
+ given(month, "month").ensureHasValue().ensureIsNumber().ensure(t => t >= 1 && t <= 12);
130
+ given(day, "day").ensureHasValue().ensureIsNumber().ensure(t => t >= 1 && t <= 31);
131
+
132
+ const timeSplit = time.split(":");
133
+ const hour = Number.parseInt(timeSplit[0]);
134
+ const minute = Number.parseInt(timeSplit[1]);
135
+ const second = Number.parseInt(timeSplit[2]);
136
+
137
+ given(hour, "hour").ensureHasValue().ensureIsNumber().ensure(t => t >= 0 && t <= 23);
138
+ given(minute, "minute").ensureHasValue().ensureIsNumber().ensure(t => t >= 0 && t <= 59);
139
+ given(second, "second").ensureHasValue().ensureIsNumber().ensure(t => t >= 0 && t <= 59);
140
+
141
+ given(zone, "zone").ensureHasValue().ensureIsString();
142
+ zone = zone.trim();
143
+ if (zone.toLowerCase() === "utc")
144
+ zone = zone.toLowerCase();
145
+
146
+ DateTime._validateZone(zone);
147
+
148
+ const dateTime = LuxonDateTime.fromFormat(
149
+ value,
150
+ DateTimeFormat.yearMonthDayHourMinuteSecond,
151
+ { zone }
152
+ );
153
+ given(data, "data")
154
+ .ensure(
155
+ _ => dateTime.isValid,
156
+ `value and zone is invalid (${dateTime.invalidReason}: ${dateTime.invalidExplanation})`
157
+ );
158
+
159
+ this._value = value;
160
+ this._zone = zone;
161
+ this._dateTime = dateTime;
162
+ this._timestamp = this._dateTime.toUnixInteger();
163
+
164
+ this._dateCode = dateSplit.join("");
165
+ this._timeCode = timeSplit.join("");
166
+
167
+ this._dateValue = date;
168
+ this._timeValue = time;
169
+ }
170
+
171
+ /**
172
+ * Sets a fixed timestamp for testing purposes. All calls to DateTime.now() will return this fixed time.
173
+ *
174
+ * @param timestamp - The Unix timestamp in seconds to use as the fixed "now" time.
175
+ * @throws Error if timestamp is not a valid number.
176
+ */
177
+ public static useFixedNow(timestamp: number): void
178
+ {
179
+ given(timestamp, "timestamp").ensureHasValue().ensureIsNumber();
180
+
181
+ DateTime._fixedNow = timestamp;
182
+ DateTime._relativeNow = null;
183
+ }
184
+
185
+ /**
186
+ * Sets a relative timestamp for testing purposes. DateTime.now() will return times relative to this base timestamp,
187
+ * advancing as real time advances.
188
+ *
189
+ * @param timestamp - The Unix timestamp in seconds to use as the base "now" time.
190
+ * @throws Error if timestamp is not a valid number.
191
+ */
192
+ public static useRelativeNow(timestamp: number): void
193
+ {
194
+ given(timestamp, "timestamp").ensureHasValue().ensureIsNumber();
195
+
196
+ DateTime._relativeNow = {
197
+ baseTimestamp: timestamp,
198
+ baseRealTime: Date.now()
199
+ };
200
+ DateTime._fixedNow = null;
201
+ }
202
+
203
+ /**
204
+ * Resets any fixed or relative "now" time set by useFixedNow or useRelativeNow.
205
+ * DateTime.now() will return the actual current time after calling this method.
206
+ */
207
+ public static resetFixedOrRelativeNow(): void
208
+ {
209
+ DateTime._fixedNow = null;
210
+ DateTime._relativeNow = null;
211
+ }
212
+
213
+
214
+ /**
215
+ * Creates a DateTime instance for the current time.
216
+ *
217
+ * @param zone - The timezone identifier. If not specified, UTC is used.
218
+ * @returns A new DateTime instance representing the current time.
219
+ */
220
+ public static now(zone?: string): DateTime
221
+ {
222
+ given(zone, "zone").ensureIsString();
223
+
224
+ // Check if we're using fixed or relative now for testing
225
+ let timestamp: number | null = null;
226
+ if (DateTime._fixedNow !== null)
227
+ {
228
+ timestamp = DateTime._fixedNow;
229
+ }
230
+ else if (DateTime._relativeNow !== null)
231
+ {
232
+ const elapsedMs = Date.now() - DateTime._relativeNow.baseRealTime;
233
+ timestamp = DateTime._relativeNow.baseTimestamp + Math.floor(elapsedMs / 1000);
234
+ }
235
+
236
+ // If we have a timestamp, use it
237
+ if (timestamp !== null)
238
+ {
239
+ const targetZone = zone ?? "utc";
240
+ return DateTime.createFromTimestamp(timestamp, targetZone);
241
+ }
242
+
243
+ // Otherwise, use real current time
244
+ if (zone != null)
245
+ {
246
+ return new DateTime({
247
+ value: LuxonDateTime.now().setZone(zone).toFormat(DateTimeFormat_DEFAULT),
248
+ zone
249
+ });
250
+ }
251
+ else
252
+ {
253
+ return new DateTime({
254
+ value: LuxonDateTime.utc().toFormat(DateTimeFormat_DEFAULT),
255
+ zone: "utc"
256
+ });
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Creates a DateTime from a Unix timestamp.
262
+ *
263
+ * @param timestamp - The number of seconds since the Unix epoch.
264
+ * @param zone - The timezone identifier.
265
+ * @returns A new DateTime instance.
266
+ */
267
+ public static createFromTimestamp(timestamp: number, zone: string): DateTime
268
+ {
269
+ given(timestamp, "timestamp").ensureHasValue().ensureIsNumber();
270
+ given(zone, "zone").ensureHasValue().ensureIsString();
271
+
272
+ const dateTimeString = LuxonDateTime.fromSeconds(timestamp)
273
+ .setZone(zone).toFormat(DateTimeFormat_DEFAULT);
274
+
275
+ return new DateTime({
276
+ value: dateTimeString,
277
+ zone
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Creates a DateTime from milliseconds since the Unix epoch.
283
+ *
284
+ * @param milliseconds - The number of milliseconds since the Unix epoch.
285
+ * @param zone - The timezone identifier.
286
+ * @returns A new DateTime instance.
287
+ */
288
+ public static createFromMilliSecondsSinceEpoch(milliseconds: number, zone: string): DateTime
289
+ {
290
+ given(milliseconds, "milliseconds").ensureHasValue().ensureIsNumber();
291
+ given(zone, "zone").ensureHasValue().ensureIsString();
292
+
293
+ const dateTimeString = LuxonDateTime.fromMillis(milliseconds)
294
+ .setZone(zone).toFormat(DateTimeFormat_DEFAULT);
295
+ return new DateTime({
296
+ value: dateTimeString,
297
+ zone
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Creates a DateTime from date and time codes.
303
+ *
304
+ * @param dateCode - The date code in YYYYMMDD format.
305
+ * @param timeCode - The time code in HHMM format.
306
+ * @param zone - The timezone identifier.
307
+ * @returns A new DateTime instance.
308
+ */
309
+ public static createFromCodes(dateCode: string, timeCode: string, zone: string): DateTime
310
+ {
311
+ given(dateCode, "dateCode").ensureHasValue().ensureIsString()
312
+ .ensure(t => t.matchesFormat("########"));
313
+
314
+ given(timeCode, "timeCode").ensureHasValue().ensureIsString()
315
+ .ensure(t => t.matchesFormat("######"));
316
+
317
+ given(zone, "zone").ensureHasValue().ensureIsString();
318
+
319
+ const dateCodeSplit = dateCode.split("");
320
+ const timeCodeSplit = timeCode.split("");
321
+
322
+ const year = dateCodeSplit.take(4).join("");
323
+ const month = dateCodeSplit.skip(4).take(2).join("");
324
+ const day = dateCodeSplit.skip(6).join("");
325
+
326
+ const hour = timeCodeSplit.take(2).join("");
327
+ const minute = timeCodeSplit.skip(2).take(2).join("");
328
+ const second = timeCodeSplit.skip(4).join("");
329
+
330
+ const dateTimeString = `${year}-${month}-${day} ${hour}:${minute}:${second}`;
331
+
332
+ return new DateTime({
333
+ value: dateTimeString,
334
+ zone
335
+ });
336
+ }
337
+
338
+ /**
339
+ * Creates a DateTime from date and time values.
340
+ *
341
+ * @param dateValue - The date in YYYY-MM-DD format.
342
+ * @param timeValue - The time in HH:mm format.
343
+ * @param zone - The timezone identifier.
344
+ * @returns A new DateTime instance.
345
+ */
346
+ public static createFromValues(dateValue: string, timeValue: string, zone: string): DateTime
347
+ {
348
+ given(dateValue, "dateValue").ensureHasValue().ensureIsString()
349
+ .ensure(t => t.matchesFormat("####-##-##"));
350
+
351
+ given(timeValue, "timeValue").ensureHasValue().ensureIsString()
352
+ .ensure(t => t.matchesFormat("##:##:##"));
353
+
354
+ given(zone, "zone").ensureHasValue().ensureIsString();
355
+
356
+ const dateTimeString = `${dateValue} ${timeValue}`;
357
+
358
+ return new DateTime({
359
+ value: dateTimeString,
360
+ zone
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Returns the earlier of two DateTime instances.
366
+ *
367
+ * @param dateTime1 - The first DateTime instance.
368
+ * @param dateTime2 - The second DateTime instance.
369
+ * @returns The earlier DateTime instance.
370
+ */
371
+ public static min(dateTime1: DateTime, dateTime2: DateTime): DateTime
372
+ {
373
+ given(dateTime1, "dateTime1").ensureHasValue().ensureIsType(DateTime);
374
+ given(dateTime2, "dateTime2").ensureHasValue().ensureIsType(DateTime);
375
+
376
+ if (dateTime1.valueOf() < dateTime2.valueOf())
377
+ return dateTime1;
378
+
379
+ return dateTime2;
380
+ }
381
+
382
+ /**
383
+ * Returns the later of two DateTime instances.
384
+ *
385
+ * @param dateTime1 - The first DateTime instance.
386
+ * @param dateTime2 - The second DateTime instance.
387
+ * @returns The later DateTime instance.
388
+ */
389
+ public static max(dateTime1: DateTime, dateTime2: DateTime): DateTime
390
+ {
391
+ given(dateTime1, "dateTime1").ensureHasValue().ensureIsType(DateTime);
392
+ given(dateTime2, "dateTime2").ensureHasValue().ensureIsType(DateTime);
393
+
394
+ if (dateTime1.valueOf() > dateTime2.valueOf())
395
+ return dateTime1;
396
+
397
+ return dateTime2;
398
+ }
399
+
400
+ /**
401
+ * Validates if a string matches the DateTime format "yyyy-MM-dd HH:mm".
402
+ *
403
+ * @param value - The string to validate.
404
+ * @returns True if the string matches the format, false otherwise.
405
+ */
406
+ public static validateDateTimeFormat(value: string, format: DateTimeFormat): boolean
407
+ {
408
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
409
+ if (value == null || value.isEmptyOrWhiteSpace())
410
+ return false;
411
+
412
+ return LuxonDateTime.fromFormat(value, format).isValid;
413
+ }
414
+
415
+ /**
416
+ * Validates if a string matches the date format "yyyy-MM-dd".
417
+ *
418
+ * @param value - The string to validate.
419
+ * @returns True if the string matches the format, false otherwise.
420
+ */
421
+ public static validateDateFormat(value: string): boolean
422
+ {
423
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
424
+ if (value == null || value.isEmptyOrWhiteSpace())
425
+ return false;
426
+
427
+ return LuxonDateTime.fromFormat(value, "yyyy-MM-dd").isValid;
428
+ }
429
+
430
+ /**
431
+ * Validates if a string matches the time format "HH:mm".
432
+ *
433
+ * @param value - The string to validate.
434
+ * @returns True if the string matches the format, false otherwise.
435
+ */
436
+ public static validateTimeFormat(value: string): boolean
437
+ {
438
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
439
+ if (value == null || value.isEmptyOrWhiteSpace())
440
+ return false;
441
+
442
+ return LuxonDateTime.fromFormat(value, "HH:mm").isValid;
443
+ }
444
+
445
+ /**
446
+ * Validates if a string is a valid timezone.
447
+ *
448
+ * @param zone - The timezone string to validate.
449
+ * @returns True if the timezone is valid, false otherwise.
450
+ */
451
+ public static validateTimeZone(zone: string): boolean
452
+ {
453
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
454
+ if (zone == null || zone.isEmptyOrWhiteSpace())
455
+ return false;
456
+
457
+ try
458
+ {
459
+ DateTime._validateZone(zone);
460
+ }
461
+ catch
462
+ {
463
+ return false;
464
+ }
465
+
466
+ return LuxonDateTime.now().setZone(zone).isValid;
467
+ }
468
+
469
+
470
+ /**
471
+ * Validates a timezone string.
472
+ *
473
+ * @param zone - The timezone string to validate.
474
+ * @throws Error if the timezone is invalid.
475
+ * @private
476
+ */
477
+ private static _validateZone(zone: string): void
478
+ {
479
+ zone = zone.trim();
480
+
481
+ if (zone.toLowerCase() === "utc")
482
+ return;
483
+
484
+ given(zone, "zone")
485
+ .ensureWhen(
486
+ zone.toLowerCase() === "local",
487
+ _ => false,
488
+ "should not use local zone")
489
+ .ensureWhen(
490
+ zone.toLowerCase().startsWith("utc+"),
491
+ t =>
492
+ {
493
+ // range is +00:00 to +14:00 (https://en.wikipedia.org/wiki/List_of_UTC_offsets)
494
+ let offset = t.split("+").takeLast().trim();
495
+
496
+ if (!offset.contains(":"))
497
+ offset = `${offset}:00`;
498
+
499
+ const [hour, minute] = offset.split(":").map(t => TypeHelper.parseNumber(t));
500
+
501
+ if (hour == null || minute == null)
502
+ return false;
503
+
504
+ return (hour >= 0 && hour < 14 && minute >= 0 && minute < 60)
505
+ || (hour === 14 && minute === 0);
506
+ },
507
+ "Invalid UTC offset for zone")
508
+ .ensureWhen(
509
+ zone.toLowerCase().startsWith("utc-"),
510
+ t =>
511
+ {
512
+ // range is -00:00 to -12:00 (https://en.wikipedia.org/wiki/List_of_UTC_offsets)
513
+ let offset = t.split("-").takeLast();
514
+
515
+ if (!offset.contains(":"))
516
+ offset = `${offset}:00`;
517
+
518
+ const [hour, minute] = offset.split(":").map(t => TypeHelper.parseNumber(t));
519
+
520
+ if (hour == null || minute == null)
521
+ return false;
522
+
523
+ return hour >= 0 && hour < 12 && minute >= 0 && minute < 60
524
+ || (hour === 12 && minute === 0);
525
+ },
526
+ "Invalid UTC offset for zone");
527
+ }
528
+
529
+
530
+ /**
531
+ * Gets the numeric value of this DateTime.
532
+ *
533
+ * @returns The milliseconds since the Unix epoch.
534
+ */
535
+ public override valueOf(): number
536
+ {
537
+ return this._dateTime.valueOf();
538
+ }
539
+
540
+ /**
541
+ * Compares this DateTime with another for equality.
542
+ *
543
+ * @param value - The DateTime to compare with.
544
+ * @returns True if the DateTime instances are equal, false otherwise.
545
+ */
546
+ public equals(value?: DateTime | null): boolean
547
+ {
548
+ given(value, "value").ensureIsType(DateTime);
549
+
550
+ if (value == null)
551
+ return false;
552
+
553
+ if (value === this)
554
+ return true;
555
+
556
+ return value.value === this._value && value.zone === this._zone;
557
+ }
558
+
559
+ /**
560
+ * Returns the string representation of this DateTime.
561
+ *
562
+ * @returns The string representation in the format "YYYY-MM-DD HH:mm:ss zone".
563
+ */
564
+ public override toString(): string
565
+ {
566
+ return `${this._value} ${this._zone}`;
567
+ }
568
+
569
+ /**
570
+ * Returns the date and time string.
571
+ *
572
+ * @returns The string in the format "YYYY-MM-DD HH:mm:ss".
573
+ */
574
+ public toStringDateTime(): string
575
+ {
576
+ return this._value;
577
+ }
578
+
579
+ /**
580
+ * Returns the ISO string representation.
581
+ *
582
+ * @returns The ISO 8601 string representation.
583
+ */
584
+ public toStringISO(): string
585
+ {
586
+ return this._dateTime.toISO({ format: "extended", includeOffset: true })!;
587
+ }
588
+
589
+ /**
590
+ * Formats a DateTime object to a string using the specified format
591
+ * @param dateTime The DateTime object to format
592
+ * @param format The format to use (defaults to yearMonthDayHourMinute: "yyyy-MM-dd HH:mm")
593
+ * @returns The formatted datetime string
594
+ */
595
+ public format(format: DateTimeFormat = DateTimeFormat.yearMonthDayHourMinuteSecond): string
596
+ {
597
+ // For the default format, use the built-in toStringDateTime()
598
+ if (format === DateTimeFormat.yearMonthDayHourMinuteSecond)
599
+ return this.toStringDateTime();
600
+
601
+ // For other formats, parse and reformat
602
+ const value = this.toStringDateTime();
603
+
604
+ // Parse the value (format: "yyyy-MM-dd HH:mm:ss")
605
+ const [date, time] = value.split(" ");
606
+ const [year, month, day] = date.split("-");
607
+ const [hour, minute, _second] = time.split(":");
608
+
609
+ switch (format)
610
+ {
611
+ case DateTimeFormat.yearMonthDayHourMinute:
612
+ return `${year}-${month}-${day} ${hour}:${minute}`;
613
+ case DateTimeFormat.yearMonthDayHour:
614
+ return `${year}-${month}-${day} ${hour}`;
615
+ case DateTimeFormat.yearMonthDay:
616
+ return `${year}-${month}-${day}`;
617
+ case DateTimeFormat.yearMonth:
618
+ return `${year}-${month}`;
619
+ case DateTimeFormat.year:
620
+ return year;
621
+ default:
622
+ return value;
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Formats a DateTime object to a string using extended format capabilities.
628
+ * This method leverages Luxon's full formatting capabilities for more complex formatting needs.
629
+ *
630
+ * @param format - The format string to use. Can be a predefined DateTimeFormatExt or any custom Luxon format string.
631
+ * @returns The formatted datetime string.
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * const dt = DateTime.now("America/New_York");
636
+ * dt.formatExt("DD HH:mm:ss"); // "Jul 2, 2023 15:30:20"
637
+ * dt.formatExt("MMMM d, yyyy"); // "July 2, 2023"
638
+ * dt.formatExt("EEEE DD"); // "Friday Jul 2, 2023"
639
+ *
640
+ * export type DateTimeFormatExt =
641
+ "DD HH:mm:ss" // Jul 2, 2023 15:30:20
642
+ | "MMMM d, HH:mm:ss" // Jul 2 15:30:20
643
+ | "DD HH:mm" // Jul 2, 2023 15:30
644
+ | "MMMM d, HH:mm" // Jul 2 15:30
645
+ | "yyyy/LL/dd" // 2023/07/21
646
+ | "yyyy/LL/dd HH:mm:ss"
647
+ | "yyyy/LL/dd HH:mm"
648
+ | "yyyy-MM-dd" // 2023-07-21
649
+ | "HH:mm:ss" // 15:30:20
650
+ | "HH:mm" // 15:30
651
+ | "DDD" // July 21, 2023
652
+ | "DD" // Jul 21, 2023
653
+ | "yyyy-MM" // 2023-07
654
+ | "MMMM yyyy" // July 2023
655
+ | "DDDD" // Sunday, July 9, 2023
656
+ | "EEEE DD" // Friday Aug 4, 2023
657
+ | "LLL yyyy" // Jul 2025
658
+ | "LLLL yyyy" // July 2025
659
+ | "MMMM d" // November 2
660
+ | "LLL d" // Nov 2
661
+ ;
662
+ *
663
+ * ```
664
+ */
665
+ public formatExt(format: DateTimeFormatExt | string): string
666
+ {
667
+ given(format, "format").ensureHasValue().ensureIsString();
668
+
669
+ return this._dateTime.toFormat(format);
670
+ }
671
+
672
+ /**
673
+ * Checks if this DateTime represents the same instant as another.
674
+ *
675
+ * @param value - The DateTime to compare with.
676
+ * @returns True if the DateTime instances represent the same instant, false otherwise.
677
+ */
678
+ public isSame(value: DateTime): boolean
679
+ {
680
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
681
+
682
+ return this.valueOf() === value.valueOf();
683
+ }
684
+
685
+ /**
686
+ * Checks if this DateTime is before another.
687
+ *
688
+ * @param value - The DateTime to compare with.
689
+ * @returns True if this DateTime is before the other, false otherwise.
690
+ */
691
+ public isBefore(value: DateTime): boolean
692
+ {
693
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
694
+
695
+ return this.valueOf() < value.valueOf();
696
+ }
697
+
698
+ /**
699
+ * Checks if this DateTime is the same or before another.
700
+ *
701
+ * @param value - The DateTime to compare with.
702
+ * @returns True if this DateTime is the same or before the other, false otherwise.
703
+ */
704
+ public isSameOrBefore(value: DateTime): boolean
705
+ {
706
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
707
+
708
+ return this.valueOf() <= value.valueOf();
709
+ }
710
+
711
+ /**
712
+ * Checks if this DateTime is after another.
713
+ *
714
+ * @param value - The DateTime to compare with.
715
+ * @returns True if this DateTime is after the other, false otherwise.
716
+ */
717
+ public isAfter(value: DateTime): boolean
718
+ {
719
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
720
+
721
+ return this.valueOf() > value.valueOf();
722
+ }
723
+
724
+ /**
725
+ * Checks if this DateTime is the same or after another.
726
+ *
727
+ * @param value - The DateTime to compare with.
728
+ * @returns True if this DateTime is the same or after the other, false otherwise.
729
+ */
730
+ public isSameOrAfter(value: DateTime): boolean
731
+ {
732
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
733
+
734
+ return this.valueOf() >= value.valueOf();
735
+ }
736
+
737
+ /**
738
+ * Checks if this DateTime is between two others.
739
+ *
740
+ * @param start - The start DateTime.
741
+ * @param end - The end DateTime.
742
+ * @returns True if this DateTime is between start and end, false otherwise.
743
+ * @throws Error if end is before start.
744
+ */
745
+ public isBetween(start: DateTime, end: DateTime): boolean
746
+ {
747
+ given(start, "start").ensureHasValue().ensureIsType(DateTime);
748
+ given(end, "end").ensureHasValue().ensureIsType(DateTime)
749
+ .ensure(t => t.isSameOrAfter(start), "must be same or after start");
750
+
751
+ return this.isSameOrAfter(start) && this.isSameOrBefore(end);
752
+ }
753
+
754
+ /**
755
+ * Calculates the time difference between this DateTime and another.
756
+ *
757
+ * @param value - The DateTime to compare with.
758
+ * @returns A Duration representing the time difference.
759
+ */
760
+ public timeDiff(value: DateTime): Duration
761
+ {
762
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
763
+
764
+ return Duration.fromMilliSeconds(Math.abs(this.valueOf() - value.valueOf()));
765
+ }
766
+
767
+ /**
768
+ * Calculates the days difference between this DateTime and another.
769
+ *
770
+ * @param value - The DateTime to compare with.
771
+ * @returns The number of days difference.
772
+ */
773
+ public daysDiff(value: DateTime): number
774
+ {
775
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
776
+
777
+ return Math.abs(Number.parseInt(this._dateTime.diff(value._dateTime, ["days"]).days.toString()));
778
+ }
779
+
780
+ /**
781
+ * Checks if this DateTime is on the same day as another.
782
+ *
783
+ * @param value - The DateTime to compare with.
784
+ * @returns True if the DateTime instances are on the same day, false otherwise.
785
+ */
786
+ public isSameDay(value: DateTime): boolean
787
+ {
788
+ given(value, "value").ensureHasValue().ensureIsType(DateTime);
789
+
790
+ const daysDiff = this._dateTime.diff(value._dateTime, ["days"]).days;
791
+
792
+ return Math.abs(daysDiff) < 1;
793
+ }
794
+
795
+ /**
796
+ * Adds a duration to this DateTime. this accounts for shift in DST
797
+ *
798
+ * @param time - The duration to add.
799
+ * @returns A new DateTime instance with the duration added.
800
+ */
801
+ public addTime(time: Duration): DateTime
802
+ {
803
+ given(time, "time").ensureHasValue().ensureIsObject().ensureIsInstanceOf(Duration);
804
+
805
+ return new DateTime({
806
+ value: this._dateTime.plus({ milliseconds: time.toMilliSeconds() }).toFormat(DateTimeFormat_DEFAULT),
807
+ zone: this._zone
808
+ });
809
+ }
810
+
811
+ /**
812
+ * Subtracts a duration from this DateTime. this accounts for shift in DST
813
+ *
814
+ * @param time - The duration to subtract.
815
+ * @returns A new DateTime instance with the duration subtracted.
816
+ */
817
+ public subtractTime(time: Duration): DateTime
818
+ {
819
+ given(time, "time").ensureHasValue().ensureIsObject().ensureIsInstanceOf(Duration);
820
+
821
+ return new DateTime({
822
+ value: this._dateTime.minus({ milliseconds: time.toMilliSeconds() }).toFormat(DateTimeFormat_DEFAULT),
823
+ zone: this._zone
824
+ });
825
+ }
826
+
827
+ /**
828
+ * Adds days to this DateTime. this doesn't change time based on DST
829
+ *
830
+ * @param days - The number of days to add.
831
+ * @returns A new DateTime instance with the days added.
832
+ * @throws Error if days is not a positive integer.
833
+ */
834
+ public addDays(days: number): DateTime
835
+ {
836
+ given(days, "days").ensureHasValue().ensureIsNumber()
837
+ .ensure(t => t >= 0 && Number.isInteger(t), "days should be positive integer");
838
+
839
+ return new DateTime({
840
+ value: this._dateTime.plus({ days }).toFormat(DateTimeFormat_DEFAULT),
841
+ zone: this._zone
842
+ });
843
+ }
844
+
845
+ /**
846
+ * Subtracts days from this DateTime. this doesn't change time based on DST
847
+ *
848
+ * @param days - The number of days to subtract.
849
+ * @returns A new DateTime instance with the days subtracted.
850
+ * @throws Error if days is not a positive integer.
851
+ */
852
+ public subtractDays(days: number): DateTime
853
+ {
854
+ given(days, "days").ensureHasValue().ensureIsNumber()
855
+ .ensure(t => t >= 0 && Number.isInteger(t), "days should be positive integer");
856
+
857
+ return new DateTime({
858
+ value: this._dateTime.minus({ days }).toFormat(DateTimeFormat_DEFAULT),
859
+ zone: this._zone
860
+ });
861
+ }
862
+
863
+ /**
864
+ * Gets an array of DateTime instances for each day of the month.
865
+ *
866
+ * @returns An array of DateTime instances, where:
867
+ * - First element is the start of the month (e.g., "2023-06-01 00:00")
868
+ * - Last element is the end of the month (e.g., "2023-06-30 23:59")
869
+ * - Elements in between represent the start of each day (e.g., "2023-06-11 00:00")
870
+ */
871
+ public getDaysOfMonth(): Array<DateTime>
872
+ {
873
+ const startOfMonth = this._dateTime.startOf("month");
874
+ const endOfMonth = this._dateTime.endOf("month");
875
+
876
+ const luxonDays = LuxonInterval.fromDateTimes(startOfMonth, endOfMonth).splitBy({ days: 1 })
877
+ .map((t) => t.start!);
878
+
879
+ luxonDays[0] = startOfMonth;
880
+ luxonDays[luxonDays.length - 1] = endOfMonth;
881
+
882
+ return luxonDays.map(t => new DateTime({
883
+ value: t.toFormat(DateTimeFormat_DEFAULT),
884
+ zone: this._zone
885
+ }));
886
+ }
887
+
888
+ /**
889
+ * Converts this DateTime to a different timezone.
890
+ *
891
+ * @param zone - The target timezone.
892
+ * @returns A new DateTime instance in the specified timezone.
893
+ * @throws Error if the timezone is invalid.
894
+ */
895
+ public convertToZone(zone: string): DateTime
896
+ {
897
+ given(zone, "zone").ensureHasValue().ensureIsString()
898
+ .ensure(t => DateTime.validateTimeZone(t));
899
+
900
+ if (zone === this.zone)
901
+ return this;
902
+
903
+ const newDateTime = this._dateTime.setZone(zone).toFormat(DateTimeFormat_DEFAULT);
904
+
905
+ return new DateTime({
906
+ value: newDateTime,
907
+ zone
908
+ });
909
+ }
910
+
911
+ /**
912
+ * Checks if this DateTime is within a time range.
913
+ *
914
+ * @param startTimeCode - The start time code in HHMM format.
915
+ * @param endTimeCode - The end time code in HHMM format.
916
+ * @returns True if this DateTime is within the time range, false otherwise.
917
+ * @throws Error if the time codes are invalid or if endTimeCode is before startTimeCode.
918
+ */
919
+ public isWithinTimeRange(startTimeCode: string, endTimeCode: string): boolean
920
+ {
921
+ given(startTimeCode, "startTimeCode").ensureHasValue().ensureIsString()
922
+ .ensure(t => t.matchesFormat("######"))
923
+ .ensure(t => Number.parseInt(t) >= 0 && Number.parseInt(t) <= 235959);
924
+
925
+ given(endTimeCode, "endTimeCode").ensureHasValue().ensureIsString()
926
+ .ensure(t => t.matchesFormat("######"))
927
+ .ensure(t => Number.parseInt(t) >= 0 && Number.parseInt(t) <= 235959)
928
+ .ensure(t => Number.parseInt(t) >= Number.parseInt(startTimeCode),
929
+ "must be >= startTimeCode");
930
+
931
+ const startDateTime = DateTime.createFromCodes(
932
+ this.dateCode,
933
+ startTimeCode,
934
+ this.zone
935
+ );
936
+
937
+ const endDateTime = DateTime.createFromCodes(
938
+ this.dateCode,
939
+ endTimeCode,
940
+ this.zone
941
+ );
942
+
943
+ return this.isBetween(startDateTime, endDateTime);
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Schema type for DateTime serialization.
949
+ */
950
+ export type DateTimeSchema = Schema<DateTime, "value" | "zone">;