@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.
- package/core/calendar/Calendar.js +715 -0
- package/core/calendar/DateUtils.js +553 -0
- package/core/conflicts/ConflictDetector.js +517 -0
- package/core/events/Event.js +914 -0
- package/core/events/EventStore.js +1198 -0
- package/core/events/RRuleParser.js +420 -0
- package/core/events/RecurrenceEngine.js +382 -0
- package/core/ics/ICSHandler.js +389 -0
- package/core/ics/ICSParser.js +475 -0
- package/core/performance/AdaptiveMemoryManager.js +333 -0
- package/core/performance/LRUCache.js +118 -0
- package/core/performance/PerformanceOptimizer.js +523 -0
- package/core/search/EventSearch.js +476 -0
- package/core/state/StateManager.js +546 -0
- package/core/timezone/TimezoneDatabase.js +294 -0
- package/core/timezone/TimezoneManager.js +419 -0
- package/core/types.js +366 -0
- package/package.json +11 -9
|
@@ -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
|
+
}
|