@beinformed/ui 1.58.3 → 1.59.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/esm/constants/Constants.js +10 -0
  3. package/esm/constants/Constants.js.map +1 -1
  4. package/esm/constants/Settings.js +5 -1
  5. package/esm/constants/Settings.js.map +1 -1
  6. package/esm/models/attributes/DatetimeAttributeModel.js +39 -4
  7. package/esm/models/attributes/DatetimeAttributeModel.js.map +1 -1
  8. package/esm/models/attributes/input-constraints/DatetimeFormatConstraint.js +31 -2
  9. package/esm/models/attributes/input-constraints/DatetimeFormatConstraint.js.map +1 -1
  10. package/esm/models/content/SectionModel.js +1 -1
  11. package/esm/models/content/SectionModel.js.map +1 -1
  12. package/esm/models/content/SubSectionModel.js +12 -4
  13. package/esm/models/content/SubSectionModel.js.map +1 -1
  14. package/esm/react-client/client.js +2 -1
  15. package/esm/react-client/client.js.map +1 -1
  16. package/esm/react-server/serverUtil.js +2 -1
  17. package/esm/react-server/serverUtil.js.map +1 -1
  18. package/esm/redux/actions/Preferences.js +15 -1
  19. package/esm/redux/actions/Preferences.js.map +1 -1
  20. package/esm/utils/datetime/DateTimeUtil.js +292 -94
  21. package/esm/utils/datetime/DateTimeUtil.js.map +1 -1
  22. package/lib/constants/Constants.js +11 -1
  23. package/lib/constants/Constants.js.flow +11 -0
  24. package/lib/constants/Constants.js.map +1 -1
  25. package/lib/constants/Settings.js +7 -2
  26. package/lib/constants/Settings.js.flow +6 -0
  27. package/lib/constants/Settings.js.map +1 -1
  28. package/lib/models/attributes/DatetimeAttributeModel.js +38 -3
  29. package/lib/models/attributes/DatetimeAttributeModel.js.flow +54 -4
  30. package/lib/models/attributes/DatetimeAttributeModel.js.map +1 -1
  31. package/lib/models/attributes/__tests__/DatetimeAttributeModel.spec.js.flow +9 -0
  32. package/lib/models/attributes/__tests__/DatetimeAttributeModel_offset.spec.js.flow +306 -0
  33. package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js +31 -2
  34. package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js.flow +42 -3
  35. package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js.map +1 -1
  36. package/lib/models/content/SectionModel.js +1 -1
  37. package/lib/models/content/SectionModel.js.flow +2 -1
  38. package/lib/models/content/SectionModel.js.map +1 -1
  39. package/lib/models/content/SubSectionModel.js +12 -3
  40. package/lib/models/content/SubSectionModel.js.flow +20 -3
  41. package/lib/models/content/SubSectionModel.js.map +1 -1
  42. package/lib/models/content/__tests__/ContentModel.spec.js.flow +3 -3
  43. package/lib/react-client/client.js +1 -0
  44. package/lib/react-client/client.js.flow +2 -0
  45. package/lib/react-client/client.js.map +1 -1
  46. package/lib/react-server/__tests__/serverUtil.spec.js.flow +12 -0
  47. package/lib/react-server/serverUtil.js +1 -0
  48. package/lib/react-server/serverUtil.js.flow +2 -0
  49. package/lib/react-server/serverUtil.js.map +1 -1
  50. package/lib/redux/actions/Preferences.js +17 -2
  51. package/lib/redux/actions/Preferences.js.flow +22 -0
  52. package/lib/redux/actions/Preferences.js.map +1 -1
  53. package/lib/redux/reducers/__tests__/ModelCatalogReducer.spec.js.flow +23 -0
  54. package/lib/utils/datetime/DateTimeUtil.js +292 -93
  55. package/lib/utils/datetime/DateTimeUtil.js.flow +482 -172
  56. package/lib/utils/datetime/DateTimeUtil.js.map +1 -1
  57. package/lib/utils/datetime/__tests__/DateTime.spec.js.flow +771 -483
  58. package/package.json +11 -9
  59. package/src/constants/Constants.js +11 -0
  60. package/src/constants/Settings.js +6 -0
  61. package/src/models/attributes/DatetimeAttributeModel.js +54 -4
  62. package/src/models/attributes/__tests__/DatetimeAttributeModel.spec.js +9 -0
  63. package/src/models/attributes/__tests__/DatetimeAttributeModel_offset.spec.js +306 -0
  64. package/src/models/attributes/input-constraints/DatetimeFormatConstraint.js +42 -3
  65. package/src/models/content/SectionModel.js +2 -1
  66. package/src/models/content/SubSectionModel.js +20 -3
  67. package/src/models/content/__tests__/ContentModel.spec.js +3 -3
  68. package/src/react-client/client.js +2 -0
  69. package/src/react-server/__tests__/serverUtil.spec.js +12 -0
  70. package/src/react-server/serverUtil.js +2 -0
  71. package/src/redux/actions/Preferences.js +22 -0
  72. package/src/redux/reducers/__tests__/ModelCatalogReducer.spec.js +23 -0
  73. package/src/utils/datetime/DateTimeUtil.js +482 -172
  74. package/src/utils/datetime/__tests__/DateTime.spec.js +771 -483
