@timestamp-js/core 0.1.0-alpha.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.
package/dist/index.js ADDED
@@ -0,0 +1,1410 @@
1
+ /**
2
+ * Matches supported date and date-time input.
3
+ *
4
+ * Accepts `YYYY-MM`, `YYYY-MM-DD`, space-separated date/time strings,
5
+ * ISO-style `T` separators, optional seconds, optional milliseconds, and
6
+ * optional timezone suffixes such as `Z`, `+06:00`, or `-0700`.
7
+ */
8
+ export const PARSE_DATETIME = /^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?(?:[Tt\s]+(\d{1,2})(?::(\d{1,2}))?(?::(\d{1,2})(?:\.(\d{1,3}))?)?)?(?:\s*(Z|[+-]\d{2}:?\d{2}))?$/;
9
+ /**
10
+ * Matches the date portion of a timestamp string.
11
+ */
12
+ export const PARSE_DATE = /^(\d{4})-(\d{1,2})(-(\d{1,2}))?/;
13
+ /**
14
+ * Matches `HH`, `HH:mm`, `HH:mm:ss`, or `HH:mm:ss.SSS` time strings.
15
+ */
16
+ export const PARSE_TIME = /^(\d\d?)(?::(\d\d?))?(?::(\d\d?))?(?:\.(\d{1,3}))?$/;
17
+ /**
18
+ * Month lengths for a non-leap Gregorian year.
19
+ *
20
+ * Index `0` is intentionally unused so month numbers can be used directly.
21
+ */
22
+ export const DAYS_IN_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
23
+ /**
24
+ * Month lengths for a leap Gregorian year.
25
+ *
26
+ * Index `0` is intentionally unused so month numbers can be used directly.
27
+ */
28
+ export const DAYS_IN_MONTH_LEAP = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
29
+ /**
30
+ * Shared conversion constants for milliseconds, seconds, minutes, hours, and days.
31
+ */
32
+ export const TIME_CONSTANTS = {
33
+ MILLISECONDS_IN: {
34
+ SECOND: 1000,
35
+ MINUTE: 60000,
36
+ HOUR: 3600000,
37
+ DAY: 86400000,
38
+ WEEK: 604800000,
39
+ },
40
+ SECONDS_IN: {
41
+ MINUTE: 60,
42
+ HOUR: 3600,
43
+ DAY: 86400,
44
+ WEEK: 604800,
45
+ },
46
+ MINUTES_IN: {
47
+ MINUTE: 1,
48
+ HOUR: 60,
49
+ DAY: 1440,
50
+ WEEK: 10080,
51
+ },
52
+ HOURS_IN: {
53
+ DAY: 24,
54
+ WEEK: 168,
55
+ },
56
+ DAYS_IN: {
57
+ WEEK: 7,
58
+ },
59
+ };
60
+ /**
61
+ * Minimum number of days found in any Gregorian month.
62
+ */
63
+ export const DAYS_IN_MONTH_MIN = 28;
64
+ /**
65
+ * Maximum number of days found in any Gregorian month.
66
+ */
67
+ export const DAYS_IN_MONTH_MAX = 31;
68
+ /**
69
+ * Maximum Gregorian month number.
70
+ */
71
+ export const MONTH_MAX = 12;
72
+ /**
73
+ * Minimum Gregorian month number.
74
+ */
75
+ export const MONTH_MIN = 1;
76
+ /**
77
+ * Minimum day-of-month number.
78
+ */
79
+ export const DAY_MIN = 1;
80
+ /**
81
+ * First hour in a 24-hour day.
82
+ */
83
+ export const FIRST_HOUR = 0;
84
+ /**
85
+ * Number of days in a week.
86
+ */
87
+ export const DAYS_IN_WEEK = TIME_CONSTANTS.DAYS_IN.WEEK;
88
+ /**
89
+ * Number of minutes in an hour.
90
+ */
91
+ export const MINUTES_IN_HOUR = TIME_CONSTANTS.MINUTES_IN.HOUR;
92
+ /**
93
+ * Number of hours in a day.
94
+ */
95
+ export const HOURS_IN_DAY = TIME_CONSTANTS.HOURS_IN.DAY;
96
+ /**
97
+ * Number of milliseconds in one minute.
98
+ */
99
+ export const MILLISECONDS_IN_MINUTE = TIME_CONSTANTS.MILLISECONDS_IN.MINUTE;
100
+ /**
101
+ * Number of milliseconds in one hour.
102
+ */
103
+ export const MILLISECONDS_IN_HOUR = TIME_CONSTANTS.MILLISECONDS_IN.HOUR;
104
+ /**
105
+ * Number of milliseconds in one day.
106
+ */
107
+ export const MILLISECONDS_IN_DAY = TIME_CONSTANTS.MILLISECONDS_IN.DAY;
108
+ /**
109
+ * Number of milliseconds in one week.
110
+ */
111
+ export const MILLISECONDS_IN_WEEK = TIME_CONSTANTS.MILLISECONDS_IN.WEEK;
112
+ /**
113
+ * Frozen empty timestamp template.
114
+ *
115
+ * Use {@link copyTimestamp} or parser helpers to create new timestamp objects
116
+ * instead of mutating this shared default.
117
+ */
118
+ export const Timestamp = freezeTimestamp({
119
+ date: "",
120
+ hasDay: false,
121
+ year: 0,
122
+ month: 0,
123
+ day: 0,
124
+ hasTime: false,
125
+ hour: 0,
126
+ minute: 0,
127
+ weekday: 0,
128
+ doy: 0,
129
+ workweek: 0,
130
+ past: false,
131
+ current: false,
132
+ future: false,
133
+ disabled: false,
134
+ });
135
+ /**
136
+ * Frozen empty time-object template.
137
+ */
138
+ export const TimeObject = Object.freeze({
139
+ hour: 0,
140
+ minute: 0,
141
+ });
142
+ function freezeTimestamp(timestamp) {
143
+ return Object.freeze({ ...timestamp });
144
+ }
145
+ function cloneTimestamp(timestamp) {
146
+ return { ...timestamp };
147
+ }
148
+ function parseMillisecond(value) {
149
+ return value === undefined ? undefined : parseInt(value.padEnd(3, "0"), 10);
150
+ }
151
+ /**
152
+ * Validates whether an input string matches the supported timestamp grammar.
153
+ *
154
+ * @param {string} input A string in the form `YYYY-MM-DD`, `YYYY-MM-DD HH:mm`, or a full ISO-like date time.
155
+ * @returns {boolean} True if parseable
156
+ */
157
+ export function validateTimestamp(input) {
158
+ if (typeof input !== "string")
159
+ return false;
160
+ return PARSE_DATETIME.test(input);
161
+ }
162
+ /**
163
+ * Fast low-level parser for date and date-time strings.
164
+ *
165
+ * This parser fills numeric fields, but does not update formatted date,
166
+ * weekday, day-of-year, workweek, or relative flags. Use
167
+ * {@link parseTimestamp} when those derived fields are needed.
168
+ *
169
+ * @param {string} input In the form `YYYY-MM-DD`, `YYYY-MM-DD HH:mm:ss`, or an ISO-like date time with optional milliseconds and timezone suffix.
170
+ * @returns {Timestamp} This {@link Timestamp} is minimally filled in. The {@link Timestamp.date} and {@link Timestamp.time} as well as relative data will not be filled in.
171
+ */
172
+ export function parsed(input) {
173
+ if (typeof input !== "string")
174
+ return null;
175
+ const parts = PARSE_DATETIME.exec(input);
176
+ if (!parts || !parts[1] || !parts[2])
177
+ return null;
178
+ const year = parseInt(parts[1], 10);
179
+ const month = parseInt(parts[2], 10);
180
+ const day = parseInt(parts[3] || "1", 10);
181
+ const hour = parseInt(parts[4] || "0", 10);
182
+ const minute = parseInt(parts[5] || "0", 10);
183
+ const second = parts[6] === undefined ? undefined : parseInt(parts[6], 10);
184
+ const millisecond = parseMillisecond(parts[7]);
185
+ const timestamp = {
186
+ date: input,
187
+ year,
188
+ month,
189
+ day,
190
+ hour,
191
+ minute,
192
+ hasDay: !!parts[3],
193
+ hasTime: true, // time is always present, even if '00:00'
194
+ past: false,
195
+ current: false,
196
+ future: false,
197
+ disabled: false,
198
+ weekday: 0,
199
+ doy: 0,
200
+ workweek: 0,
201
+ };
202
+ if (second !== undefined) {
203
+ timestamp.second = second;
204
+ }
205
+ if (millisecond !== undefined) {
206
+ timestamp.millisecond = millisecond;
207
+ }
208
+ if (parts[8] !== undefined) {
209
+ timestamp.timezone = parts[8];
210
+ }
211
+ timestamp.time = getTime(timestamp);
212
+ return freezeTimestamp(timestamp);
213
+ }
214
+ /**
215
+ * Takes a JavaScript Date and returns a {@link Timestamp}. The {@link Timestamp} is not updated with relative information.
216
+ * @param {Date} date JavaScript Date
217
+ * @param {boolean} utc If set the {@link Timestamp} will parse the Date as UTC
218
+ * @returns {Timestamp} A minimal {@link Timestamp} without updated or relative updates.
219
+ */
220
+ export function parseDate(date, utc = false) {
221
+ if (!(date instanceof Date))
222
+ return null;
223
+ const UTC = utc ? "UTC" : "";
224
+ const second = date[`get${UTC}Seconds`]();
225
+ const millisecond = date[`get${UTC}Milliseconds`]();
226
+ const timestamp = {
227
+ date: padNumber(date[`get${UTC}FullYear`](), 4) +
228
+ "-" +
229
+ padNumber(date[`get${UTC}Month`]() + 1, 2) +
230
+ "-" +
231
+ padNumber(date[`get${UTC}Date`](), 2),
232
+ time: padNumber(date[`get${UTC}Hours`]() || 0, 2) +
233
+ ":" +
234
+ padNumber(date[`get${UTC}Minutes`]() || 0, 2),
235
+ year: date[`get${UTC}FullYear`](),
236
+ month: date[`get${UTC}Month`]() + 1,
237
+ day: date[`get${UTC}Date`](),
238
+ hour: date[`get${UTC}Hours`](),
239
+ minute: date[`get${UTC}Minutes`](),
240
+ weekday: 0,
241
+ doy: 0,
242
+ workweek: 0,
243
+ hasDay: true,
244
+ hasTime: true, // Date always has time, even if it is '00:00'
245
+ past: false,
246
+ current: false,
247
+ future: false,
248
+ disabled: false,
249
+ };
250
+ if (second !== 0) {
251
+ timestamp.second = second;
252
+ }
253
+ if (millisecond !== 0) {
254
+ timestamp.millisecond = millisecond;
255
+ }
256
+ return updateFormatted(timestamp);
257
+ }
258
+ /**
259
+ * Pads a number to a requested string length.
260
+ *
261
+ * Useful for formatting values such as `5` as `05`.
262
+ * @param {number} x The number to pad
263
+ * @param {number} length The length of the required number as a string
264
+ * @returns {string} The padded number (as a string). (ie: 5 = '05')
265
+ */
266
+ export function padNumber(x, length) {
267
+ let padded = String(x);
268
+ while (padded.length < length) {
269
+ padded = "0" + padded;
270
+ }
271
+ return padded;
272
+ }
273
+ /**
274
+ * Returns if the passed year is a leap year
275
+ * @param {number} year The year to check (ie: 1999, 2020)
276
+ * @returns {boolean} True if the year is a leap year
277
+ */
278
+ export function isLeapYear(year) {
279
+ // A year is a Gregorian leap year if it is divisible by 4,
280
+ // but not by 100, unless it is also divisible by 400.
281
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
282
+ }
283
+ /**
284
+ * Returns the days of the specified month in a year
285
+ * @param {number} year The year (ie: 1999, 2020)
286
+ * @param {number} month The Gregorian month number, where January is `1`
287
+ * @returns {number} The number of days in the month (corrected for leap years)
288
+ */
289
+ export function daysInMonth(year, month) {
290
+ return (isLeapYear(year) ? DAYS_IN_MONTH_LEAP[month] : DAYS_IN_MONTH[month]);
291
+ }
292
+ /**
293
+ * Returns a {@link Timestamp} of next day from passed in {@link Timestamp}
294
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
295
+ * @returns {Timestamp} A new {@link Timestamp} representing the next day
296
+ */
297
+ export function nextDay(timestamp) {
298
+ const date = new Date(timestamp.year, timestamp.month - 1, timestamp.day + 1);
299
+ return updateFormatted(normalizeTimestamp({
300
+ ...timestamp,
301
+ year: date.getFullYear(),
302
+ month: date.getMonth() + 1,
303
+ day: date.getDate(),
304
+ }));
305
+ }
306
+ /**
307
+ * Returns a {@link Timestamp} of previous day from passed in {@link Timestamp}
308
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
309
+ * @returns {Timestamp} A new {@link Timestamp} representing the previous day
310
+ */
311
+ export function prevDay(timestamp) {
312
+ const date = new Date(timestamp.year, timestamp.month - 1, timestamp.day - 1);
313
+ return updateFormatted(normalizeTimestamp({
314
+ ...timestamp,
315
+ year: date.getFullYear(),
316
+ month: date.getMonth() + 1,
317
+ day: date.getDate(),
318
+ }));
319
+ }
320
+ /**
321
+ * Returns today's date
322
+ * @returns {string} Date string in the form `YYYY-MM-DD`
323
+ */
324
+ export function today() {
325
+ const d = new Date(), month = d.getMonth() + 1, day = d.getDate(), year = d.getFullYear();
326
+ return [year, padNumber(month, 2), padNumber(day, 2)].join("-");
327
+ }
328
+ /**
329
+ * Takes a date string ('YYYY-MM-DD') and validates if it is today's date
330
+ * @param {string} date Date string in the form 'YYYY-MM-DD'
331
+ * @returns {boolean} True if the date is today's date
332
+ */
333
+ export function isToday(date) {
334
+ return date === today();
335
+ }
336
+ /**
337
+ * Returns the start of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the start of the week).
338
+ * If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}.
339
+ * @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the week
340
+ * @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday
341
+ * @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information
342
+ * @returns {Timestamp} A new {@link Timestamp} representing the start of the week
343
+ */
344
+ export function getStartOfWeek(timestamp, weekdays, today) {
345
+ let start = cloneTimestamp(timestamp);
346
+ if (!weekdays) {
347
+ return freezeTimestamp(start);
348
+ }
349
+ if (start.day === 1 || start.weekday === 0) {
350
+ while (!weekdays.includes(Number(start.weekday))) {
351
+ start = nextDay(start);
352
+ }
353
+ }
354
+ start = findWeekday(start, weekdays[0], prevDay);
355
+ start = updateFormatted(start);
356
+ if (today) {
357
+ start = updateRelative(start, today, start.hasTime);
358
+ }
359
+ return start;
360
+ }
361
+ /**
362
+ * Returns the end of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the last of the week).
363
+ * If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}.
364
+ * @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the week
365
+ * @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday
366
+ * @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information
367
+ * @returns {Timestamp} A new {@link Timestamp} representing the end of the week
368
+ */
369
+ export function getEndOfWeek(timestamp, weekdays, today) {
370
+ let end = cloneTimestamp(timestamp);
371
+ if (!weekdays || !Array.isArray(weekdays)) {
372
+ return freezeTimestamp(end);
373
+ }
374
+ // is last day of month?
375
+ const lastDay = daysInMonth(end.year, end.month);
376
+ if (lastDay === end.day || end.weekday === weekdays[weekdays.length - 1]) {
377
+ while (!weekdays.includes(Number(end.weekday))) {
378
+ end = prevDay(end);
379
+ }
380
+ }
381
+ end = findWeekday(end, weekdays[weekdays.length - 1], nextDay);
382
+ end = updateFormatted(end);
383
+ if (today) {
384
+ end = updateRelative(end, today, end.hasTime);
385
+ }
386
+ return end;
387
+ }
388
+ /**
389
+ * Finds the start of the month based on the passed in {@link Timestamp}
390
+ * @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the month
391
+ * @returns {Timestamp} A {@link Timestamp} of the start of the month
392
+ */
393
+ export function getStartOfMonth(timestamp) {
394
+ let start = cloneTimestamp(timestamp);
395
+ start.day = DAY_MIN;
396
+ start = updateFormatted(start);
397
+ return start;
398
+ }
399
+ /**
400
+ * Finds the end of the month based on the passed in {@link Timestamp}
401
+ * @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the month
402
+ * @returns {Timestamp} A {@link Timestamp} of the end of the month
403
+ */
404
+ export function getEndOfMonth(timestamp) {
405
+ let end = cloneTimestamp(timestamp);
406
+ end.day = daysInMonth(end.year, end.month);
407
+ end = updateFormatted(end);
408
+ return end;
409
+ }
410
+ /**
411
+ * Converts time input into minutes since midnight.
412
+ *
413
+ * Strings may include seconds or milliseconds, but sub-minute precision is
414
+ * ignored for this minute-oriented helper.
415
+ *
416
+ * @param input - Minutes since midnight, a time string, or an object with hour and minute fields.
417
+ * @returns Minutes since midnight, or `false` when the input cannot be parsed.
418
+ */
419
+ export function parseTime(input) {
420
+ const type = Object.prototype.toString.call(input);
421
+ switch (type) {
422
+ case "[object Number]":
423
+ // when a number is given, it's minutes since 12:00am
424
+ return input;
425
+ case "[object String]": {
426
+ // when a string is given, it's a hh:mm:ss format where seconds are optional, but not used for minute math
427
+ const parts = PARSE_TIME.exec(input);
428
+ if (!parts) {
429
+ return false;
430
+ }
431
+ return parseInt(parts[1], 10) * 60 + parseInt(parts[2] || "0", 10);
432
+ }
433
+ case "[object Object]":
434
+ // when an object is given, it must have hour and minute
435
+ if (typeof input !== "object" ||
436
+ typeof input.hour !== "number" ||
437
+ typeof input.minute !== "number") {
438
+ return false;
439
+ }
440
+ if (typeof input === "object" && "hour" in input && "minute" in input) {
441
+ return input.hour * 60 + input.minute;
442
+ }
443
+ return false;
444
+ }
445
+ return false;
446
+ }
447
+ /**
448
+ * Compares two {@link Timestamp}s for exactness
449
+ * @param {Timestamp} ts1 The first {@link Timestamp}
450
+ * @param {Timestamp} ts2 The second {@link Timestamp}
451
+ * @returns {boolean} True if the two {@link Timestamp}s are an exact match
452
+ */
453
+ export function compareTimestamps(ts1, ts2) {
454
+ if (!ts1 || !ts2)
455
+ return false;
456
+ return (ts1.year === ts2.year &&
457
+ ts1.month === ts2.month &&
458
+ ts1.day === ts2.day &&
459
+ ts1.hour === ts2.hour &&
460
+ ts1.minute === ts2.minute &&
461
+ ts1.second === ts2.second &&
462
+ ts1.millisecond === ts2.millisecond &&
463
+ ts1.timezone === ts2.timezone);
464
+ }
465
+ /**
466
+ * Compares the date of two {@link Timestamp}s that have been updated with relative data
467
+ * @param {Timestamp} ts1 The first {@link Timestamp}
468
+ * @param {Timestamp} ts2 The second {@link Timestamp}
469
+ * @returns {boolean} True if the two dates are the same
470
+ */
471
+ export function compareDate(ts1, ts2) {
472
+ return getDate(ts1) === getDate(ts2);
473
+ }
474
+ /**
475
+ * Compares the time of two {@link Timestamp}s that have been updated with relative data
476
+ * @param {Timestamp} ts1 The first {@link Timestamp}
477
+ * @param {Timestamp} ts2 The second {@link Timestamp}
478
+ * @returns {boolean} True if the two times are an exact match
479
+ */
480
+ export function compareTime(ts1, ts2) {
481
+ return getTime(ts1) === getTime(ts2);
482
+ }
483
+ /**
484
+ * Compares the date and time of two {@link Timestamp}s that have been updated with relative data
485
+ * @param {Timestamp} ts1 The first {@link Timestamp}
486
+ * @param {Timestamp} ts2 The second {@link Timestamp}
487
+ * @returns {boolean} True if the date and time are an exact match
488
+ */
489
+ export function compareDateTime(ts1, ts2) {
490
+ return getDateTime(ts1) === getDateTime(ts2);
491
+ }
492
+ /**
493
+ * High-level parser that converts a string to a fully formatted {@link Timestamp}.
494
+ *
495
+ * If `now` is supplied, the returned timestamp also includes relative flags
496
+ * such as `past`, `current`, `future`, and `currentWeekday`.
497
+ *
498
+ * @param {string} input In the form `YYYY-MM-DD`, `YYYY-MM-DD HH:mm:ss`, or an ISO-like date time with optional milliseconds and timezone suffix.
499
+ * @param {Timestamp} now A {@link Timestamp} to use for relative data updates
500
+ * @returns {Timestamp} The {@link Timestamp.date} will be filled in as well as the {@link Timestamp.time} if a time is supplied and formatted fields (doy, weekday, workweek, etc). If 'now' is supplied, then relative data will also be updated.
501
+ */
502
+ export function parseTimestamp(input, now = null) {
503
+ let timestamp = parsed(input);
504
+ if (!timestamp)
505
+ return null;
506
+ timestamp = updateFormatted(timestamp);
507
+ if (now) {
508
+ timestamp = updateRelative(timestamp, now, timestamp.hasTime);
509
+ }
510
+ return timestamp;
511
+ }
512
+ /**
513
+ * Converts a {@link Timestamp} into a numeric date identifier based on the passed {@link Timestamp}'s date
514
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
515
+ * @returns {number} The numeric date identifier
516
+ */
517
+ export function getDayIdentifier(timestamp) {
518
+ return ((timestamp.year ?? 0) * 100000000 +
519
+ (timestamp.month ?? 0) * 1000000 +
520
+ (timestamp.day ?? 0) * 10000);
521
+ }
522
+ /**
523
+ * Converts a {@link Timestamp} into a numeric time identifier based on the passed {@link Timestamp}'s time
524
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
525
+ * @returns {number} The numeric time identifier
526
+ */
527
+ export function getTimeIdentifier(timestamp) {
528
+ return (timestamp.hour ?? 0) * 100 + (timestamp.minute ?? 0);
529
+ }
530
+ function getTimeComparisonValue(timestamp) {
531
+ return ((((timestamp.hour ?? 0) * TIME_CONSTANTS.MINUTES_IN.HOUR + (timestamp.minute ?? 0)) *
532
+ TIME_CONSTANTS.SECONDS_IN.MINUTE +
533
+ (timestamp.second ?? 0)) *
534
+ TIME_CONSTANTS.MILLISECONDS_IN.SECOND +
535
+ (timestamp.millisecond ?? 0));
536
+ }
537
+ /**
538
+ * Converts a {@link Timestamp} into a numeric date and time identifier based on the passed {@link Timestamp}'s date and time
539
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
540
+ * @returns {number} The numeric date+time identifier
541
+ */
542
+ export function getDayTimeIdentifier(timestamp) {
543
+ return getDayIdentifier(timestamp) + getTimeIdentifier(timestamp);
544
+ }
545
+ /**
546
+ * Returns the difference between two {@link Timestamp}s
547
+ * @param {Timestamp} ts1 The first {@link Timestamp}
548
+ * @param {Timestamp} ts2 The second {@link Timestamp}
549
+ * @param {boolean=} strict Optional flag to not to return negative numbers
550
+ * @returns {number} The difference
551
+ */
552
+ export function diffTimestamp(ts1, ts2, strict = false) {
553
+ const utc1 = Date.UTC(ts1.year ?? 0, (ts1.month ?? 1) - 1, ts1.day ?? 1, ts1.hour ?? 0, ts1.minute ?? 0, ts1.second ?? 0, ts1.millisecond ?? 0);
554
+ const utc2 = Date.UTC(ts2.year ?? 0, (ts2.month ?? 1) - 1, ts2.day ?? 1, ts2.hour ?? 0, ts2.minute ?? 0, ts2.second ?? 0, ts2.millisecond ?? 0);
555
+ if (strict === true && utc2 < utc1) {
556
+ // Not negative number
557
+ // utc2 - utc1 < 0 -> utc2 < utc1 -> NO: utc1 >= utc2
558
+ return 0;
559
+ }
560
+ return utc2 - utc1;
561
+ }
562
+ /**
563
+ * Updates a {@link Timestamp} with relative data (past, current and future)
564
+ * @param {Timestamp} timestamp The {@link Timestamp} that needs relative data updated
565
+ * @param {Timestamp} now {@link Timestamp} that represents the current date (optional time)
566
+ * @param {boolean=} time Optional flag to include time ('timestamp' and 'now' params should have time values)
567
+ * @returns {Timestamp} A new {@link Timestamp}
568
+ */
569
+ export function updateRelative(timestamp, now, time = false) {
570
+ const ts = cloneTimestamp(timestamp);
571
+ let a = getDayIdentifier(now);
572
+ let b = getDayIdentifier(ts);
573
+ let current = a === b;
574
+ if (ts.hasTime && time && current) {
575
+ a = getTimeComparisonValue(now);
576
+ b = getTimeComparisonValue(ts);
577
+ current = a === b;
578
+ }
579
+ ts.past = b < a;
580
+ ts.current = current;
581
+ ts.future = b > a;
582
+ ts.currentWeekday = ts.weekday === now.weekday;
583
+ return freezeTimestamp(ts);
584
+ }
585
+ /**
586
+ * Returns a timestamp set to a number of minutes past midnight.
587
+ *
588
+ * The returned timestamp has updated hour/minute fields and clears second and
589
+ * millisecond precision because this helper is minute-oriented.
590
+ *
591
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
592
+ * @param {number} minutes The number of minutes to set from midnight
593
+ * @param {Timestamp=} now Optional {@link Timestamp} representing current date and time
594
+ * @returns {Timestamp} A new {@link Timestamp}
595
+ */
596
+ export function updateMinutes(timestamp, minutes, now = null) {
597
+ let ts = cloneTimestamp(timestamp);
598
+ ts.hasTime = true;
599
+ ts.hour = Math.floor(minutes / TIME_CONSTANTS.MINUTES_IN.HOUR);
600
+ ts.minute = minutes % TIME_CONSTANTS.MINUTES_IN.HOUR;
601
+ delete ts.second;
602
+ delete ts.millisecond;
603
+ ts.time = getTime(ts);
604
+ if (now) {
605
+ return updateRelative(ts, now, true);
606
+ }
607
+ return freezeTimestamp(ts);
608
+ }
609
+ /**
610
+ * Updates the {@link Timestamp} with the weekday
611
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
612
+ * @returns A new Timestamp
613
+ */
614
+ export function updateWeekday(timestamp) {
615
+ const ts = cloneTimestamp(timestamp);
616
+ ts.weekday = getWeekday(ts);
617
+ return freezeTimestamp(ts);
618
+ }
619
+ /**
620
+ * Updates the {@link Timestamp} with the day of the year (doy)
621
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
622
+ * @returns A new Timestamp
623
+ */
624
+ export function updateDayOfYear(timestamp) {
625
+ const ts = cloneTimestamp(timestamp);
626
+ ts.doy = getDayOfYear(ts) || 0;
627
+ return freezeTimestamp(ts);
628
+ }
629
+ /**
630
+ * Updates the {@link Timestamp} with the workweek
631
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
632
+ * @returns A new {@link Timestamp}
633
+ */
634
+ export function updateWorkWeek(timestamp) {
635
+ const ts = cloneTimestamp(timestamp);
636
+ ts.workweek = getWorkWeek(ts);
637
+ return freezeTimestamp(ts);
638
+ }
639
+ function isDisabledDayConfig(value) {
640
+ return typeof value === "object" && value !== null && Array.isArray(value) === false;
641
+ }
642
+ function applyDisabledDayConfig(timestamp, config) {
643
+ timestamp.disabled = true;
644
+ if (config !== undefined) {
645
+ timestamp.disabledColor = config.color;
646
+ timestamp.disabledTextColor = config.textColor;
647
+ timestamp.disabledClass = config.class;
648
+ timestamp.disabledStyle = config.style;
649
+ timestamp.disabledLabel = config.label;
650
+ }
651
+ return timestamp;
652
+ }
653
+ function isTimestampInDisabledDay(timestamp, day) {
654
+ const target = getDayIdentifier(timestamp);
655
+ if (Array.isArray(day) === true) {
656
+ if (day.length === 2 && day[0] && day[1]) {
657
+ const start = parsed(day[0]);
658
+ const end = parsed(day[1]);
659
+ return start !== null && end !== null && isBetweenDates(timestamp, start, end);
660
+ }
661
+ return day.some((date) => {
662
+ const disabledDay = parseTimestamp(date);
663
+ return disabledDay !== null && getDayIdentifier(disabledDay) === target;
664
+ });
665
+ }
666
+ if (isDisabledDayConfig(day) === true) {
667
+ const date = day.date;
668
+ const startDate = day.from ?? day.start;
669
+ const endDate = day.to ?? day.end;
670
+ if (date !== undefined) {
671
+ const disabledDay = parseTimestamp(date);
672
+ return disabledDay !== null && getDayIdentifier(disabledDay) === target;
673
+ }
674
+ if (startDate !== undefined && endDate !== undefined) {
675
+ const start = parsed(startDate);
676
+ const end = parsed(endDate);
677
+ return start !== null && end !== null && isBetweenDates(timestamp, start, end);
678
+ }
679
+ return false;
680
+ }
681
+ const disabledDay = parseTimestamp(day);
682
+ return disabledDay !== null && getDayIdentifier(disabledDay) === target;
683
+ }
684
+ /**
685
+ * Updates the passed {@link Timestamp} with disabled, if needed
686
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
687
+ * @param {string} [disabledBefore] In `YYYY-MM-DD` format
688
+ * @param {string} [disabledAfter] In `YYYY-MM-DD` format
689
+ * @param {number[]} [disabledWeekdays] An array of numbers representing weekdays [0 = Sun, ..., 6 = Sat]
690
+ * @param {DisabledDays} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range. Object entries can include date/from/to plus color metadata.
691
+ * @returns A new {@link Timestamp}
692
+ */
693
+ export function updateDisabled(timestamp, disabledBefore, disabledAfter, disabledWeekdays, disabledDays) {
694
+ let ts = cloneTimestamp(timestamp);
695
+ const t = getDayIdentifier(ts);
696
+ if (disabledBefore !== undefined) {
697
+ const disabledDay = parsed(disabledBefore);
698
+ if (disabledDay) {
699
+ const before = getDayIdentifier(disabledDay);
700
+ if (t <= before) {
701
+ ts.disabled = true;
702
+ }
703
+ }
704
+ }
705
+ if (ts.disabled !== true && disabledAfter !== undefined) {
706
+ const disabledDay = parsed(disabledAfter);
707
+ if (disabledDay) {
708
+ const after = getDayIdentifier(disabledDay);
709
+ if (t >= after) {
710
+ ts.disabled = true;
711
+ }
712
+ }
713
+ }
714
+ if (ts.disabled !== true && Array.isArray(disabledWeekdays) && disabledWeekdays.length > 0) {
715
+ for (const weekday in disabledWeekdays) {
716
+ if (disabledWeekdays[weekday] === ts.weekday) {
717
+ ts.disabled = true;
718
+ break;
719
+ }
720
+ }
721
+ }
722
+ if (ts.disabled !== true && Array.isArray(disabledDays) && disabledDays.length > 0) {
723
+ for (const day of disabledDays) {
724
+ if (isTimestampInDisabledDay(ts, day) === true) {
725
+ ts = applyDisabledDayConfig(ts, isDisabledDayConfig(day) === true ? day : undefined);
726
+ break;
727
+ }
728
+ }
729
+ }
730
+ return freezeTimestamp(ts);
731
+ }
732
+ /**
733
+ * Updates the passed {@link Timestamp} with formatted data (time string, date string, weekday, day of year and workweek)
734
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
735
+ * @returns A new {@link Timestamp}
736
+ */
737
+ export function updateFormatted(timestamp) {
738
+ const ts = cloneTimestamp(timestamp);
739
+ ts.hasTime = true;
740
+ ts.time = getTime(ts);
741
+ ts.date = getDate(ts);
742
+ ts.weekday = getWeekday(ts);
743
+ ts.doy = getDayOfYear(ts) || 0;
744
+ ts.workweek = getWorkWeek(ts);
745
+ return freezeTimestamp(ts);
746
+ }
747
+ /**
748
+ * Returns day of the year (doy) for the passed in {@link Timestamp}
749
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
750
+ * @returns {number} The day of the year
751
+ */
752
+ export function getDayOfYear(timestamp) {
753
+ if (timestamp.year === 0)
754
+ return;
755
+ return ((Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day) -
756
+ Date.UTC(timestamp.year, 0, 0)) /
757
+ 24 /
758
+ 60 /
759
+ 60 /
760
+ 1000);
761
+ }
762
+ /**
763
+ * Returns workweek for the passed in {@link Timestamp}
764
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
765
+ * @returns {number} The work week
766
+ */
767
+ export function getWorkWeek(timestamp) {
768
+ let ts = timestamp;
769
+ if (ts.year === 0) {
770
+ const parsedToday = parseTimestamp(today());
771
+ if (parsedToday) {
772
+ ts = parsedToday;
773
+ }
774
+ }
775
+ // Remove time components of date
776
+ const weekday = new Date(Date.UTC(ts.year, ts.month - 1, ts.day));
777
+ // Adjust the date to the correct day of the week
778
+ const dayAdjustment = 4; // thursday is 4
779
+ weekday.setUTCDate(weekday.getUTCDate() - ((weekday.getUTCDay() + 6) % 7) + dayAdjustment);
780
+ // Set to nearest Thursday: current date + 4 - current day number
781
+ // Make Sunday's day number 7
782
+ weekday.setUTCDate(weekday.getUTCDate() + dayAdjustment - (weekday.getUTCDay() || 7));
783
+ // Get first day of year
784
+ var yearStart = new Date(Date.UTC(weekday.getUTCFullYear(), 0, 1));
785
+ // Calculate full weeks to nearest Thursday
786
+ var weekNumber = Math.ceil(((weekday.valueOf() - yearStart.valueOf()) / 86400000 + 1) / 7);
787
+ return weekNumber;
788
+ }
789
+ /**
790
+ * Returns weekday for the passed in {@link Timestamp}
791
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
792
+ * @returns {number} The weekday
793
+ */
794
+ export function getWeekday(timestamp) {
795
+ let weekday = timestamp.weekday;
796
+ if (timestamp.hasDay) {
797
+ const floor = Math.floor;
798
+ const day = timestamp.day;
799
+ const month = ((timestamp.month + 9) % MONTH_MAX) + 1;
800
+ const century = floor(timestamp.year / 100);
801
+ const year = (timestamp.year % 100) - (timestamp.month <= 2 ? 1 : 0);
802
+ weekday =
803
+ (((day +
804
+ floor(2.6 * month - 0.2) -
805
+ 2 * century +
806
+ year +
807
+ floor(year / 4) +
808
+ floor(century / 4)) %
809
+ 7) +
810
+ 7) %
811
+ 7;
812
+ }
813
+ return weekday ?? 0;
814
+ }
815
+ /**
816
+ * Makes a copy of the passed in {@link Timestamp}
817
+ * @param {Timestamp} timestamp The original {@link Timestamp}
818
+ * @returns {Timestamp} A copy of the original {@link Timestamp}
819
+ */
820
+ export function copyTimestamp(timestamp) {
821
+ return freezeTimestamp(timestamp);
822
+ }
823
+ /**
824
+ * Used internally to convert {@link Timestamp} used with 'parsed' or 'parseDate' so the 'date' portion of the {@link Timestamp} is correct.
825
+ * @param {Timestamp} timestamp The (raw) {@link Timestamp}
826
+ * @returns {string} A formatted date ('YYYY-MM-DD')
827
+ */
828
+ export function getDate(timestamp) {
829
+ let str = `${padNumber(timestamp.year, 4)}-${padNumber(timestamp.month, 2)}`;
830
+ if (timestamp.hasDay)
831
+ str += `-${padNumber(timestamp.day, 2)}`;
832
+ return str;
833
+ }
834
+ /**
835
+ * Used internally to convert {@link Timestamp} with 'parsed' or 'parseDate' so the 'time' portion of the {@link Timestamp} is correct.
836
+ * @param {Timestamp} timestamp The (raw) {@link Timestamp}
837
+ * @returns {string} A formatted time ('hh:mm')
838
+ */
839
+ export function getTime(timestamp) {
840
+ if (!timestamp.hasTime) {
841
+ return "";
842
+ }
843
+ let time = `${padNumber(timestamp.hour, 2)}:${padNumber(timestamp.minute, 2)}`;
844
+ if (timestamp.second !== undefined || timestamp.millisecond !== undefined) {
845
+ time += `:${padNumber(timestamp.second ?? 0, 2)}`;
846
+ }
847
+ if (timestamp.millisecond !== undefined) {
848
+ time += `.${padNumber(timestamp.millisecond, 3)}`;
849
+ }
850
+ return time;
851
+ }
852
+ /**
853
+ * Returns a formatted string date and time ('YYYY-YY-MM hh:mm')
854
+ * @param {Timestamp} timestamp The {@link Timestamp}
855
+ * @returns {string} A formatted date time ('YYYY-MM-DD HH:mm')
856
+ */
857
+ export function getDateTime(timestamp) {
858
+ return getDate(timestamp) + " " + (timestamp.hasTime ? getTime(timestamp) : "00:00");
859
+ }
860
+ /**
861
+ * An alias for {relativeDays}
862
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
863
+ * @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}).
864
+ * @param {number} [days=1] The number of days to move.
865
+ * @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat].
866
+ * @returns A new {@link Timestamp}
867
+ */
868
+ export function moveRelativeDays(timestamp, mover = nextDay, days = 1, allowedWeekdays = [0, 1, 2, 3, 4, 5, 6]) {
869
+ return relativeDays(timestamp, mover, days, allowedWeekdays);
870
+ }
871
+ /**
872
+ * Moves the {@link Timestamp} the number of relative days
873
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
874
+ * @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}).
875
+ * @param {number} [days=1] The number of days to move.
876
+ * @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat].
877
+ * @returns A new {@link Timestamp}
878
+ */
879
+ export function relativeDays(timestamp, mover = nextDay, days = 1, allowedWeekdays = [0, 1, 2, 3, 4, 5, 6]) {
880
+ let ts = copyTimestamp(timestamp);
881
+ if (!allowedWeekdays.includes(Number(ts.weekday)) && ts.weekday === 0 && mover === nextDay) {
882
+ ++days;
883
+ }
884
+ while (--days >= 0) {
885
+ ts = mover(ts);
886
+ if (allowedWeekdays.length < 7 && !allowedWeekdays.includes(Number(ts.weekday))) {
887
+ ++days;
888
+ }
889
+ }
890
+ return ts;
891
+ }
892
+ /**
893
+ * Finds the specified weekday (forward or back) based on the {@link Timestamp}
894
+ * @param {Timestamp} timestamp The {@link Timestamp} to transform
895
+ * @param {number} weekday The weekday number (Sun = 0, ..., Sat = 6)
896
+ * @param {function} [mover=nextDay] The function to use ({prevDay} or {nextDay}).
897
+ * @param {number} [maxDays=6] The number of days to look forward or back.
898
+ * @returns A new {@link Timestamp}
899
+ */
900
+ export function findWeekday(timestamp, weekday, mover = nextDay, maxDays = 6) {
901
+ let ts = copyTimestamp(timestamp);
902
+ while (ts.weekday !== weekday && --maxDays >= 0)
903
+ ts = mover(ts);
904
+ return ts;
905
+ }
906
+ /**
907
+ * Creates an array of {@link Timestamp}s based on start and end params
908
+ * @param {Timestamp} start The starting {@link Timestamp}
909
+ * @param {Timestamp} end The ending {@link Timestamp}
910
+ * @param {Timestamp} now The relative day
911
+ * @param {number[]} weekdays An array of numbers (representing days of the week) that are 0 (=Sunday) to 6 (=Saturday)
912
+ * @param {string} [disabledBefore] Days before this date are disabled (YYYY-MM-DD)
913
+ * @param {string} [disabledAfter] Days after this date are disabled (YYYY-MM-DD)
914
+ * @param {number[]} [disabledWeekdays] An array representing weekdays that are disabled [0 = Sun, ..., 6 = Sat]
915
+ * @param {DisabledDays} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range.
916
+ * @param {number} [max=42] Max days to do
917
+ * @param {number} [min=0] Min days to do
918
+ * @returns {Timestamp[]} The requested array of {@link Timestamp}s
919
+ */
920
+ export function createDayList(start, end, now, weekdays = [0, 1, 2, 3, 4, 5, 6], disabledBefore = undefined, disabledAfter = undefined, disabledWeekdays = [], disabledDays = [], max = 42, min = 0) {
921
+ const begin = getDayIdentifier(start);
922
+ const stop = getDayIdentifier(end);
923
+ const days = [];
924
+ let current = copyTimestamp(start);
925
+ let currentIdentifier = 0;
926
+ let stopped = currentIdentifier === stop;
927
+ if (stop < begin) {
928
+ return days;
929
+ }
930
+ while ((!stopped || days.length < min) && days.length < max) {
931
+ currentIdentifier = getDayIdentifier(current);
932
+ stopped = stopped || (currentIdentifier > stop && days.length >= min);
933
+ if (stopped) {
934
+ break;
935
+ }
936
+ if (!weekdays.includes(Number(current.weekday))) {
937
+ current = relativeDays(current, nextDay);
938
+ continue;
939
+ }
940
+ let day = copyTimestamp(current);
941
+ day = updateFormatted(day);
942
+ day = updateRelative(day, now);
943
+ day = updateDisabled(day, disabledBefore, disabledAfter, disabledWeekdays, disabledDays);
944
+ days.push(day);
945
+ current = relativeDays(current, nextDay);
946
+ }
947
+ return days;
948
+ }
949
+ /**
950
+ * Creates an array of interval {@link Timestamp}s based on params
951
+ * @param {Timestamp} timestamp The starting {@link Timestamp}
952
+ * @param {number} first The starting interval time
953
+ * @param {number} minutes How many minutes between intervals (ie: 60, 30, 15 would be common ones)
954
+ * @param {number} count The number of intervals needed
955
+ * @param {Timestamp} now A relative {@link Timestamp} with time
956
+ * @returns {Timestamp[]} The requested array of interval {@link Timestamp}s
957
+ */
958
+ export function createIntervalList(timestamp, first, minutes, count, now) {
959
+ const intervals = [];
960
+ for (let i = 0; i < count; ++i) {
961
+ const mins = (first + i) * minutes;
962
+ intervals.push(updateMinutes(timestamp, mins, now));
963
+ }
964
+ return intervals;
965
+ }
966
+ /**
967
+ * @callback getOptions
968
+ * @param {Timestamp} timestamp A {@link Timestamp} object
969
+ * @param {boolean} short True if using short options
970
+ * @returns {Object} An Intl object representing options to be used
971
+ */
972
+ /**
973
+ * @callback formatter
974
+ * @param {Timestamp} timestamp The {@link Timestamp} being used
975
+ * @param {boolean} short If short format is being requested
976
+ * @returns {string} The localized string of the formatted {@link Timestamp}
977
+ */
978
+ /**
979
+ * Returns a locale formatter backed by `Intl.DateTimeFormat`.
980
+ *
981
+ * The helper is SSR-safe: if `Intl.DateTimeFormat` is unavailable in a target
982
+ * runtime, it returns a formatter that produces an empty string instead of
983
+ * throwing during module load.
984
+ *
985
+ * @param {string} locale The locale to use (ie: en-US)
986
+ * @param {getOptions} cb The function to call for options. This function should return an Intl formatted object. The function is passed (timestamp, short).
987
+ * @returns {formatter} The function has params (timestamp, short). The short is to use the short options.
988
+ */
989
+ export function createNativeLocaleFormatter(locale, cb) {
990
+ const emptyFormatter = () => "";
991
+ /* istanbul ignore next */
992
+ if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat === "undefined") {
993
+ return emptyFormatter;
994
+ }
995
+ return (timestamp, short) => {
996
+ try {
997
+ const intlFormatter = new Intl.DateTimeFormat(locale || undefined, cb(timestamp, short));
998
+ return intlFormatter.format(makeDateTime(timestamp));
999
+ }
1000
+ catch (e) /* istanbul ignore next */ {
1001
+ console.error(`Intl.DateTimeFormat: ${e.message} -> ${getDateTime(timestamp)}`);
1002
+ return "";
1003
+ }
1004
+ };
1005
+ }
1006
+ /**
1007
+ * Makes a JavaScript Date from the passed {@link Timestamp}
1008
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
1009
+ * @param {boolean} utc True to get Date object using UTC
1010
+ * @returns {Date} A JavaScript Date
1011
+ */
1012
+ export function makeDate(timestamp, utc = true) {
1013
+ if (utc)
1014
+ return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0));
1015
+ return new Date(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0);
1016
+ }
1017
+ /**
1018
+ * Makes a JavaScript Date from the passed {@link Timestamp} (with time)
1019
+ * @param {Timestamp} timestamp The {@link Timestamp} to use
1020
+ * @param {boolean} utc True to get Date object using UTC
1021
+ * @returns {Date} A JavaScript Date
1022
+ */
1023
+ export function makeDateTime(timestamp, utc = true) {
1024
+ if (utc)
1025
+ return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second ?? 0, timestamp.millisecond ?? 0));
1026
+ return new Date(timestamp.year, timestamp.month - 1, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second ?? 0, timestamp.millisecond ?? 0);
1027
+ }
1028
+ /**
1029
+ * Converts a {@link Timestamp} to a local JavaScript `Date`.
1030
+ *
1031
+ * This is equivalent to `makeDateTime(timestamp, false)`.
1032
+ *
1033
+ * @param {Timestamp} timestamp The {@link Timestamp} to convert
1034
+ * @returns {Date} A local JavaScript Date
1035
+ */
1036
+ export function getDateObject(timestamp) {
1037
+ return makeDateTime(timestamp, false);
1038
+ }
1039
+ /**
1040
+ * Validates if the input is a finite number.
1041
+ *
1042
+ * @param input - The value to be validated. Can be a string or a number.
1043
+ * @returns A boolean indicating whether the input is a finite number.
1044
+ * Returns true if the input is a finite number, false otherwise.
1045
+ */
1046
+ export function validateNumber(input) {
1047
+ return isFinite(Number(input));
1048
+ }
1049
+ /**
1050
+ * Given an array of {@link Timestamp}s, finds the max date (and possible time)
1051
+ * @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s
1052
+ * @param {boolean=} useTime Default false; if true, uses time in the comparison as well
1053
+ * @returns The {@link Timestamp} with the highest date (and possibly time) value
1054
+ */
1055
+ export function maxTimestamp(timestamps, useTime = false) {
1056
+ const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier;
1057
+ return timestamps.reduce((prev, cur) => {
1058
+ return Math.max(func(prev), func(cur)) === func(prev) ? prev : cur;
1059
+ });
1060
+ }
1061
+ /**
1062
+ * Given an array of {@link Timestamp}s, finds the min date (and possible time)
1063
+ * @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s
1064
+ * @param {boolean=} useTime Default false; if true, uses time in the comparison as well
1065
+ * @returns The {@link Timestamp} with the lowest date (and possibly time) value
1066
+ */
1067
+ export function minTimestamp(timestamps, useTime = false) {
1068
+ const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier;
1069
+ return timestamps.reduce((prev, cur) => {
1070
+ return Math.min(func(prev), func(cur)) === func(prev) ? prev : cur;
1071
+ });
1072
+ }
1073
+ /**
1074
+ * Determines if the passed {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range)
1075
+ * @param {Timestamp} timestamp The {@link Timestamp} for testing
1076
+ * @param {Timestamp} startTimestamp The starting {@link Timestamp}
1077
+ * @param {Timestamp} endTimestamp The ending {@link Timestamp}
1078
+ * @param {boolean=} useTime If true, use time from the {@link Timestamp}s
1079
+ * @returns {boolean} True if {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range)
1080
+ */
1081
+ export function isBetweenDates(timestamp, startTimestamp, endTimestamp, useTime = false) {
1082
+ const cd = getDayIdentifier(timestamp) + (useTime === true ? getTimeIdentifier(timestamp) : 0);
1083
+ const sd = getDayIdentifier(startTimestamp) + (useTime === true ? getTimeIdentifier(startTimestamp) : 0);
1084
+ const ed = getDayIdentifier(endTimestamp) + (useTime === true ? getTimeIdentifier(endTimestamp) : 0);
1085
+ return cd >= sd && cd <= ed;
1086
+ }
1087
+ /**
1088
+ * Determine if two ranges of {@link Timestamp}s overlap each other
1089
+ * @param {Timestamp} startTimestamp The starting {@link Timestamp} of first range
1090
+ * @param {Timestamp} endTimestamp The endinging {@link Timestamp} of first range
1091
+ * @param {Timestamp} firstTimestamp The starting {@link Timestamp} of second range
1092
+ * @param {Timestamp} lastTimestamp The ending {@link Timestamp} of second range
1093
+ * @returns {boolean} True if the two ranges overlap each other
1094
+ */
1095
+ export function isOverlappingDates(startTimestamp, endTimestamp, firstTimestamp, lastTimestamp) {
1096
+ const start = getDayIdentifier(startTimestamp);
1097
+ const end = getDayIdentifier(endTimestamp);
1098
+ const first = getDayIdentifier(firstTimestamp);
1099
+ const last = getDayIdentifier(lastTimestamp);
1100
+ return ((start >= first && start <= last) || // overlap left
1101
+ (end >= first && end <= last) || // overlap right
1102
+ (first >= start && end >= last) // surrounding
1103
+ );
1104
+ }
1105
+ /**
1106
+ * Adds or subtracts date/time units from a timestamp.
1107
+ *
1108
+ * This function returns a new frozen {@link Timestamp}; it does not mutate the
1109
+ * timestamp passed in.
1110
+ *
1111
+ * @param {Timestamp} timestamp The {@link Timestamp} object
1112
+ * @param {Object} options configuration data
1113
+ * @param {number=} options.year If positive, adds years. If negative, removes years.
1114
+ * @param {number=} options.month If positive, adds months. If negative, removes month.
1115
+ * @param {number=} options.day If positive, adds days. If negative, removes days.
1116
+ * @param {number=} options.hour If positive, adds hours. If negative, removes hours.
1117
+ * @param {number=} options.minute If positive, adds minutes. If negative, removes minutes.
1118
+ * @param {number=} options.second If positive, adds seconds. If negative, removes seconds.
1119
+ * @param {number=} options.millisecond If positive, adds milliseconds. If negative, removes milliseconds.
1120
+ * @returns {Timestamp} A new normalized {@link Timestamp}
1121
+ */
1122
+ export function addToDate(timestamp, options) {
1123
+ const ts = cloneTimestamp(timestamp);
1124
+ if (options.year)
1125
+ ts.year += options.year;
1126
+ if (options.month)
1127
+ ts.month += options.month;
1128
+ if (options.day)
1129
+ ts.day += options.day;
1130
+ if (options.hour)
1131
+ ts.hour += options.hour;
1132
+ if (options.minute)
1133
+ ts.minute += options.minute;
1134
+ if (options.second)
1135
+ ts.second = (ts.second ?? 0) + options.second;
1136
+ if (options.millisecond)
1137
+ ts.millisecond = (ts.millisecond ?? 0) + options.millisecond;
1138
+ return updateFormatted(normalizeTimestamp(ts));
1139
+ }
1140
+ /**
1141
+ * Normalizes a timestamp object by creating a JavaScript Date object and extracting standardized values.
1142
+ * This function ensures that the timestamp values are consistent and correctly represent a valid date and time.
1143
+ *
1144
+ * @param {Object} ts - The timestamp object to normalize.
1145
+ * @param {number} ts.year - The year of the timestamp.
1146
+ * @param {number} ts.month - The month of the timestamp (1-12).
1147
+ * @param {number} ts.day - The day of the month.
1148
+ * @param {number} ts.hour - The hour of the day (0-23).
1149
+ * @param {number} ts.minute - The minute of the hour (0-59).
1150
+ * @returns {Object} A new object with normalized timestamp values.
1151
+ * The returned object includes all properties from the input object,
1152
+ * with year, month, day, hour, and minute properties updated to normalized values.
1153
+ */
1154
+ function normalizeTimestamp(ts) {
1155
+ const date = new Date(ts.year, ts.month - 1, ts.day, ts.hour, ts.minute, ts.second ?? 0, ts.millisecond ?? 0);
1156
+ const timestamp = {
1157
+ ...ts,
1158
+ year: date.getFullYear(),
1159
+ month: date.getMonth() + 1,
1160
+ day: date.getDate(),
1161
+ hour: date.getHours(),
1162
+ minute: date.getMinutes(),
1163
+ };
1164
+ if (ts.second !== undefined || date.getSeconds() !== 0) {
1165
+ timestamp.second = date.getSeconds();
1166
+ }
1167
+ if (ts.millisecond !== undefined || date.getMilliseconds() !== 0) {
1168
+ timestamp.millisecond = date.getMilliseconds();
1169
+ }
1170
+ return freezeTimestamp(timestamp);
1171
+ }
1172
+ /**
1173
+ * Returns number of days between two {@link Timestamp}s
1174
+ * @param {Timestamp} ts1 The first {@link Timestamp}
1175
+ * @param {Timestamp} ts2 The second {@link Timestamp}
1176
+ * @returns Number of days
1177
+ */
1178
+ export function daysBetween(ts1, ts2) {
1179
+ const diff = diffTimestamp(ts1, ts2, true);
1180
+ return Math.floor(diff / TIME_CONSTANTS.MILLISECONDS_IN.DAY);
1181
+ }
1182
+ /**
1183
+ * Returns number of weeks between two {@link Timestamp}s
1184
+ * @param {Timestamp} ts1 The first {@link Timestamp}
1185
+ * @param {Timestamp} ts2 The second {@link Timestamp}
1186
+ */
1187
+ export function weeksBetween(ts1, ts2) {
1188
+ let t1 = copyTimestamp(ts1);
1189
+ let t2 = copyTimestamp(ts2);
1190
+ t1 = findWeekday(t1, 0);
1191
+ t2 = findWeekday(t2, 6);
1192
+ return Math.ceil(daysBetween(t1, t2) / TIME_CONSTANTS.DAYS_IN.WEEK);
1193
+ }
1194
+ // Known dates
1195
+ const weekdayDateMap = {
1196
+ Sun: new Date("2020-01-05T00:00:00.000Z"),
1197
+ Mon: new Date("2020-01-06T00:00:00.000Z"),
1198
+ Tue: new Date("2020-01-07T00:00:00.000Z"),
1199
+ Wed: new Date("2020-01-08T00:00:00.000Z"),
1200
+ Thu: new Date("2020-01-09T00:00:00.000Z"),
1201
+ Fri: new Date("2020-01-10T00:00:00.000Z"),
1202
+ Sat: new Date("2020-01-11T00:00:00.000Z"),
1203
+ };
1204
+ function resolveIntlNameFormat(options, type) {
1205
+ if (type === "long" || type === "short" || type === "narrow") {
1206
+ return options[type];
1207
+ }
1208
+ return options.long;
1209
+ }
1210
+ /**
1211
+ * Returns a function that uses Intl.DateTimeFormat to format weekdays.
1212
+ *
1213
+ * @function getWeekdayFormatter
1214
+ * @returns {function} A function that formats weekdays.
1215
+ *
1216
+ * @example
1217
+ * const formatWeekday = getWeekdayFormatter();
1218
+ * console.log(formatWeekday('Mon', 'long', 'en-US')); // "Monday"
1219
+ * console.log(formatWeekday('Mon', 'short', 'fr-FR')); // "lun."
1220
+ *
1221
+ * @param {string} weekday - The abbreviation of the weekday (e.g., 'Mon', 'Tue', 'Wed', etc.).
1222
+ * @param {string} [type='long'] - The type of formatting to use ('narrow', 'short', or 'long').
1223
+ * @param {string} [locale=''] - The locale to use for formatting.
1224
+ *
1225
+ * @returns {string} The formatted weekday.
1226
+ */
1227
+ export function getWeekdayFormatter() {
1228
+ const emptyFormatter = () => "";
1229
+ const options = {
1230
+ long: { timeZone: "UTC", weekday: "long" },
1231
+ short: { timeZone: "UTC", weekday: "short" },
1232
+ narrow: { timeZone: "UTC", weekday: "narrow" },
1233
+ };
1234
+ /* istanbul ignore next */
1235
+ if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat === "undefined") {
1236
+ return emptyFormatter;
1237
+ }
1238
+ /**
1239
+ * Formats a given weekday into a localized string based on the specified type and locale.
1240
+ *
1241
+ * @param {number} weekday - The day of the week (0 for Sunday, 1 for Monday, etc.).
1242
+ * @param {string} type - The format type (e.g., 'narrow', 'short', 'long') to use for formatting.
1243
+ * @param {string} [locale] - The locale string (e.g., 'en-US') to use for formatting. Defaults to the user's locale if not provided.
1244
+ * @returns {string} The formatted weekday string.
1245
+ */
1246
+ function weekdayFormatter(weekday, type, locale) {
1247
+ try {
1248
+ const intlFormatter = new Intl.DateTimeFormat(locale || undefined, resolveIntlNameFormat(options, type));
1249
+ return intlFormatter.format(weekdayDateMap[weekday]);
1250
+ }
1251
+ catch (e) /* istanbul ignore next */ {
1252
+ if (e instanceof Error) {
1253
+ console.error(`Intl.DateTimeFormat: ${e.message} -> day of week: ${weekday}`);
1254
+ }
1255
+ return "";
1256
+ }
1257
+ }
1258
+ return weekdayFormatter;
1259
+ }
1260
+ /**
1261
+ * Retrieves an array of localized weekday names.
1262
+ *
1263
+ * @param {string} type - The format type for the weekday names. Can be 'narrow', 'short', or 'long'.
1264
+ * @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
1265
+ * @returns {string[]} An array of localized weekday names in the specified format.
1266
+ */
1267
+ export function getWeekdayNames(type, locale) {
1268
+ const shortWeekdays = Object.keys(weekdayDateMap);
1269
+ const weekdayFormatter = getWeekdayFormatter();
1270
+ return shortWeekdays.map((weekday) => String(weekdayFormatter(weekday, type, locale)));
1271
+ }
1272
+ /**
1273
+ * Creates and returns a function for formatting month names based on locale and format type.
1274
+ *
1275
+ * @returns {Function} A function that formats month names.
1276
+ * The returned function accepts the following parameters:
1277
+ * @param {number} month - The month to format (0-11, where 0 is January).
1278
+ * @param {string} [type='long'] - The format type: 'narrow', 'short', or 'long'.
1279
+ * @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
1280
+ * @returns {string} The formatted month name.
1281
+ *
1282
+ * @throws {Error} If Intl or Intl.DateTimeFormat is not supported in the environment.
1283
+ */
1284
+ export function getMonthFormatter() {
1285
+ const emptyFormatter = () => "";
1286
+ const options = {
1287
+ long: { timeZone: "UTC", month: "long" },
1288
+ short: { timeZone: "UTC", month: "short" },
1289
+ narrow: { timeZone: "UTC", month: "narrow" },
1290
+ };
1291
+ /* istanbul ignore next */
1292
+ if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat === "undefined") {
1293
+ return emptyFormatter;
1294
+ }
1295
+ /**
1296
+ * Formats a given month into a string based on the specified type and locale.
1297
+ *
1298
+ * @param {number} month - The month to format (0 for January, 11 for December).
1299
+ * @param {string} type - The format type (e.g., 'narrow', 'long', 'short', etc.).
1300
+ * @param {string} [locale] - The locale to use for formatting (defaults to the system locale if not provided).
1301
+ * @returns {string} The formatted month string.
1302
+ */
1303
+ function monthFormatter(month, type = "long", locale) {
1304
+ try {
1305
+ const intlFormatter = new Intl.DateTimeFormat(locale || undefined, resolveIntlNameFormat(options, type));
1306
+ const date = new Date();
1307
+ date.setDate(1);
1308
+ date.setMonth(month);
1309
+ return intlFormatter.format(date);
1310
+ }
1311
+ catch (e) /* istanbul ignore next */ {
1312
+ if (e instanceof Error) {
1313
+ console.error(`Intl.DateTimeFormat: ${e.message} -> month: ${month}`);
1314
+ }
1315
+ return "";
1316
+ }
1317
+ }
1318
+ return monthFormatter;
1319
+ }
1320
+ /**
1321
+ * Retrieves an array of localized month names.
1322
+ *
1323
+ * @param {string} type - The format type for the month names. Can be 'narrow', 'short', or 'long'.
1324
+ * @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
1325
+ * @returns {string[]} An array of localized month names in the specified format.
1326
+ */
1327
+ export function getMonthNames(type, locale) {
1328
+ const monthFormatter = getMonthFormatter();
1329
+ return [...Array(12).keys()].map((month) => monthFormatter(month, type, locale));
1330
+ }
1331
+ // the exports...
1332
+ export default {
1333
+ PARSE_DATETIME,
1334
+ PARSE_DATE,
1335
+ PARSE_TIME,
1336
+ DAYS_IN_MONTH,
1337
+ DAYS_IN_MONTH_LEAP,
1338
+ DAYS_IN_MONTH_MIN,
1339
+ DAYS_IN_MONTH_MAX,
1340
+ MONTH_MAX,
1341
+ MONTH_MIN,
1342
+ DAY_MIN,
1343
+ TIME_CONSTANTS,
1344
+ DAYS_IN_WEEK,
1345
+ MINUTES_IN_HOUR,
1346
+ HOURS_IN_DAY,
1347
+ FIRST_HOUR,
1348
+ MILLISECONDS_IN_MINUTE,
1349
+ MILLISECONDS_IN_HOUR,
1350
+ MILLISECONDS_IN_DAY,
1351
+ MILLISECONDS_IN_WEEK,
1352
+ Timestamp,
1353
+ TimeObject,
1354
+ today,
1355
+ getStartOfWeek,
1356
+ getEndOfWeek,
1357
+ getStartOfMonth,
1358
+ getEndOfMonth,
1359
+ parseTime,
1360
+ validateTimestamp,
1361
+ parsed,
1362
+ parseTimestamp,
1363
+ parseDate,
1364
+ getDayIdentifier,
1365
+ getTimeIdentifier,
1366
+ getDayTimeIdentifier,
1367
+ diffTimestamp,
1368
+ updateRelative,
1369
+ updateMinutes,
1370
+ updateWeekday,
1371
+ updateDayOfYear,
1372
+ updateWorkWeek,
1373
+ updateDisabled,
1374
+ updateFormatted,
1375
+ getDayOfYear,
1376
+ getWorkWeek,
1377
+ getWeekday,
1378
+ isLeapYear,
1379
+ daysInMonth,
1380
+ copyTimestamp,
1381
+ padNumber,
1382
+ getDate,
1383
+ getTime,
1384
+ getDateTime,
1385
+ nextDay,
1386
+ prevDay,
1387
+ relativeDays,
1388
+ findWeekday,
1389
+ createDayList,
1390
+ createIntervalList,
1391
+ createNativeLocaleFormatter,
1392
+ makeDate,
1393
+ makeDateTime,
1394
+ getDateObject,
1395
+ validateNumber,
1396
+ isBetweenDates,
1397
+ isOverlappingDates,
1398
+ daysBetween,
1399
+ weeksBetween,
1400
+ addToDate,
1401
+ compareTimestamps,
1402
+ compareDate,
1403
+ compareTime,
1404
+ compareDateTime,
1405
+ getWeekdayFormatter,
1406
+ getWeekdayNames,
1407
+ getMonthFormatter,
1408
+ getMonthNames,
1409
+ };
1410
+ //# sourceMappingURL=index.js.map