@tactics/toddle-styleguide 5.3.1 → 5.3.3

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 (178) hide show
  1. package/App.tsx +0 -19
  2. package/app.json +8 -4
  3. package/index.tsx +0 -4
  4. package/package.json +55 -27
  5. package/src/components/atoms/calendar/calendar.component.tsx +10 -6
  6. package/src/components/atoms/calendar/calendar.preview.tsx +4 -3
  7. package/src/components/molecules/amount/amount.component.tsx +5 -4
  8. package/src/components/molecules/calendar-select/calendar-select.component.d.ts +4 -4
  9. package/src/components/molecules/calendar-select/calendar-select.component.tsx +17 -16
  10. package/src/components/molecules/calendar-select/calendar-select.preview.tsx +16 -5
  11. package/src/components/molecules/date-input/date-input.component.d.ts +2 -2
  12. package/src/components/molecules/date-input/date-input.component.tsx +3 -3
  13. package/src/components/molecules/date-input/date-input.preview.tsx +5 -5
  14. package/src/components/molecules/day/day.component.d.ts +3 -3
  15. package/src/components/molecules/day/day.component.tsx +22 -17
  16. package/src/components/molecules/selectable-list-item/selectable-list-item.component.tsx +12 -7
  17. package/src/components/molecules/timestamp/timestamp.component.d.ts +2 -2
  18. package/src/components/molecules/timestamp/timestamp.component.tsx +12 -5
  19. package/src/components/organisms/journal-entry/components/journal-entry-type/journal-entry-type.component.d.ts +2 -2
  20. package/src/components/organisms/journal-entry/components/journal-entry-type/journal-entry-type.component.tsx +5 -5
  21. package/src/components/organisms/journal-entry/journal-entry.component.d.ts +3 -3
  22. package/src/components/organisms/journal-entry/journal-entry.component.tsx +7 -3
  23. package/src/components/organisms/journal-entry/journal-entry.preview.tsx +7 -3
  24. package/src/components/organisms/text-bubble/text-bubble.component.d.ts +2 -2
  25. package/src/components/organisms/text-bubble/text-bubble.component.tsx +6 -3
  26. package/src/components/organisms/text-bubble/text-bubble.preview.tsx +19 -7
  27. package/src/components/templates/popover-action/popover-action.component.tsx +9 -8
  28. package/src/icons/solid/cloud-download/cloud-download.icon.tsx +1 -1
  29. package/src/icons/solid/refresh/refresh-solid.icon.tsx +2 -2
  30. package/src/utilities/datetime/clock.class.tsx +14 -0
  31. package/src/utilities/datetime/dateonly.class.tsx +287 -0
  32. package/src/utilities/datetime/datetime.class.tsx +288 -0
  33. package/src/utilities/datetime/day.class.tsx +48 -0
  34. package/src/utilities/datetime/dayjs-config.ts +96 -0
  35. package/src/utilities/datetime/dayoftheweek.class.tsx +242 -0
  36. package/src/utilities/datetime/hour.class.tsx +60 -0
  37. package/src/utilities/datetime/locale.tsx +6 -0
  38. package/src/utilities/datetime/millisecond.class.tsx +48 -0
  39. package/src/utilities/datetime/minute.class.tsx +55 -0
  40. package/src/utilities/datetime/month.class.tsx +74 -0
  41. package/src/utilities/datetime/second.class.tsx +52 -0
  42. package/src/utilities/datetime/time.class.tsx +190 -0
  43. package/src/utilities/datetime/timezone.class.tsx +36 -0
  44. package/src/utilities/datetime/year.class.tsx +78 -0
  45. package/src/utilities/datetime/yearandmonth.class.tsx +80 -0
  46. package/src/components/atoms/background-gradient/__snapshots__/background-gradient.test.js.snap +0 -40
  47. package/src/components/atoms/background-gradient/background-gradient.test.js +0 -10
  48. package/src/components/atoms/calendar/__snapshots__/calendar.test.js.snap +0 -6817
  49. package/src/components/atoms/calendar/calendar.test.js +0 -35
  50. package/src/components/atoms/check-switch/__snapshots__/check-switch.test.js.snap +0 -84
  51. package/src/components/atoms/check-switch/check-switch.test.js +0 -13
  52. package/src/components/atoms/heading-components/all-caps-heading/__snapshots__/all-caps-heading.test.js.snap +0 -113
  53. package/src/components/atoms/heading-components/all-caps-heading/all-caps-heading.test.js +0 -44
  54. package/src/components/atoms/heading-components/heading1/__snapshots__/heading1.test.js.snap +0 -121
  55. package/src/components/atoms/heading-components/heading1/heading1.test.js +0 -51
  56. package/src/components/atoms/heading-components/heading2/__snapshots__/heading2.test.js.snap +0 -121
  57. package/src/components/atoms/heading-components/heading2/heading2.test.js +0 -51
  58. package/src/components/atoms/heading-components/heading3/__snapshots__/heading3.test.js.snap +0 -121
  59. package/src/components/atoms/heading-components/heading3/heading3.test.js +0 -51
  60. package/src/components/atoms/heading-components/heading4/__snapshots__/heading4.test.js.snap +0 -121
  61. package/src/components/atoms/heading-components/heading4/heading4.test.js +0 -51
  62. package/src/components/atoms/image-bubble/__snapshots__/image-bubble.test.js.snap +0 -67
  63. package/src/components/atoms/image-bubble/image-bubble.test.js +0 -20
  64. package/src/components/atoms/increment-input/__snapshots__/increment-input.test.js.snap +0 -269
  65. package/src/components/atoms/increment-input/increment-input.test.js +0 -14
  66. package/src/components/atoms/logo/__snapshots__/logo.test.js.snap +0 -113
  67. package/src/components/atoms/logo/logo.test.js +0 -16
  68. package/src/components/atoms/paragraph-components/paragraph/__snapshots__/paragraph.test.js.snap +0 -121
  69. package/src/components/atoms/paragraph-components/paragraph/paragraph.test.js +0 -76
  70. package/src/components/atoms/paragraph-components/small-text/__snapshots__/small-text.test.js.snap +0 -121
  71. package/src/components/atoms/paragraph-components/small-text/small-text.test.js +0 -76
  72. package/src/components/atoms/paragraph-components/tiny-text/__snapshots__/tiny-text.test.js.snap +0 -121
  73. package/src/components/atoms/paragraph-components/tiny-text/tiny-text.test.js +0 -76
  74. package/src/components/atoms/quick-message/__snapshots__/quick-message.test.js.snap +0 -143
  75. package/src/components/atoms/quick-message/quick-message.test.js +0 -58
  76. package/src/components/atoms/split-container/__snapshots__/split-container.test.js.snap +0 -333
  77. package/src/components/atoms/split-container/split-container.test.js +0 -45
  78. package/src/components/atoms/text-input/__snapshots__/text-input.test.js.snap +0 -123
  79. package/src/components/atoms/text-input/text-input.test.js +0 -59
  80. package/src/components/molecules/avatar/__snapshots__/avatar.test.js.snap +0 -97
  81. package/src/components/molecules/avatar/avatar.test.js +0 -22
  82. package/src/components/molecules/blocked-message/__snapshots__/blocked-message.test.js.snap +0 -107
  83. package/src/components/molecules/blocked-message/blocked-message.test.js +0 -12
  84. package/src/components/molecules/button/__snapshots__/button.test.js.snap +0 -652
  85. package/src/components/molecules/button/button.test.js +0 -56
  86. package/src/components/molecules/calendar-select/__snapshots__/calendar-select.test.js.snap +0 -343
  87. package/src/components/molecules/calendar-select/calendar-select.test.js +0 -20
  88. package/src/components/molecules/cancel-link/__snapshots__/cancel-link.test.js.snap +0 -139
  89. package/src/components/molecules/cancel-link/cancel-link.test.js +0 -28
  90. package/src/components/molecules/checkbox/__snapshots__/checkbox.test.js.snap +0 -176
  91. package/src/components/molecules/checkbox/checkbox.test.js +0 -30
  92. package/src/components/molecules/contact-address/__snapshots__/contact-address.test.js.snap +0 -113
  93. package/src/components/molecules/contact-address/contact-address.test.js +0 -18
  94. package/src/components/molecules/contact-role/__snapshots__/contact-role.test.js.snap +0 -113
  95. package/src/components/molecules/contact-role/contact-role.test.js +0 -18
  96. package/src/components/molecules/date-input/__snapshots__/date-input.test.js.snap +0 -140
  97. package/src/components/molecules/date-input/date-input.test.js +0 -23
  98. package/src/components/molecules/day/__snapshots__/day.test.js.snap +0 -263
  99. package/src/components/molecules/day/day.test.js +0 -37
  100. package/src/components/molecules/default-select/__snapshots__/default-select.test.js.snap +0 -140
  101. package/src/components/molecules/default-select/default-select.test.js +0 -17
  102. package/src/components/molecules/department_logo/__snapshots__/department-logo.test.js.snap +0 -27
  103. package/src/components/molecules/department_logo/department-logo.test.js +0 -12
  104. package/src/components/molecules/failed-to-send/__snapshots__/failed-bubble.test.js.snap +0 -386
  105. package/src/components/molecules/failed-to-send/failed-bubble.test.js +0 -75
  106. package/src/components/molecules/filter-range/__snapshots__/filter-range.test.js.snap +0 -208
  107. package/src/components/molecules/filter-range/filter-range.test.js +0 -20
  108. package/src/components/molecules/filter-tab/__snapshots__/filter-tab.test.js.snap +0 -536
  109. package/src/components/molecules/filter-tab/filter-tab.test.js +0 -42
  110. package/src/components/molecules/info/__snapshots__/info.test.js.snap +0 -64
  111. package/src/components/molecules/info/info.test.js +0 -18
  112. package/src/components/molecules/language-button/__snapshots__/language-button.test.js.snap +0 -129
  113. package/src/components/molecules/language-button/language-button.test.js +0 -29
  114. package/src/components/molecules/message-input/__snapshots__/message-input.test.js.snap +0 -611
  115. package/src/components/molecules/message-input/message-input.test.js +0 -63
  116. package/src/components/molecules/more-info-button/__snapshots__/more-info-button.test.js.snap +0 -133
  117. package/src/components/molecules/more-info-button/more-info-button.test.js +0 -29
  118. package/src/components/molecules/password-input/__snapshots__/password-input.test.js.snap +0 -504
  119. package/src/components/molecules/password-input/password-input.test.js +0 -46
  120. package/src/components/molecules/pill/__snapshots__/pill.test.js.snap +0 -226
  121. package/src/components/molecules/pill/pill.test.js +0 -42
  122. package/src/components/molecules/pressable-icon/__snapshots__/pressable-icon.test.js.snap +0 -460
  123. package/src/components/molecules/pressable-icon/pressable-icon.test.js +0 -51
  124. package/src/components/molecules/quick-filter/__snapshots__/quick-filter.test.js.snap +0 -557
  125. package/src/components/molecules/quick-filter/quick-filter.test.js +0 -134
  126. package/src/components/molecules/search-input/__snapshots__/search.test.js.snap +0 -145
  127. package/src/components/molecules/search-input/search.test.js +0 -22
  128. package/src/components/molecules/select-link/__snapshots__/select-link.test.js.snap +0 -70
  129. package/src/components/molecules/select-link/select-link.test.js +0 -17
  130. package/src/components/molecules/select-list-item/__snapshots__/select-list-item.test.js.snap +0 -762
  131. package/src/components/molecules/select-list-item/select-list-item.test.js +0 -38
  132. package/src/components/molecules/select-picker/__snapshots__/select-picker.test.js.snap +0 -407
  133. package/src/components/molecules/select-picker/select-picker.test.js +0 -31
  134. package/src/components/molecules/send-bubble/__snapshots__/send-text-bubble.test.js.snap +0 -1979
  135. package/src/components/molecules/send-bubble/send-text-bubble.test.js +0 -156
  136. package/src/components/molecules/snackbar/__snapshots__/snackbar.test.js.snap +0 -557
  137. package/src/components/molecules/snackbar/snackbar.test.js +0 -35
  138. package/src/components/molecules/swipe/__snapshots__/swipe.test.js.snap +0 -340
  139. package/src/components/molecules/swipe/swipe.test.js +0 -46
  140. package/src/components/molecules/tag/__snapshots__/tag.test.js.snap +0 -139
  141. package/src/components/molecules/tag/tag.test.js +0 -34
  142. package/src/components/molecules/time-picker/__snapshots__/time-picker.test.js.snap +0 -2221
  143. package/src/components/molecules/time-picker/time-picker.test.js +0 -18
  144. package/src/components/molecules/time-tracker/__snapshots__/time-tracker.test.js.snap +0 -266
  145. package/src/components/molecules/time-tracker/time-tracker.test.js +0 -36
  146. package/src/components/molecules/timeline/__snapshots__/timeline.test.js.snap +0 -257
  147. package/src/components/molecules/timeline/timeline.test.js +0 -18
  148. package/src/components/molecules/timestamp/__snapshots__/timestamp.test.js.snap +0 -28
  149. package/src/components/molecules/timestamp/timestamp.test.js +0 -16
  150. package/src/components/molecules/wave-background/__snapshots__/wave.test.js.snap +0 -173
  151. package/src/components/molecules/wave-background/wave.test.js +0 -25
  152. package/src/components/molecules/wide-button/__snapshots__/wide-button.test.js.snap +0 -269
  153. package/src/components/molecules/wide-button/wide-button.test.js +0 -30
  154. package/src/components/organisms/child-list-item/__snapshots__/child-list-item.test.js.snap +0 -1040
  155. package/src/components/organisms/child-list-item/child-list-item.test.js +0 -75
  156. package/src/components/organisms/contact-item/__snapshots__/contact-item.test.js.snap +0 -404
  157. package/src/components/organisms/contact-item/contact-item.test.js +0 -22
  158. package/src/components/organisms/loading-indicator/__snapshots__/loading-indicator.test.js.snap +0 -474
  159. package/src/components/organisms/loading-indicator/loading-indicator.test.js +0 -41
  160. package/src/components/organisms/my-child-list-item/__snapshots__/my-child-list-item.test.js.snap +0 -293
  161. package/src/components/organisms/my-child-list-item/my-child-list-item.test.js +0 -23
  162. package/src/components/organisms/person-info-card/__snapshots__/person-info-card.test.js.snap +0 -709
  163. package/src/components/organisms/person-info-card/person-info-card.test.js +0 -85
  164. package/src/components/organisms/text-bubble/__snapshots__/text-bubble.test.js.snap +0 -3046
  165. package/src/components/organisms/text-bubble/text-bubble.test.js +0 -144
  166. package/src/utilities/toddle-datetime/interfaces/duration.interface.d.ts +0 -22
  167. package/src/utilities/toddle-datetime/interfaces/duration.interface.tsx +0 -23
  168. package/src/utilities/toddle-datetime/interfaces/toddle-datetime.interface.d.ts +0 -22
  169. package/src/utilities/toddle-datetime/interfaces/toddle-datetime.interface.tsx +0 -25
  170. package/src/utilities/toddle-datetime/toddle-datetime.class.d.ts +0 -50
  171. package/src/utilities/toddle-datetime/toddle-datetime.class.tsx +0 -206
  172. package/src/utilities/toddle-datetime/toddle-datetime.preview.d.ts +0 -2
  173. package/src/utilities/toddle-datetime/toddle-datetime.preview.tsx +0 -160
  174. package/src/utilities/toddle-datetime/toddle-datetime.test.js +0 -127
  175. package/src/utilities/toddle-datetime/types/duration.type.d.ts +0 -4
  176. package/src/utilities/toddle-datetime/types/duration.type.tsx +0 -6
  177. package/src/utilities/toddle-datetime/types/toddle-datetime.type.d.ts +0 -5
  178. package/src/utilities/toddle-datetime/types/toddle-datetime.type.tsx +0 -23
