@forcecalendar/core 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,553 @@
1
+ /**
2
+ * DateUtils - Date manipulation utilities
3
+ * Pure functions, no external dependencies
4
+ * Locker Service compatible
5
+ */
6
+ export class DateUtils {
7
+ /**
8
+ * Get the start of a day
9
+ * @param {Date} date - The date
10
+ * @returns {Date}
11
+ */
12
+ static startOfDay(date) {
13
+ const result = new Date(date);
14
+ result.setHours(0, 0, 0, 0);
15
+ return result;
16
+ }
17
+
18
+ /**
19
+ * Get the end of a day
20
+ * @param {Date} date - The date
21
+ * @returns {Date}
22
+ */
23
+ static endOfDay(date) {
24
+ const result = new Date(date);
25
+ result.setHours(23, 59, 59, 999);
26
+ return result;
27
+ }
28
+
29
+ /**
30
+ * Get the start of a week
31
+ * @param {Date} date - The date
32
+ * @param {number} [weekStartsOn=0] - 0 = Sunday, 1 = Monday, etc.
33
+ * @returns {Date} Start of the week
34
+ */
35
+ static startOfWeek(date, weekStartsOn = 0) {
36
+ const result = new Date(date);
37
+ const day = result.getDay();
38
+ const diff = (day < weekStartsOn ? 7 : 0) + day - weekStartsOn;
39
+
40
+ // Use setTime to handle month/year boundaries correctly
41
+ result.setTime(result.getTime() - (diff * 24 * 60 * 60 * 1000));
42
+ result.setHours(0, 0, 0, 0);
43
+ return result;
44
+ }
45
+
46
+ /**
47
+ * Get the end of a week
48
+ * @param {Date} date - The date
49
+ * @param {number} weekStartsOn - 0 = Sunday, 1 = Monday, etc.
50
+ * @returns {Date}
51
+ */
52
+ static endOfWeek(date, weekStartsOn = 0) {
53
+ const result = DateUtils.startOfWeek(date, weekStartsOn);
54
+ // Use setTime to handle month/year boundaries correctly
55
+ result.setTime(result.getTime() + (6 * 24 * 60 * 60 * 1000));
56
+ result.setHours(23, 59, 59, 999);
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Get the start of a month
62
+ * @param {Date} date - The date
63
+ * @returns {Date}
64
+ */
65
+ static startOfMonth(date) {
66
+ return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
67
+ }
68
+
69
+ /**
70
+ * Get the end of a month
71
+ * @param {Date} date - The date
72
+ * @returns {Date}
73
+ */
74
+ static endOfMonth(date) {
75
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
76
+ }
77
+
78
+ /**
79
+ * Get the start of a year
80
+ * @param {Date} date - The date
81
+ * @returns {Date}
82
+ */
83
+ static startOfYear(date) {
84
+ return new Date(date.getFullYear(), 0, 1, 0, 0, 0, 0);
85
+ }
86
+
87
+ /**
88
+ * Get the end of a year
89
+ * @param {Date} date - The date
90
+ * @returns {Date}
91
+ */
92
+ static endOfYear(date) {
93
+ return new Date(date.getFullYear(), 11, 31, 23, 59, 59, 999);
94
+ }
95
+
96
+ /**
97
+ * Add days to a date
98
+ * @param {Date} date - The date
99
+ * @param {number} days - Number of days to add (can be negative)
100
+ * @returns {Date}
101
+ */
102
+ static addDays(date, days) {
103
+ const result = new Date(date);
104
+ // Use setTime to handle month/year boundaries correctly
105
+ result.setTime(result.getTime() + (days * 24 * 60 * 60 * 1000));
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Add weeks to a date
111
+ * @param {Date} date - The date
112
+ * @param {number} weeks - Number of weeks to add
113
+ * @returns {Date}
114
+ */
115
+ static addWeeks(date, weeks) {
116
+ return DateUtils.addDays(date, weeks * 7);
117
+ }
118
+
119
+ /**
120
+ * Add months to a date
121
+ * @param {Date} date - The date
122
+ * @param {number} months - Number of months to add
123
+ * @returns {Date}
124
+ */
125
+ static addMonths(date, months) {
126
+ const result = new Date(date);
127
+ const dayOfMonth = result.getDate();
128
+ result.setMonth(result.getMonth() + months);
129
+
130
+ // Handle edge case where day doesn't exist in new month
131
+ if (result.getDate() !== dayOfMonth) {
132
+ result.setDate(0); // Go to last day of previous month
133
+ }
134
+
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * Add years to a date
140
+ * @param {Date} date - The date
141
+ * @param {number} years - Number of years to add
142
+ * @returns {Date}
143
+ */
144
+ static addYears(date, years) {
145
+ const result = new Date(date);
146
+ result.setFullYear(result.getFullYear() + years);
147
+ return result;
148
+ }
149
+
150
+ /**
151
+ * Get a consistent UTC date string for indexing (YYYY-MM-DD format)
152
+ * @param {Date} date - The date
153
+ * @returns {string} UTC date string
154
+ */
155
+ static getUTCDateString(date) {
156
+ const year = date.getUTCFullYear();
157
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
158
+ const day = String(date.getUTCDate()).padStart(2, '0');
159
+ return `${year}-${month}-${day}`;
160
+ }
161
+
162
+ /**
163
+ * Get a consistent local date string for indexing (YYYY-MM-DD format)
164
+ * @param {Date} date - The date
165
+ * @returns {string} Local date string
166
+ */
167
+ static getLocalDateString(date) {
168
+ const year = date.getFullYear();
169
+ const month = String(date.getMonth() + 1).padStart(2, '0');
170
+ const day = String(date.getDate()).padStart(2, '0');
171
+ return `${year}-${month}-${day}`;
172
+ }
173
+
174
+ /**
175
+ * Check if a date is today
176
+ * @param {Date} date - The date to check
177
+ * @returns {boolean}
178
+ */
179
+ static isToday(date) {
180
+ const today = new Date();
181
+ return date.toDateString() === today.toDateString();
182
+ }
183
+
184
+ /**
185
+ * Check if a date is in the past
186
+ * @param {Date} date - The date to check
187
+ * @returns {boolean}
188
+ */
189
+ static isPast(date) {
190
+ return date < new Date();
191
+ }
192
+
193
+ /**
194
+ * Check if a date is in the future
195
+ * @param {Date} date - The date to check
196
+ * @returns {boolean}
197
+ */
198
+ static isFuture(date) {
199
+ return date > new Date();
200
+ }
201
+
202
+ /**
203
+ * Check if two dates are on the same day
204
+ * @param {Date} date1 - First date
205
+ * @param {Date} date2 - Second date
206
+ * @returns {boolean}
207
+ */
208
+ static isSameDay(date1, date2) {
209
+ return date1.getFullYear() === date2.getFullYear() &&
210
+ date1.getMonth() === date2.getMonth() &&
211
+ date1.getDate() === date2.getDate();
212
+ }
213
+
214
+ /**
215
+ * Check if two dates are in the same week
216
+ * @param {Date} date1 - First date
217
+ * @param {Date} date2 - Second date
218
+ * @param {number} weekStartsOn - 0 = Sunday, 1 = Monday, etc.
219
+ * @returns {boolean}
220
+ */
221
+ static isSameWeek(date1, date2, weekStartsOn = 0) {
222
+ const week1Start = DateUtils.startOfWeek(date1, weekStartsOn);
223
+ const week2Start = DateUtils.startOfWeek(date2, weekStartsOn);
224
+ return week1Start.toDateString() === week2Start.toDateString();
225
+ }
226
+
227
+ /**
228
+ * Check if two dates are in the same month
229
+ * @param {Date} date1 - First date
230
+ * @param {Date} date2 - Second date
231
+ * @returns {boolean}
232
+ */
233
+ static isSameMonth(date1, date2) {
234
+ return date1.getFullYear() === date2.getFullYear() &&
235
+ date1.getMonth() === date2.getMonth();
236
+ }
237
+
238
+ /**
239
+ * Check if two dates are in the same year
240
+ * @param {Date} date1 - First date
241
+ * @param {Date} date2 - Second date
242
+ * @returns {boolean}
243
+ */
244
+ static isSameYear(date1, date2) {
245
+ return date1.getFullYear() === date2.getFullYear();
246
+ }
247
+
248
+ /**
249
+ * Get the difference in days between two dates
250
+ * @param {Date} date1 - First date
251
+ * @param {Date} date2 - Second date
252
+ * @returns {number}
253
+ */
254
+ static differenceInDays(date1, date2) {
255
+ const diff = date1.getTime() - date2.getTime();
256
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
257
+ }
258
+
259
+ /**
260
+ * Get the difference in weeks between two dates
261
+ * @param {Date} date1 - First date
262
+ * @param {Date} date2 - Second date
263
+ * @returns {number}
264
+ */
265
+ static differenceInWeeks(date1, date2) {
266
+ return Math.floor(DateUtils.differenceInDays(date1, date2) / 7);
267
+ }
268
+
269
+ /**
270
+ * Get the difference in months between two dates
271
+ * @param {Date} date1 - First date
272
+ * @param {Date} date2 - Second date
273
+ * @returns {number}
274
+ */
275
+ static differenceInMonths(date1, date2) {
276
+ const yearDiff = date1.getFullYear() - date2.getFullYear();
277
+ const monthDiff = date1.getMonth() - date2.getMonth();
278
+ return yearDiff * 12 + monthDiff;
279
+ }
280
+
281
+ /**
282
+ * Get the week number of a date
283
+ * @param {Date} date - The date
284
+ * @returns {number}
285
+ */
286
+ static getWeekNumber(date) {
287
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
288
+ const pastDaysOfYear = (date - firstDayOfYear) / 86400000;
289
+ return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
290
+ }
291
+
292
+ /**
293
+ * Get the day of week for a date
294
+ * @param {Date} date - The date
295
+ * @param {number} weekStartsOn - 0 = Sunday, 1 = Monday, etc.
296
+ * @returns {number} 0-6 where 0 is the first day of the week
297
+ */
298
+ static getDayOfWeek(date, weekStartsOn = 0) {
299
+ const day = date.getDay();
300
+ return (day - weekStartsOn + 7) % 7;
301
+ }
302
+
303
+ /**
304
+ * Get days in a month
305
+ * @param {Date} date - Any date in the month
306
+ * @returns {number}
307
+ */
308
+ static getDaysInMonth(date) {
309
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
310
+ }
311
+
312
+ /**
313
+ * Format a date using Intl.DateTimeFormat
314
+ * @param {Date} date - The date to format
315
+ * @param {string} locale - The locale
316
+ * @param {Object} options - Intl.DateTimeFormat options
317
+ * @returns {string}
318
+ */
319
+ static format(date, locale = 'en-US', options = {}) {
320
+ return new Intl.DateTimeFormat(locale, options).format(date);
321
+ }
322
+
323
+ /**
324
+ * Get month name
325
+ * @param {Date} date - The date
326
+ * @param {string} locale - The locale
327
+ * @param {string} format - 'long', 'short', or 'narrow'
328
+ * @returns {string}
329
+ */
330
+ static getMonthName(date, locale = 'en-US', format = 'long') {
331
+ return DateUtils.format(date, locale, { month: format });
332
+ }
333
+
334
+ /**
335
+ * Get day name
336
+ * @param {Date} date - The date
337
+ * @param {string} locale - The locale
338
+ * @param {string} format - 'long', 'short', or 'narrow'
339
+ * @returns {string}
340
+ */
341
+ static getDayName(date, locale = 'en-US', format = 'long') {
342
+ return DateUtils.format(date, locale, { weekday: format });
343
+ }
344
+
345
+ /**
346
+ * Format time
347
+ * @param {Date} date - The date
348
+ * @param {string} locale - The locale
349
+ * @param {boolean} use24Hour - Use 24-hour format
350
+ * @returns {string}
351
+ */
352
+ static formatTime(date, locale = 'en-US', use24Hour = false) {
353
+ return DateUtils.format(date, locale, {
354
+ hour: 'numeric',
355
+ minute: '2-digit',
356
+ hour12: !use24Hour
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Parse a time string (HH:MM) to hours and minutes
362
+ * @param {string} timeString - Time string like "09:30"
363
+ * @returns {{hours: number, minutes: number}}
364
+ */
365
+ static parseTime(timeString) {
366
+ const [hours, minutes] = timeString.split(':').map(Number);
367
+ return { hours, minutes };
368
+ }
369
+
370
+ /**
371
+ * Set time on a date
372
+ * @param {Date} date - The date
373
+ * @param {string} timeString - Time string like "09:30"
374
+ * @returns {Date}
375
+ */
376
+ static setTime(date, timeString) {
377
+ const result = new Date(date);
378
+ const { hours, minutes } = DateUtils.parseTime(timeString);
379
+ result.setHours(hours, minutes, 0, 0);
380
+ return result;
381
+ }
382
+
383
+ /**
384
+ * Check if a year is a leap year
385
+ * @param {number} year - The year
386
+ * @returns {boolean}
387
+ */
388
+ static isLeapYear(year) {
389
+ return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
390
+ }
391
+
392
+ /**
393
+ * Get an array of dates between start and end
394
+ * @param {Date} start - Start date
395
+ * @param {Date} end - End date
396
+ * @returns {Date[]}
397
+ */
398
+ static getDateRange(start, end) {
399
+ const dates = [];
400
+ const current = new Date(start);
401
+ const endTime = end.getTime();
402
+
403
+ while (current.getTime() <= endTime) {
404
+ dates.push(new Date(current));
405
+ // Use setTime to handle month/year boundaries correctly
406
+ current.setTime(current.getTime() + (24 * 60 * 60 * 1000));
407
+ }
408
+
409
+ return dates;
410
+ }
411
+
412
+ /**
413
+ * Clone a date
414
+ * @param {Date} date - The date to clone
415
+ * @returns {Date}
416
+ */
417
+ static clone(date) {
418
+ return new Date(date);
419
+ }
420
+
421
+ /**
422
+ * Validate if a value is a valid date
423
+ * @param {*} value - Value to check
424
+ * @returns {boolean}
425
+ */
426
+ static isValidDate(value) {
427
+ return value instanceof Date && !isNaN(value.getTime());
428
+ }
429
+
430
+ /**
431
+ * Convert a date to a specific timezone
432
+ * @param {Date} date - The date to convert
433
+ * @param {string} timeZone - IANA timezone string (e.g., 'America/New_York')
434
+ * @returns {Date} - Date object adjusted for timezone
435
+ */
436
+ static toTimeZone(date, timeZone) {
437
+ // Get the date string in the target timezone
438
+ const formatter = new Intl.DateTimeFormat('en-US', {
439
+ timeZone,
440
+ year: 'numeric',
441
+ month: '2-digit',
442
+ day: '2-digit',
443
+ hour: '2-digit',
444
+ minute: '2-digit',
445
+ second: '2-digit',
446
+ hour12: false
447
+ });
448
+
449
+ const parts = formatter.formatToParts(date);
450
+ const dateObj = {};
451
+ parts.forEach(part => {
452
+ if (part.type !== 'literal') {
453
+ dateObj[part.type] = part.value;
454
+ }
455
+ });
456
+
457
+ // Create new date in the target timezone
458
+ return new Date(
459
+ `${dateObj.year}-${dateObj.month}-${dateObj.day}T${dateObj.hour}:${dateObj.minute}:${dateObj.second}`
460
+ );
461
+ }
462
+
463
+ /**
464
+ * Get timezone offset in minutes for a date
465
+ * @param {Date} date - The date
466
+ * @param {string} timeZone - IANA timezone string
467
+ * @returns {number} - Offset in minutes
468
+ */
469
+ static getTimezoneOffset(date, timeZone) {
470
+ const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
471
+ const tzDate = new Date(date.toLocaleString('en-US', { timeZone }));
472
+ return (utcDate.getTime() - tzDate.getTime()) / 60000;
473
+ }
474
+
475
+ /**
476
+ * Check if DST is in effect for a date in a timezone
477
+ * @param {Date} date - The date to check
478
+ * @param {string} timeZone - IANA timezone string
479
+ * @returns {boolean}
480
+ */
481
+ static isDST(date, timeZone) {
482
+ const jan = new Date(date.getFullYear(), 0, 1);
483
+ const jul = new Date(date.getFullYear(), 6, 1);
484
+ const janOffset = DateUtils.getTimezoneOffset(jan, timeZone);
485
+ const julOffset = DateUtils.getTimezoneOffset(jul, timeZone);
486
+ const currentOffset = DateUtils.getTimezoneOffset(date, timeZone);
487
+
488
+ return Math.max(janOffset, julOffset) === currentOffset;
489
+ }
490
+
491
+ /**
492
+ * Add time accounting for DST transitions
493
+ * @param {Date} date - The date
494
+ * @param {number} hours - Hours to add
495
+ * @param {string} timeZone - IANA timezone string
496
+ * @returns {Date}
497
+ */
498
+ static addHoursWithDST(date, hours, timeZone) {
499
+ const result = new Date(date);
500
+ const originalOffset = DateUtils.getTimezoneOffset(date, timeZone);
501
+
502
+ // Add hours
503
+ result.setTime(result.getTime() + (hours * 60 * 60 * 1000));
504
+
505
+ // Check if DST transition occurred
506
+ const newOffset = DateUtils.getTimezoneOffset(result, timeZone);
507
+ if (originalOffset !== newOffset) {
508
+ // Adjust for DST change
509
+ const dstAdjustment = (newOffset - originalOffset) * 60000;
510
+ result.setTime(result.getTime() + dstAdjustment);
511
+ }
512
+
513
+ return result;
514
+ }
515
+
516
+ /**
517
+ * Create a date in a specific timezone
518
+ * @param {number} year
519
+ * @param {number} month - 0-indexed
520
+ * @param {number} day
521
+ * @param {number} hour
522
+ * @param {number} minute
523
+ * @param {number} second
524
+ * @param {string} timeZone - IANA timezone string
525
+ * @returns {Date}
526
+ */
527
+ static createInTimeZone(year, month, day, hour = 0, minute = 0, second = 0, timeZone) {
528
+ // Create date string in ISO format
529
+ const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
530
+ const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
531
+
532
+ // Use Intl API to get the UTC time for this local time in the timezone
533
+ const formatter = new Intl.DateTimeFormat('en-US', {
534
+ timeZone,
535
+ year: 'numeric',
536
+ month: '2-digit',
537
+ day: '2-digit',
538
+ hour: '2-digit',
539
+ minute: '2-digit',
540
+ second: '2-digit',
541
+ hour12: false
542
+ });
543
+
544
+ // Parse the local date in the target timezone
545
+ const localDate = new Date(`${dateStr}T${timeStr}`);
546
+
547
+ // Get offset and adjust
548
+ const offset = DateUtils.getTimezoneOffset(localDate, timeZone);
549
+ const utcTime = localDate.getTime() + (offset * 60000);
550
+
551
+ return new Date(utcTime);
552
+ }
553
+ }