@@ -1,25 +1,14 @@
1
1
  // @flow
2
2
  import {
3
- parse,
4
3
  format,
5
- isValid,
6
4
  isAfter,
7
5
  isBefore,
8
6
  isSameDay,
9
- addSeconds,
10
- addMinutes,
11
- addHours,
12
- addDays,
13
- addWeeks,
14
- addMonths,
15
- addYears,
16
- subSeconds,
17
- subMinutes,
18
- subHours,
19
- subDays,
20
- subWeeks,
21
- subMonths,
22
- subYears,
7
+ isValid,
8
+ parse,
9
+ add,
10
+ sub,
11
+ set,
23
12
  startOfMonth,
24
13
  endOfMonth,
25
14
  startOfWeek,
@@ -27,52 +16,56 @@ import {
27
16
  getMinutes,
28
17
  getDay,
29
18
  getWeek,
30
- setMilliseconds,
31
- setSeconds,
32
- setMinutes,
33
- setHours,
34
- setMonth,
35
- setYear,
36
- set,
37
19
  } from "date-fns";
38
- import { nl, enGB } from "date-fns/locale";
20
+ import { enGB, nl } from "date-fns/locale";
21
+ import { tz } from "@date-fns/tz";
39
22
 
40
- import { getCookie } from "../browser/Cookies";
41
-
42
- import { getSetting } from "../../constants/Settings";
23
+ import soft from "timezone-soft";
43
24
 
44
25
  import {
26
+ DATETIME_OFFSET_FORMAT,
27
+ DEFAULT_FIRST_WEEK_CONTAINS_DATE,
28
+ DEFAULT_WEEK_STARTS_ON,
29
+ getSetting,
30
+ IS_GRAALJS,
31
+ isIncludeTimeOffsetInDateTimes,
45
32
  ISO_DATE_FORMAT,
46
33
  ISO_DATETIME_FORMAT,
47
34
  ISO_TIME_FORMAT,
48
35
  ISO_TIMESTAMP_FORMAT,
49
- DEFAULT_WEEK_STARTS_ON,
50
- DEFAULT_FIRST_WEEK_CONTAINS_DATE,
51
36
  } from "../../constants";
52
37
 
38
+ import { getCookie } from "../browser/Cookies";
39
+
40
+ type OffsetInfoInput = {
41
+ abbr: string,
42
+ offset: number,
43
+ name: string,
44
+ };
45
+
46
+ export type OffsetInfo = {
47
+ abbr: string,
48
+ label: string,
49
+ value: string,
50
+ };
51
+
53
52
  /**
54
53
  * @hideconstructor
55
54
  */
56
55
  class BaseDateTimeUtil {
57
56
  _isoFormat: string;
57
+ _timeZone: string =
58
+ typeof Intl === "undefined"
59
+ ? "Etc/UTC"
60
+ : Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC";
58
61
 
59
62
  constructor(isoFormat: string) {
60
63
  this._isoFormat = isoFormat;
61
64
  }
62
65
 
63
66
  /**
67
+ * Returns the options defined in settings
64
68
  */
65
- getLocale(): typeof nl | typeof enGB {
66
- const locale = getCookie("locale") ?? "en";
67
-
68
- if (locale === "nl") {
69
- return nl;
70
- }
71
-
72
- return enGB;
73
- }
74
-
75
- // returns the options defined in settings
76
69
  getOptions(): {
77
70
  weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6,
78
71
  firstWeekContainsDate: 1 | 2 | 3 | 4 | 5 | 6 | 7,
@@ -94,10 +87,45 @@ class BaseDateTimeUtil {
94
87
 
95
88
  /**
96
89
  */
97
- toDate(date: string, inputFormat: string = this._isoFormat): Date {
90
+ getIsoFormat(): string {
91
+ if (
92
+ isIncludeTimeOffsetInDateTimes() &&
93
+ this._isoFormat.startsWith(ISO_DATETIME_FORMAT)
94
+ ) {
95
+ return this._isoFormat + DATETIME_OFFSET_FORMAT;
96
+ }
97
+ return this._isoFormat;
98
+ }
99
+
100
+ /**
101
+ */
102
+ convertFormat(sourceFormat: string): string {
103
+ return sourceFormat ? sourceFormat : this.getIsoFormat();
104
+ }
105
+
106
+ /**
107
+ */
108
+ getLocale(): typeof nl | typeof enGB {
109
+ const locale = getCookie("locale") ?? "en";
110
+
111
+ if (locale === "nl") {
112
+ return nl;
113
+ }
114
+
115
+ return enGB;
116
+ }
117
+
118
+ /**
119
+ */
120
+ toDate(date: string | Date, inputFormat: string = this.getIsoFormat()): Date {
121
+ if (date instanceof Date) {
122
+ return date;
123
+ }
124
+
98
125
  const { weekStartsOn, firstWeekContainsDate } = this.getOptions();
99
126
 
100
127
  return parse(date, inputFormat, new Date(), {
128
+ in: this.getDateFnsContext(),
101
129
  weekStartsOn,
102
130
  firstWeekContainsDate,
103
131
  });
@@ -105,13 +133,18 @@ class BaseDateTimeUtil {
105
133
 
106
134
  /**
107
135
  */
108
- toFormat(date: string | Date, targetFormat: string): string {
136
+ toFormat(
137
+ isoDate: string | Date,
138
+ targetFormat: string,
139
+ inputFormat: string = this.getIsoFormat(),
140
+ ): string {
109
141
  const { weekStartsOn } = this.getOptions();
110
142
 
111
- const parsedDate = typeof date === "string" ? this.toDate(date) : date;
143
+ const parsedDate = this.toDate(isoDate, inputFormat);
112
144
 
113
145
  if (isValid(parsedDate)) {
114
146
  return format(parsedDate, targetFormat, {
147
+ in: this.getDateFnsContext(),
115
148
  weekStartsOn,
116
149
  locale: this.getLocale(),
117
150
  });
@@ -122,19 +155,11 @@ class BaseDateTimeUtil {
122
155
 
123
156
  /**
124
157
  */
125
- toISO(date: string | Date, sourceFormat: string = this._isoFormat): string {
126
- const { weekStartsOn } = this.getOptions();
127
-
128
- const parsedDate =
129
- typeof date === "string" ? this.toDate(date, sourceFormat) : date;
130
-
131
- if (isValid(parsedDate)) {
132
- return format(parsedDate, this._isoFormat, {
133
- weekStartsOn,
134
- });
135
- }
136
-
137
- return "Invalid Date";
158
+ toISO(
159
+ date: string | Date,
160
+ sourceFormat: string = this.getIsoFormat(),
161
+ ): string {
162
+ return this.toFormat(date, this.getIsoFormat(), sourceFormat);
138
163
  }
139
164
 
140
165
  /**
@@ -143,52 +168,49 @@ class BaseDateTimeUtil {
143
168
  return this.toISO(new Date());
144
169
  }
145
170
 
146
- /**
147
- */
148
- convertFormat(sourceFormat: string): string {
149
- return sourceFormat ? sourceFormat : this._isoFormat;
150
- }
171
+ //////////// VALIDATION
151
172
 
152
173
  /**
153
- * VALIDATION
154
174
  */
155
175
  hasFormat(date: string, sourceFormat: string): boolean {
156
- const { weekStartsOn } = this.getOptions();
157
-
158
- const realDate = this.toDate(date, sourceFormat);
159
- if (isValid(realDate)) {
160
- const formattedValue = format(realDate, sourceFormat, {
161
- weekStartsOn,
162
- });
163
-
164
- const isOutsideRange =
165
- isBefore(realDate, new Date(1000, 1, 1)) ||
166
- isAfter(realDate, new Date(9999, 11, 31));
167
- if (isOutsideRange) {
168
- return false;
169
- }
176
+ const formattedValue = this.toFormat(date, sourceFormat, sourceFormat);
177
+ if (formattedValue === "Invalid Date") {
178
+ return false;
179
+ }
170
180
 
171
- return (
172
- formattedValue.replace(/0/gu, "").toUpperCase() ===
173
- date.replace(/0/gu, "").toUpperCase()
174
- );
181
+ const isOutsideRange =
182
+ isBefore(formattedValue, new Date(1000, 1, 1)) ||
183
+ isAfter(formattedValue, new Date(9999, 11, 31));
184
+ if (isOutsideRange) {
185
+ return false;
175
186
  }
176
187
 
177
- return false;
188
+ return (
189
+ formattedValue.replace(/0/gu, "").toUpperCase() ===
190
+ date.replace(/0/gu, "").toUpperCase()
191
+ );
178
192
  }
179
193
 
180
194
  /**
181
195
  */
182
- isValid(date: string, inputFormat: string = this._isoFormat): boolean {
183
- return isValid(this.toDate(date, inputFormat));
196
+ isValid(
197
+ date: string | Date | null | void,
198
+ inputFormat: string = this.getIsoFormat(),
199
+ ): boolean {
200
+ if (!date) {
201
+ return false;
202
+ }
203
+
204
+ const parsedDate = this.toDate(date, inputFormat);
205
+ return isValid(parsedDate);
184
206
  }
185
207
 
186
208
  /**
187
209
  */
188
210
  isAfter(
189
- inputDate: string,
190
- afterISODate: ?string,
191
- inputFormat: string = this._isoFormat,
211
+ inputDate: string | Date,
212
+ afterISODate: ?string | Date,
213
+ inputFormat: string = this.getIsoFormat(),
192
214
  ): boolean {
193
215
  return (
194
216
  afterISODate != null &&
@@ -199,9 +221,9 @@ class BaseDateTimeUtil {
199
221
  /**
200
222
  */
201
223
  isSameOrAfter(
202
- inputDate: string,
203
- afterISODate: ?string,
204
- inputFormat: string = this._isoFormat,
224
+ inputDate: string | Date,
225
+ afterISODate: ?string | Date,
226
+ inputFormat: string = this.getIsoFormat(),
205
227
  ): boolean {
206
228
  return (
207
229
  afterISODate != null &&
@@ -213,9 +235,9 @@ class BaseDateTimeUtil {
213
235
  /**
214
236
  */
215
237
  isBefore(
216
- inputDate: string,
217
- beforeISODate: ?string,
218
- inputFormat: string = this._isoFormat,
238
+ inputDate: string | Date,
239
+ beforeISODate: ?string | Date,
240
+ inputFormat: string = this.getIsoFormat(),
219
241
  ): boolean {
220
242
  return (
221
243
  beforeISODate != null &&
@@ -226,9 +248,9 @@ class BaseDateTimeUtil {
226
248
  /**
227
249
  */
228
250
  isSameOrBefore(
229
- inputDate: string,
230
- beforeISODate: ?string,
231
- inputFormat: string = this._isoFormat,
251
+ inputDate: string | Date,
252
+ beforeISODate: ?string | Date,
253
+ inputFormat: string = this.getIsoFormat(),
232
254
  ): boolean {
233
255
  return (
234
256
  beforeISODate != null &&
@@ -240,9 +262,9 @@ class BaseDateTimeUtil {
240
262
  /**
241
263
  */
242
264
  isSame(
243
- inputDate: string,
244
- compareDate: ?string,
245
- inputFormat: string = this._isoFormat,
265
+ inputDate: string | Date,
266
+ compareDate: ?string | Date,
267
+ inputFormat: string = this.getIsoFormat(),
246
268
  ): boolean {
247
269
  return (
248
270
  compareDate != null &&
@@ -254,9 +276,9 @@ class BaseDateTimeUtil {
254
276
  /**
255
277
  */
256
278
  isSameDay(
257
- inputDate: string,
258
- compareDate: string,
259
- inputFormat: string = this._isoFormat,
279
+ inputDate: string | Date,
280
+ compareDate: string | Date,
281
+ inputFormat: string = this.getIsoFormat(),
260
282
  ): boolean {
261
283
  return (
262
284
  compareDate != null &&
@@ -270,16 +292,16 @@ class BaseDateTimeUtil {
270
292
  /**
271
293
  */
272
294
  isOther(
273
- inputDate: string,
274
- compareDate: ?string,
275
- inputFormat: string = this._isoFormat,
295
+ inputDate: string | Date,
296
+ compareDate: ?string | Date,
297
+ inputFormat: string = this.getIsoFormat(),
276
298
  ): boolean {
277
299
  return !this.isSame(inputDate, compareDate, inputFormat);
278
300
  }
279
301
 
280
302
  /**
281
303
  */
282
- isWeekend(inputDate: string): boolean {
304
+ isWeekend(inputDate: string | Date): boolean {
283
305
  const SATURDAY_NUMBER = 6;
284
306
  const SUNDAY_NUMBER = 0;
285
307
  const weekDay = getDay(this.toDate(inputDate));
@@ -288,175 +310,371 @@ class BaseDateTimeUtil {
288
310
  }
289
311
 
290
312
  /**
291
- * CALCULATIONS
313
+ * Checks if a given local date and time is ambiguous in the specified timezone.
314
+ * Ambiguity happens during the end of daylight saving time (DST) transitions.
292
315
  */
293
- addSeconds(date: string, amount: number): string {
294
- return this.toISO(addSeconds(this.toDate(date), amount));
316
+ isAmbiguous(
317
+ inputDate: string | Date,
318
+ inputFormat: string = this.getIsoFormat(),
319
+ ): boolean {
320
+ if (inputDate == null || typeof Intl === "undefined") {
321
+ return false;
322
+ }
323
+
324
+ const dateTime = this.toDate(inputDate, inputFormat);
325
+ if (!isValid(dateTime)) {
326
+ return false;
327
+ }
328
+
329
+ const t = [60, -60, 30, -30];
330
+ const a = t.map((x) => {
331
+ const newDate = add(dateTime, { minutes: x });
332
+ return format(newDate, "HH:mm");
333
+ });
334
+ const time = format(dateTime, "HH:mm");
335
+ return a.indexOf(time) > -1;
295
336
  }
296
337
 
338
+ //////////// CALCULATIONS
339
+
297
340
  /**
341
+ * @private
298
342
  */
299
- addMinutes(date: string, amount: number): string {
300
- return this.toISO(addMinutes(this.toDate(date), amount));
343
+ _calculate(
344
+ date: string | Date,
345
+ calculationFn: Function,
346
+ property: string,
347
+ value: number,
348
+ inputFormat: string = this.getIsoFormat(),
349
+ ): string {
350
+ const parsedDate = this.toDate(date, inputFormat);
351
+ return this.toISO(
352
+ calculationFn(
353
+ parsedDate,
354
+ { [property]: value },
355
+ {
356
+ in: this.getDateFnsContext(),
357
+ },
358
+ ),
359
+ );
301
360
  }
302
361
 
303
362
  /**
304
363
  */
305
- addHours(date: string, amount: number): string {
306
- return this.toISO(addHours(this.toDate(date), amount));
364
+ addSeconds(
365
+ date: string,
366
+ amount: number,
367
+ inputFormat: string = this.getIsoFormat(),
368
+ ): string {
369
+ return this._calculate(date, add, "seconds", amount, inputFormat);
307
370
  }
308
371
 
309
372
  /**
310
373
  */
311
- addDays(date: string, amount: number): string {
312
- return this.toISO(addDays(this.toDate(date), amount));
374
+ addMinutes(
375
+ date: string,
376
+ amount: number,
377
+ inputFormat: string = this.getIsoFormat(),
378
+ ): string {
379
+ return this._calculate(date, add, "minutes", amount, inputFormat);
313
380
  }
314
381
 
315
382
  /**
316
383
  */
317
- addWeeks(date: string, amount: number): string {
318
- return this.toISO(addWeeks(this.toDate(date), amount));
384
+ addHours(
385
+ date: string,
386
+ amount: number,
387
+ inputFormat: string = this.getIsoFormat(),
388
+ ): string {
389
+ return this._calculate(date, add, "hours", amount, inputFormat);
319
390
  }
320
391
 
321
392
  /**
322
393
  */
323
- addMonths(date: string, amount: number): string {
324
- return this.toISO(addMonths(this.toDate(date), amount));
394
+ addDays(
395
+ date: string,
396
+ amount: number,
397
+ inputFormat: string = this.getIsoFormat(),
398
+ ): string {
399
+ return this._calculate(date, add, "days", amount, inputFormat);
325
400
  }
326
401
 
327
402
  /**
328
403
  */
329
- addYears(date: string, amount: number): string {
330
- return this.toISO(addYears(this.toDate(date), amount));
404
+ addWeeks(
405
+ date: string,
406
+ amount: number,
407
+ inputFormat: string = this.getIsoFormat(),
408
+ ): string {
409
+ return this._calculate(date, add, "weeks", amount, inputFormat);
331
410
  }
332
411
 
333
412
  /**
334
413
  */
335
- subtractSeconds(date: string, amount: number): string {
336
- return this.toISO(subSeconds(this.toDate(date), amount));
414
+ addMonths(
415
+ date: string,
416
+ amount: number,
417
+ inputFormat: string = this.getIsoFormat(),
418
+ ): string {
419
+ return this._calculate(date, add, "months", amount, inputFormat);
337
420
  }
338
421
 
339
422
  /**
340
423
  */
341
- subtractMinutes(date: string, amount: number): string {
342
- return this.toISO(subMinutes(this.toDate(date), amount));
424
+ addYears(
425
+ date: string,
426
+ amount: number,
427
+ inputFormat: string = this.getIsoFormat(),
428
+ ): string {
429
+ return this._calculate(date, add, "years", amount, inputFormat);
343
430
  }
344
431
 
345
432
  /**
346
433
  */
347
- subtractHours(date: string, amount: number): string {
348
- return this.toISO(subHours(this.toDate(date), amount));
434
+ subtractSeconds(
435
+ date: string,
436
+ amount: number,
437
+ inputFormat: string = this.getIsoFormat(),
438
+ ): string {
439
+ return this._calculate(date, sub, "seconds", amount, inputFormat);
349
440
  }
350
441
 
351
442
  /**
352
443
  */
353
- subtractDays(date: string, amount: number): string {
354
- return this.toISO(subDays(this.toDate(date), amount));
444
+ subtractMinutes(
445
+ date: string,
446
+ amount: number,
447
+ inputFormat: string = this.getIsoFormat(),
448
+ ): string {
449
+ return this._calculate(date, sub, "minutes", amount, inputFormat);
355
450
  }
356
451
 
357
452
  /**
358
453
  */
359
- subtractWeeks(date: string, amount: number): string {
360
- return this.toISO(subWeeks(this.toDate(date), amount));
454
+ subtractHours(
455
+ date: string,
456
+ amount: number,
457
+ inputFormat: string = this.getIsoFormat(),
458
+ ): string {
459
+ return this._calculate(date, sub, "hours", amount, inputFormat);
361
460
  }
362
461
 
363
462
  /**
364
463
  */
365
- subtractMonths(date: string, amount: number): string {
366
- return this.toISO(subMonths(this.toDate(date), amount));
464
+ subtractDays(
465
+ date: string,
466
+ amount: number,
467
+ inputFormat: string = this.getIsoFormat(),
468
+ ): string {
469
+ return this._calculate(date, sub, "days", amount, inputFormat);
367
470
  }
368
471
 
369
472
  /**
370
473
  */
371
- subtractYears(date: string, amount: number): string {
372
- return this.toISO(subYears(this.toDate(date), amount));
474
+ subtractWeeks(
475
+ date: string,
476
+ amount: number,
477
+ inputFormat: string = this.getIsoFormat(),
478
+ ): string {
479
+ return this._calculate(date, sub, "weeks", amount, inputFormat);
373
480
  }
374
481
 
375
482
  /**
376
- * GETTERS
377
483
  */
378
- startOfMonth(date: string, inputFormat: string = this._isoFormat): string {
379
- return this.toISO(startOfMonth(this.toDate(date, inputFormat)));
484
+ subtractMonths(
485
+ date: string,
486
+ amount: number,
487
+ inputFormat: string = this.getIsoFormat(),
488
+ ): string {
489
+ return this._calculate(date, sub, "months", amount, inputFormat);
380
490
  }
381
491
 
382
492
  /**
383
493
  */
384
- endOfMonth(date: string, inputFormat: string = this._isoFormat): string {
385
- return this.toISO(endOfMonth(this.toDate(date, inputFormat)));
494
+ subtractYears(
495
+ date: string,
496
+ amount: number,
497
+ inputFormat: string = this.getIsoFormat(),
498
+ ): string {
499
+ return this._calculate(date, sub, "years", amount, inputFormat);
386
500
  }
387
501
 
502
+ //////////// GETTERS
503
+
388
504
  /**
505
+ * @private
389
506
  */
390
- startOfWeek(date: string, inputFormat: string = this._isoFormat): string {
391
- const { weekStartsOn } = this.getOptions();
392
-
507
+ _get(
508
+ date: string | Date,
509
+ method: Function,
510
+ inputFormat: string = this.getIsoFormat(),
511
+ options: Object = {},
512
+ ): string {
513
+ const parsedDate = this.toDate(date, inputFormat);
393
514
  return this.toISO(
394
- startOfWeek(this.toDate(date, inputFormat), {
395
- weekStartsOn,
515
+ method(parsedDate, {
516
+ in: this.getDateFnsContext(),
517
+ ...options,
396
518
  }),
397
519
  );
398
520
  }
399
521
 
400
522
  /**
401
523
  */
402
- getHours(date: string | Date): number {
403
- const parsedDate = typeof date === "string" ? this.toDate(date) : date;
404
- return getHours(parsedDate);
524
+ startOfMonth(
525
+ date: string | Date,
526
+ inputFormat: string = this.getIsoFormat(),
527
+ ): string {
528
+ return this._get(date, startOfMonth, inputFormat);
405
529
  }
406
530
 
407
531
  /**
408
532
  */
409
- getMinutes(date: string | Date): number {
410
- const parsedDate = typeof date === "string" ? this.toDate(date) : date;
411
- return getMinutes(parsedDate);
533
+ endOfMonth(
534
+ date: string | Date,
535
+ inputFormat: string = this.getIsoFormat(),
536
+ ): string {
537
+ return this._get(date, endOfMonth, inputFormat);
412
538
  }
413
539
 
414
540
  /**
415
541
  */
416
- getWeek(date: string): number {
417
- const { weekStartsOn, firstWeekContainsDate } = this.getOptions();
542
+ startOfWeek(
543
+ date: string | Date,
544
+ inputFormat: string = this.getIsoFormat(),
545
+ ): string {
546
+ const { weekStartsOn } = this.getOptions();
547
+ return this._get(date, startOfWeek, inputFormat, {
548
+ weekStartsOn,
549
+ });
550
+ }
551
+
552
+ /**
553
+ */
554
+ getHours(
555
+ date: string | Date,
556
+ inputFormat: string = this.getIsoFormat(),
557
+ ): number {
558
+ const parsedDate = this.toDate(date, inputFormat);
559
+ return getHours(parsedDate, { in: this.getDateFnsContext() });
560
+ }
561
+
562
+ /**
563
+ */
564
+ getMinutes(
565
+ date: string | Date,
566
+ inputFormat: string = this.getIsoFormat(),
567
+ ): number {
568
+ const parsedDate = this.toDate(date, inputFormat);
569
+ return getMinutes(parsedDate, { in: this.getDateFnsContext() });
570
+ }
418
571
 
419
- return getWeek(this.toDate(date), {
572
+ /**
573
+ */
574
+ getWeek(
575
+ date: string | Date,
576
+ inputFormat: string = this.getIsoFormat(),
577
+ ): number {
578
+ const { weekStartsOn, firstWeekContainsDate } = this.getOptions();
579
+ const parsedDate = this.toDate(date, inputFormat);
580
+ return getWeek(parsedDate, {
581
+ in: this.getDateFnsContext(),
420
582
  weekStartsOn,
421
583
  firstWeekContainsDate,
422
584
  });
423
585
  }
424
586
 
587
+ //////////// SETTERS
588
+
425
589
  /**
426
- * SETTERS
590
+ * @private
427
591
  */
428
- setYear(date: string, year: number): string {
429
- return this.toISO(setYear(this.toDate(date), year));
592
+ _set(
593
+ date: string | Date,
594
+ property: string,
595
+ value: number,
596
+ inputFormat: string = this.getIsoFormat(),
597
+ ): string {
598
+ const parsedDate = this.toDate(date, inputFormat);
599
+ return this.toISO(
600
+ set(
601
+ parsedDate,
602
+ { [property]: value },
603
+ // {
604
+ // in: this.getDateFnsContext(),
605
+ // },
606
+ ),
607
+ );
430
608
  }
431
609
 
432
610
  /**
433
611
  */
434
- setMonth(date: string, month: number): string {
435
- return this.toISO(setMonth(this.toDate(date), month));
612
+ setYear(
613
+ date: string | Date,
614
+ year: number,
615
+ inputFormat: string = this.getIsoFormat(),
616
+ ): string {
617
+ return this._set(date, "year", year, inputFormat);
436
618
  }
437
619
 
438
620
  /**
439
621
  */
440
- setHour(date: string, hour: number): string {
441
- return this.toISO(setHours(this.toDate(date), hour));
622
+ setMonth(
623
+ date: string | Date,
624
+ month: number,
625
+ inputFormat: string = this.getIsoFormat(),
626
+ ): string {
627
+ return this._set(date, "month", month, inputFormat);
442
628
  }
443
629
 
444
630
  /**
445
631
  */
446
- setMinute(date: string, minute: number): string {
447
- return this.toISO(setMinutes(this.toDate(date), minute));
632
+ setDay(
633
+ date: string | Date,
634
+ day: number,
635
+ inputFormat: string = this.getIsoFormat(),
636
+ ): string {
637
+ return this._set(date, "date", day, inputFormat);
448
638
  }
449
639
 
450
640
  /**
451
641
  */
452
- setSecond(date: string, second: number): string {
453
- return this.toISO(setSeconds(this.toDate(date), second));
642
+ setHour(
643
+ date: string | Date,
644
+ hour: number,
645
+ inputFormat: string = this.getIsoFormat(),
646
+ ): string {
647
+ return this._set(date, "hours", hour, inputFormat);
454
648
  }
455
649
 
456
650
  /**
457
651
  */
458
- setMilliseconds(date: string, milliseconds: number): string {
459
- return this.toISO(setMilliseconds(this.toDate(date), milliseconds));
652
+ setMinute(
653
+ date: string | Date,
654
+ minute: number,
655
+ inputFormat: string = this.getIsoFormat(),
656
+ ): string {
657
+ return this._set(date, "minutes", minute, inputFormat);
658
+ }
659
+
660
+ /**
661
+ */
662
+ setSecond(
663
+ date: string | Date,
664
+ second: number,
665
+ inputFormat: string = this.getIsoFormat(),
666
+ ): string {
667
+ return this._set(date, "seconds", second, inputFormat);
668
+ }
669
+
670
+ /**
671
+ */
672
+ setMilliseconds(
673
+ date: string | Date,
674
+ millisecond: number,
675
+ inputFormat: string = this.getIsoFormat(),
676
+ ): string {
677
+ return this._set(date, "milliseconds", millisecond, inputFormat);
460
678
  }
461
679
 
462
680
  /**
@@ -467,16 +685,108 @@ class BaseDateTimeUtil {
467
685
  minutes: number,
468
686
  seconds: number,
469
687
  milliseconds: number,
688
+ inputFormat: string = this.getIsoFormat(),
470
689
  ): string {
690
+ const parsedDate = this.toDate(date, inputFormat);
471
691
  return this.toISO(
472
- set(this.toDate(date), {
473
- hours,
474
- minutes,
475
- seconds,
476
- milliseconds,
477
- }),
692
+ set(
693
+ parsedDate,
694
+ { hours, minutes, seconds, milliseconds },
695
+ {
696
+ in: this.getDateFnsContext(),
697
+ },
698
+ ),
478
699
  );
479
700
  }
701
+
702
+ //////////// Timezone related
703
+
704
+ /**
705
+ */
706
+ getTimeZone(): string {
707
+ return this._timeZone;
708
+ }
709
+
710
+ /**
711
+ */
712
+ setTimeZone(timeZone: string) {
713
+ this._timeZone = timeZone;
714
+ }
715
+
716
+ /**
717
+ */
718
+ getDateFnsContext(): null | Function {
719
+ if (IS_GRAALJS) {
720
+ return null; // tz method does not work in graal (yet)
721
+ }
722
+ return tz(this.getTimeZone());
723
+ }
724
+
725
+ //////////// Offsets
726
+
727
+ /**
728
+ */
729
+ getOffset(
730
+ date: string | Date,
731
+ inputFormat: string = this.getIsoFormat(),
732
+ ): OffsetInfo | null {
733
+ const parsedDate = this.toDate(date, inputFormat);
734
+ if (isValid(parsedDate)) {
735
+ const dateOffset = this.toFormat(parsedDate, DATETIME_OFFSET_FORMAT);
736
+ const offsets = this.getTimezoneOffsets();
737
+ if (
738
+ typeof offsets.daylight !== "undefined" &&
739
+ offsets.daylight.value === dateOffset
740
+ ) {
741
+ return offsets.daylight;
742
+ } else if (
743
+ typeof offsets.standard !== "undefined" &&
744
+ offsets.standard.value === dateOffset
745
+ ) {
746
+ return offsets.standard;
747
+ }
748
+ }
749
+ return null;
750
+ }
751
+
752
+ /**
753
+ */
754
+ getTimezoneOffsets(): {
755
+ standard?: OffsetInfo,
756
+ daylight?: OffsetInfo,
757
+ } {
758
+ const createOffsetObject = (info: OffsetInfoInput): OffsetInfo => {
759
+ const sign = info.offset < 0 ? "-" : "+";
760
+ const absOffset = Math.abs(info.offset);
761
+ const hours = Math.trunc(absOffset);
762
+ const minutes = Math.round((absOffset % 1) * 60);
763
+
764
+ const offsetString = `${sign}${String(hours + (minutes === 60 ? 1 : 0)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
765
+
766
+ return {
767
+ label: info.name,
768
+ abbr: info.abbr,
769
+ value: offsetString,
770
+ };
771
+ };
772
+
773
+ const timezone = soft(this.getTimeZone());
774
+
775
+ if (timezone.length === 0) {
776
+ return {};
777
+ }
778
+
779
+ const { daylight, standard } = timezone[0];
780
+
781
+ if (daylight) {
782
+ return {
783
+ daylight: createOffsetObject(daylight),
784
+ standard: createOffsetObject(standard),
785
+ };
786
+ }
787
+
788
+ return { standard: createOffsetObject(standard) };
789
+ }
480
790
  }
481
791
 
482
792
  /**