@@ -0,0 +1,287 @@
1
+ import {Day, DayOutputFormat} from './day.class';
2
+ import {DayOfWeek, ZeroBasedWeekdayNumbers} from './dayoftheweek.class';
3
+ import {Month, MonthIndex, MonthLocalAwareOutputFormat, MonthOutputFormat} from './month.class';
4
+ import {Year, YearOutputFormat} from './year.class';
5
+ import dayjs, {Dayjs} from 'dayjs';
6
+ import {YearAndMonth, YearAndMonthLocaleAwareOutputFormat, YearAndMonthOutputFormat} from './yearandmonth.class';
7
+ import {Locale} from './locale';
8
+
9
+ export class InvalidDate extends Error {
10
+ static invalidDate() {
11
+ return new InvalidDate("The provided date is invalid.");
12
+ }
13
+
14
+ static invalidDateFormat() {
15
+ return new InvalidDate("The provided date format is invalid.");
16
+ }
17
+ }
18
+
19
+ export enum DateOnlyOutputFormat {
20
+ /** ISO 8601 standard format (e.g., 2024-10-18) */
21
+ DATEONLY_ISO8601 = "yyyy-MM-dd",
22
+
23
+ /** Format in most european countries (e.g., 18-10-2024) */
24
+ DATEONLY_DAY_MONTH_YEAR_DASH = "dd-MM-yyyy",
25
+
26
+ /** Format in uk, france, ... (e.g., 18/10/2024) */
27
+ DATEONLY_DAY_MONTH_YEAR_SLASH = "dd/MM/yyyy",
28
+
29
+ /** Format in usa, with month first (e.g., 10/18/2024) */
30
+ DATEONLY_MONTH_DAY_YEAR_SLASH = "MM/dd/yyyy",
31
+
32
+ /** Format in germany (e.g., 18.10.2024) */
33
+ DATEONLY_DAY_MONTH_YEAR_PERIOD = "dd.MM.yyyy",
34
+
35
+ /** Custom format often used in logging (e.g., 20241018) */
36
+ DATEONLY_COMPACT = "yyyyMMdd",
37
+
38
+ }
39
+
40
+ export enum DateOnlyLocaleAwareOutputFormat {
41
+
42
+ /** Full month name with day and year (e.g., October 18, 2024) */
43
+ DATEONLY_FULL = "ddd, D MMM YYYY",
44
+ }
45
+
46
+
47
+ export interface DateOnlyCalculation {
48
+ years?: number;
49
+ months?: number;
50
+ days?: number;
51
+ }
52
+
53
+ export interface DateOnlyInterface {
54
+ year: Year;
55
+ month: Month;
56
+ day: Day;
57
+ dayOfWeek: DayOfWeek;
58
+
59
+ isSame(zone: DateOnlyInterface): boolean;
60
+ isBefore(dateOnly: DateOnlyInterface): boolean;
61
+ isAfter(dateOnly: DateOnlyInterface): boolean;
62
+
63
+ format(format: DateOnlyOutputFormat): string;
64
+ formatLocale(format: DateOnlyLocaleAwareOutputFormat, locale: Locale): string;
65
+
66
+ toIso8601(): string;
67
+ toYearAndMonth(): YearAndMonth
68
+ }
69
+
70
+ export class DateOnly implements DateOnlyInterface {
71
+ public readonly year: Year;
72
+ public readonly month: Month;
73
+ public readonly day: Day;
74
+ public readonly dayOfWeek: DayOfWeek;
75
+
76
+ private constructor(year: Year, month: Month, day: Day) {
77
+ // We just convert it to a datetime to check if the date is valid with dayjs.
78
+ // f.e 31 februari 2024 is a date you could form, but is not valid.
79
+ const asDate = dayjs(`${year.format(YearOutputFormat.YEAR_NUMERIC)}-${month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}-${day.format(DayOutputFormat.DAY_TWO_DIGIT)}`, 'YYYY-MM-DD', true); // strict parsing
80
+ if (!asDate.isValid) {
81
+ throw InvalidDate.invalidDate();
82
+ }
83
+
84
+ this.year = year;
85
+ this.month = month;
86
+ this.day = day;
87
+
88
+ this.dayOfWeek = DayOfWeek.fromZeroBased(
89
+ asDate.day() as ZeroBasedWeekdayNumbers,
90
+ );
91
+ }
92
+
93
+ static from(year: Year, month: Month, day: Day) {
94
+ return new DateOnly(year, month, day);
95
+ }
96
+
97
+ // https://nl.wikipedia.org/wiki/ISO_8601
98
+ static fromIso8601(dateString: string): DateOnlyInterface {
99
+ const regex = /^\d{4}-\d{2}-\d{2}$/;
100
+
101
+ // Check needed to prevent Dayjs from parsing other formats we don't want.
102
+ if (!regex.test(dateString)) {
103
+ throw InvalidDate.invalidDateFormat();
104
+ }
105
+
106
+ const parsed = dayjs(dateString);
107
+ if (!parsed.isValid()) {
108
+ throw InvalidDate.invalidDateFormat();
109
+ }
110
+
111
+ return DateOnly.from(Year.from(parsed.year()), Month.from(parsed.month(), MonthIndex.ZERO_BASED), Day.from(parsed.date()));
112
+ }
113
+
114
+ static next(
115
+ now: DateOnlyInterface,
116
+ month: Month,
117
+ day: Day,
118
+ ): DateOnlyInterface {
119
+ if (now.month.asInt() > month.asInt()) {
120
+ return DateOnly.from(now.year.next(), month, day);
121
+ }
122
+
123
+ if (now.month.asInt() === month.asInt() && now.day.asInt() >= day.asInt()) {
124
+ return DateOnly.from(now.year.next(), month, day);
125
+ }
126
+
127
+ return DateOnly.from(now.year, month, day);
128
+ }
129
+
130
+ public toIso8601(): string {
131
+ return this.year.format(YearOutputFormat.YEAR_NUMERIC) + '-' + this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT) + '-' + this.day.format(DayOutputFormat.DAY_TWO_DIGIT);
132
+ }
133
+
134
+ public isSame(dateOnly: DateOnlyInterface): boolean {
135
+ return (
136
+ this.year.isSame(dateOnly.year) &&
137
+ this.month.isSame(dateOnly.month) &&
138
+ this.day.isSame(dateOnly.day)
139
+ );
140
+ }
141
+
142
+ public isBefore(dateOnly: DateOnlyInterface): boolean {
143
+ if (this.year.asInt() < dateOnly.year.asInt()) {
144
+ return true;
145
+ }
146
+
147
+ if (
148
+ this.year.asInt() === dateOnly.year.asInt() &&
149
+ this.month.asInt() < dateOnly.month.asInt()
150
+ ) {
151
+ return true;
152
+ }
153
+
154
+ return (
155
+ this.year.asInt() === dateOnly.year.asInt() &&
156
+ this.month.asInt() === dateOnly.month.asInt() &&
157
+ this.day.asInt() < dateOnly.day.asInt()
158
+ );
159
+ }
160
+
161
+ public isAfter(dateOnly: DateOnlyInterface): boolean {
162
+ if (this.year.asInt() > dateOnly.year.asInt()) {
163
+ return true;
164
+ }
165
+
166
+ if (
167
+ this.year.asInt() === dateOnly.year.asInt() &&
168
+ this.month.asInt() > dateOnly.month.asInt()
169
+ ) {
170
+ return true;
171
+ }
172
+
173
+ return (
174
+ this.year.asInt() === dateOnly.year.asInt() &&
175
+ this.month.asInt() === dateOnly.month.asInt() &&
176
+ this.day.asInt() > dateOnly.day.asInt()
177
+ );
178
+ }
179
+
180
+ public toYearAndMonth(): YearAndMonth {
181
+ return YearAndMonth.from(this.year, this.month);
182
+ }
183
+
184
+ private toUtcDayJs(year: Year, month: Month, day: Day): Dayjs {
185
+ const parsed = dayjs.utc(
186
+ year.format(YearOutputFormat.YEAR_NUMERIC) + '-' + month.format(MonthOutputFormat.MONTH_TWO_DIGIT) + '-' + day.format(DayOutputFormat.DAY_TWO_DIGIT) + 'T' + '12:00:00'
187
+ );
188
+
189
+ if (!parsed.isValid()) {
190
+ throw InvalidDate.invalidDateFormat();
191
+ }
192
+
193
+ return parsed.utc(false);
194
+ }
195
+
196
+ public format(
197
+ format: DateOnlyOutputFormat | YearAndMonthOutputFormat | YearOutputFormat | MonthOutputFormat | DayOutputFormat,
198
+ ): string {
199
+
200
+ const isYearAndMonthFormat = Object.values(
201
+ YearAndMonthOutputFormat,
202
+ ).includes(format as YearAndMonthOutputFormat);
203
+
204
+ if (isYearAndMonthFormat) {
205
+ return this.toYearAndMonth().format(
206
+ format as YearAndMonthOutputFormat,
207
+ );
208
+ }
209
+
210
+ const isYearFormat = Object.values(
211
+ YearOutputFormat,
212
+ ).includes(format as YearOutputFormat);
213
+
214
+ if (isYearFormat) {
215
+ return this.year.format(
216
+ format as YearOutputFormat,
217
+ );
218
+ }
219
+
220
+ const isMonthFormat = Object.values(
221
+ MonthOutputFormat,
222
+ ).includes(format as MonthOutputFormat);
223
+
224
+ if (isMonthFormat) {
225
+ return this.month.format(
226
+ format as MonthOutputFormat,
227
+ );
228
+ }
229
+
230
+ switch (format) {
231
+ case DateOnlyOutputFormat.DATEONLY_ISO8601:
232
+ return `${this.year.asInt()}-${this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}-${this.day.format(DayOutputFormat.DAY_TWO_DIGIT)}`;
233
+
234
+ case DateOnlyOutputFormat.DATEONLY_DAY_MONTH_YEAR_DASH:
235
+ return `${this.day.format(DayOutputFormat.DAY_TWO_DIGIT)}-${this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}-${this.year.asInt()}`;
236
+
237
+ case DateOnlyOutputFormat.DATEONLY_DAY_MONTH_YEAR_SLASH:
238
+ return `${this.day.format(DayOutputFormat.DAY_TWO_DIGIT)}/${this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}/${this.year.asInt()}`;
239
+
240
+ case DateOnlyOutputFormat.DATEONLY_DAY_MONTH_YEAR_PERIOD:
241
+ return `${this.day.format(DayOutputFormat.DAY_TWO_DIGIT)}.${this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}.${this.year.asInt()}`;
242
+
243
+ case DateOnlyOutputFormat.DATEONLY_MONTH_DAY_YEAR_SLASH:
244
+ return `${this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}/${this.day.format(DayOutputFormat.DAY_TWO_DIGIT)}/${this.year.asInt()}`;
245
+
246
+ case DateOnlyOutputFormat.DATEONLY_COMPACT:
247
+ return `${this.year.asInt()}${this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT)}${this.day.format(DayOutputFormat.DAY_TWO_DIGIT)}`;
248
+
249
+ default:
250
+ throw new Error(`Unsupported format: ${format}`);
251
+ }
252
+ }
253
+
254
+ public formatLocale(format: DateOnlyLocaleAwareOutputFormat | YearAndMonthLocaleAwareOutputFormat | MonthLocalAwareOutputFormat, locale: Locale): string {
255
+
256
+ const isYearAndMonthFormat = Object.values(
257
+ YearAndMonthLocaleAwareOutputFormat,
258
+ ).includes(format as YearAndMonthLocaleAwareOutputFormat);
259
+
260
+ if (isYearAndMonthFormat) {
261
+ return this.toYearAndMonth().formatLocale(
262
+ format as YearAndMonthLocaleAwareOutputFormat,
263
+ locale
264
+ );
265
+ }
266
+
267
+ const isMonthFormat = Object.values(
268
+ MonthLocalAwareOutputFormat,
269
+ ).includes(format as MonthLocalAwareOutputFormat);
270
+
271
+ if (isMonthFormat) {
272
+ return this.month.formatLocale(
273
+ format as MonthLocalAwareOutputFormat,
274
+ locale
275
+ );
276
+ }
277
+
278
+ switch (format) {
279
+ case DateOnlyLocaleAwareOutputFormat.DATEONLY_FULL:
280
+ const day = this.toUtcDayJs(this.year, this.month, this.day);
281
+ return day.locale(locale).format(DateOnlyLocaleAwareOutputFormat.DATEONLY_FULL);
282
+ default:
283
+ throw new Error(`Unsupported format: ${format}`);
284
+ }
285
+
286
+ }
287
+ }
@@ -0,0 +1,288 @@
1
+ import dayjs, {Dayjs} from 'dayjs';
2
+ import {Hour} from './hour.class';
3
+ import {Minute} from './minute.class';
4
+ import {Second} from './second.class';
5
+ import {Millisecond} from './millisecond.class';
6
+ import {Day, DayOutputFormat} from './day.class';
7
+ import {Month, MonthIndex, MonthLocalAwareOutputFormat, MonthOutputFormat} from './month.class';
8
+ import {Year, YearOutputFormat} from './year.class';
9
+ import {DayOfWeek, IDayOfWeek, ZeroBasedWeekdayNumbers} from './dayoftheweek.class';
10
+ import {YearAndMonth, YearAndMonthLocaleAwareOutputFormat} from './yearandmonth.class';
11
+ import {TimeZone, TimeZoneInterface, UTCTimezone} from './timezone.class';
12
+ import {Time, TimeInterface} from './time.class';
13
+ import {Locale} from "./locale";
14
+ import {DateOnlyLocaleAwareOutputFormat, InvalidDate} from "./dateonly.class";
15
+
16
+ // Extends for dayjs.
17
+
18
+ export class InvalidDateTime extends Error {
19
+ static invalidDateTimeFormat() {
20
+ return new InvalidDateTime("The provided datetime format is invalid.");
21
+ }
22
+ }
23
+
24
+ export enum DateTimeOutputFormat {
25
+ /** ISO 8601 standard format (e.g., 2024-10-18) */
26
+ DATETIME_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSS",
27
+
28
+ /** ISO 8601 date and time with offset but without milliseconds (e.g., 2024-10-19T15:30:45) */
29
+ DATETIME_ISO8601_NO_MILLIS = "yyyy-MM-dd'T'HH:mm:ss",
30
+
31
+ /** ISO 8601 date and time with timezone but without seconds (e.g., 2024-10-19T15:30) */
32
+ DATETIME_ISO8601_NO_SECONDS = "yyyy-MM-dd'T'HH:mm",
33
+ }
34
+
35
+ export interface DateTimeCalculation {
36
+ years?: number;
37
+ months?: number;
38
+ days?: number;
39
+ hours?: number;
40
+ minutes?: number;
41
+ seconds?: number;
42
+ milliseconds?: number;
43
+ }
44
+
45
+ export interface DateTimeInterface {
46
+ year: Year;
47
+ month : Month;
48
+ day: Day;
49
+ hour: Hour;
50
+ minute: Minute;
51
+ second: Second;
52
+ millisecond: Millisecond;
53
+ dayOfWeek: IDayOfWeek;
54
+ add(calculation : DateTimeCalculation): DateTimeInterface;
55
+ subtract(calculation : DateTimeCalculation): DateTimeInterface;
56
+
57
+ isSameDay($other : DateTimeInterface) : boolean;
58
+ startOfDay(): DateTimeInterface;
59
+ endOfDay(): DateTimeInterface;
60
+
61
+ toYearAndMonth(): YearAndMonth;
62
+ toTime(): TimeInterface;
63
+ toTimestamp(): number;
64
+ toNativeJsDate(): Date;
65
+ toDateOnlyIso8601(): string;
66
+
67
+ format(format: DateTimeOutputFormat): string;
68
+ formatLocale(format: DateOnlyLocaleAwareOutputFormat, locale: Locale): string;
69
+
70
+ inTimezone(timezone: TimeZoneInterface): DateTimeInterface;
71
+ inUtc(): DateTimeInterface;
72
+ }
73
+
74
+ export class DateTime implements DateTimeInterface {
75
+ private dateTime: Dayjs;
76
+ year: Year;
77
+ month: Month;
78
+ day: Day;
79
+ dayOfWeek: IDayOfWeek;
80
+ hour: Hour;
81
+ minute: Minute;
82
+ second: Second;
83
+ millisecond: Millisecond;
84
+ zone: TimeZoneInterface;
85
+
86
+ private constructor(dateTime: Dayjs, timezone: TimeZoneInterface) {
87
+ this.dateTime = dateTime;
88
+ this.zone = timezone;
89
+
90
+ this.year = Year.from(this.dateTime.year());
91
+ this.month = Month.from(this.dateTime.month(), MonthIndex.ZERO_BASED);
92
+ this.day = Day.from(this.dateTime.date());
93
+ this.hour = Hour.from(this.dateTime.hour());
94
+ this.minute = Minute.from(this.dateTime.minute());
95
+ this.second = Second.from(this.dateTime.second());
96
+ this.millisecond = Millisecond.from(this.dateTime.millisecond());
97
+ this.dayOfWeek = DayOfWeek.fromZeroBased(
98
+ this.dateTime.day() as ZeroBasedWeekdayNumbers,
99
+ );
100
+ }
101
+
102
+ static fromTimestamp(timestamp: number): DateTime {
103
+ const utcDate = dayjs.unix(timestamp);
104
+ return new DateTime(utcDate, TimeZone.UTC());
105
+ }
106
+
107
+ static fromIso8601(
108
+ dateString: string,
109
+ timezone: TimeZoneInterface,
110
+ ): DateTimeInterface {
111
+
112
+ const dateStringRegex =
113
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z)?$/;
114
+ const timezoneRegex = /Z$/;
115
+
116
+ const containsZ = timezoneRegex.test(dateString);
117
+ const isSupported = dateStringRegex.test(dateString);
118
+
119
+ if (containsZ && timezone.name() !== "UTC") {
120
+ throw new Error('Timezones are not matching!')
121
+ }
122
+
123
+ // Check needed to prevent dayjs from parsing other formats we don't want.
124
+ if (!isSupported) {
125
+ throw new Error('The provided datetime format is invalid!')
126
+ }
127
+
128
+ const asDate = dayjs.tz(dateString, timezone.name());
129
+ return new DateTime(asDate, timezone);
130
+ }
131
+
132
+ public add(calculation : DateTimeCalculation): DateTimeInterface {
133
+ const {
134
+ years = 0,
135
+ months = 0,
136
+ days = 0,
137
+ hours = 0,
138
+ minutes = 0,
139
+ seconds = 0,
140
+ milliseconds = 0,
141
+ } = calculation;
142
+
143
+ let _current = this.dateTime;
144
+
145
+ _current = _current.add(years, 'years');
146
+ _current = _current.add(months, 'months');
147
+ _current = _current.add(days, 'days');
148
+ _current = _current.add(hours, 'hours');
149
+ _current = _current.add(minutes, 'minutes');
150
+ _current = _current.add(seconds, 'seconds');
151
+ _current = _current.add(milliseconds, 'milliseconds');
152
+
153
+ return DateTime.fromTimestamp(_current.unix());
154
+ }
155
+
156
+ public subtract(calculation : DateTimeCalculation): DateTimeInterface {
157
+ const {
158
+ years = 0,
159
+ months = 0,
160
+ days = 0,
161
+ hours = 0,
162
+ minutes = 0,
163
+ seconds = 0,
164
+ milliseconds = 0,
165
+ } = calculation;
166
+
167
+ let _current = this.dateTime;
168
+
169
+ _current = _current.subtract(years, 'years');
170
+ _current = _current.subtract(months, 'months');
171
+ _current = _current.subtract(days, 'days');
172
+ _current = _current.subtract(hours, 'hours');
173
+ _current = _current.subtract(minutes, 'minutes');
174
+ _current =_current.subtract(seconds, 'seconds');
175
+ _current =_current.subtract(milliseconds, 'milliseconds');
176
+
177
+ return DateTime.fromTimestamp(_current.unix());
178
+ }
179
+
180
+ public startOfDay(): DateTimeInterface {
181
+ this.dateTime = this.dateTime.startOf('day');
182
+ return this;
183
+ }
184
+
185
+ public endOfDay(): DateTimeInterface {
186
+ this.dateTime = this.dateTime.endOf('day');
187
+ return this;
188
+ }
189
+
190
+ public toTime(): TimeInterface {
191
+ return Time.from(
192
+ this.hour.asInt(),
193
+ this.minute.asInt(),
194
+ this.second.asInt(),
195
+ this.millisecond.asInt(),
196
+ );
197
+ }
198
+
199
+ public toTimestamp(): number {
200
+ return this.dateTime.unix();
201
+ }
202
+
203
+ public inTimezone(timezone: TimeZoneInterface): DateTimeInterface {
204
+ const dayjs = this.dateTime.tz(timezone.name(), false);
205
+ return new DateTime(dayjs, timezone);
206
+ }
207
+
208
+ public inUtc(): DateTimeInterface {
209
+ const dayjs = this.dateTime.tz(UTCTimezone.name(), false);
210
+ return new DateTime(dayjs, UTCTimezone);
211
+ }
212
+
213
+ public toYearAndMonth(): YearAndMonth {
214
+ return YearAndMonth.from(this.year, this.month);
215
+ }
216
+
217
+ public toNativeJsDate(): Date {
218
+ return this.dateTime.toDate();
219
+ }
220
+
221
+ public format(format: DateTimeOutputFormat): string {
222
+ switch (format) {
223
+ case DateTimeOutputFormat.DATETIME_ISO8601:
224
+ return this.dateTime.format('YYYY-MM-DDTHH:mm:ss.SSS');
225
+ case DateTimeOutputFormat.DATETIME_ISO8601_NO_MILLIS:
226
+ return this.dateTime.format('YYYY-MM-DDTHH:mm:ss');
227
+ case DateTimeOutputFormat.DATETIME_ISO8601_NO_SECONDS:
228
+ return this.dateTime.format('YYYY-MM-DDTHH:mm');
229
+ default:
230
+ throw new Error(`Unsupported format: ${format}`);
231
+ }
232
+ }
233
+
234
+ public formatLocale(format: DateOnlyLocaleAwareOutputFormat | YearAndMonthLocaleAwareOutputFormat | MonthLocalAwareOutputFormat, locale: Locale): string {
235
+
236
+ const isYearAndMonthFormat = Object.values(
237
+ YearAndMonthLocaleAwareOutputFormat,
238
+ ).includes(format as YearAndMonthLocaleAwareOutputFormat);
239
+
240
+ if (isYearAndMonthFormat) {
241
+ return this.toYearAndMonth().formatLocale(
242
+ format as YearAndMonthLocaleAwareOutputFormat,
243
+ locale
244
+ );
245
+ }
246
+
247
+ const isMonthFormat = Object.values(
248
+ MonthLocalAwareOutputFormat,
249
+ ).includes(format as MonthLocalAwareOutputFormat);
250
+
251
+ if (isMonthFormat) {
252
+ return this.month.formatLocale(
253
+ format as MonthLocalAwareOutputFormat,
254
+ locale
255
+ );
256
+ }
257
+
258
+ switch (format) {
259
+ case DateOnlyLocaleAwareOutputFormat.DATEONLY_FULL:
260
+ const day = this.toUtcDayJs(this.year, this.month, this.day);
261
+ return day.locale(locale).format(DateOnlyLocaleAwareOutputFormat.DATEONLY_FULL);
262
+ default:
263
+ throw new Error(`Unsupported format: ${format}`);
264
+ }
265
+ }
266
+
267
+
268
+ public isSameDay($other : DateTimeInterface) : boolean {
269
+ return this.day.isSame($other.day) && this.month.isSame($other.month) && this.year.isSame($other.year);
270
+ }
271
+
272
+ private toUtcDayJs(year: Year, month: Month, day: Day): Dayjs {
273
+ const parsed = dayjs.utc(
274
+ year.format(YearOutputFormat.YEAR_NUMERIC) + '-' + month.format(MonthOutputFormat.MONTH_TWO_DIGIT) + '-' + day.format(DayOutputFormat.DAY_TWO_DIGIT) + 'T' + '12:00:00'
275
+ );
276
+
277
+ if (!parsed.isValid()) {
278
+ throw InvalidDate.invalidDateFormat();
279
+ }
280
+
281
+ return parsed.utc(false);
282
+ }
283
+
284
+ public toDateOnlyIso8601(): string {
285
+ return this.year.format(YearOutputFormat.YEAR_NUMERIC) + '-' + this.month.format(MonthOutputFormat.MONTH_TWO_DIGIT) + '-' + this.day.format(DayOutputFormat.DAY_TWO_DIGIT);
286
+ }
287
+
288
+ }
@@ -0,0 +1,48 @@
1
+ export class InvalidDay extends Error {
2
+ static notInScope() {
3
+ return new InvalidDay("A day must be between 1 and 31.");
4
+ }
5
+ }
6
+
7
+ export enum DayOutputFormat {
8
+ /** Month and day numeric (e.g., 1) */
9
+ DAY_NUMERIC = "day-numeric",
10
+ /** Month and day numeric (e.g., 01) */
11
+ DAY_TWO_DIGIT = "day-2-digit",
12
+ }
13
+
14
+ export interface DayInterface {
15
+ asInt(): number;
16
+ isSame(day: Day): boolean;
17
+ format(format: DayOutputFormat): string;
18
+ // formatLocale is not included since days don't get formatted local.
19
+ }
20
+
21
+ export class Day implements DayInterface {
22
+ private constructor(private readonly day: number) {
23
+ if (this.day < 1 || this.day > 31) {
24
+ throw InvalidDay.notInScope();
25
+ }
26
+ }
27
+
28
+ static from(day: number): Day {
29
+ return new Day(day);
30
+ }
31
+
32
+ asInt(): number {
33
+ return this.day;
34
+ }
35
+
36
+ format(format: DayOutputFormat): string {
37
+ switch (format) {
38
+ case DayOutputFormat.DAY_NUMERIC:
39
+ return String(this.asInt());
40
+ case DayOutputFormat.DAY_TWO_DIGIT:
41
+ return this.day.toString().padStart(2, "0");
42
+ }
43
+ }
44
+
45
+ isSame(day: Day): boolean {
46
+ return this.asInt() === day.asInt();
47
+ }
48
+ }