@forcecalendar/core 2.1.0 → 2.1.2
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 +7 -9
- package/core/calendar/DateUtils.js +10 -9
- package/core/conflicts/ConflictDetector.js +24 -24
- package/core/events/Event.js +14 -20
- package/core/events/EventStore.js +70 -19
- package/core/events/RRuleParser.js +423 -394
- package/core/events/RecurrenceEngine.js +33 -21
- package/core/events/RecurrenceEngineV2.js +536 -562
- package/core/ics/ICSHandler.js +348 -348
- package/core/ics/ICSParser.js +433 -435
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +363 -398
- package/core/performance/AdaptiveMemoryManager.js +310 -308
- package/core/performance/LRUCache.js +3 -4
- package/core/performance/PerformanceOptimizer.js +4 -6
- package/core/search/EventSearch.js +409 -417
- package/core/search/SearchWorkerManager.js +338 -338
- package/core/state/StateManager.js +4 -2
- package/core/timezone/TimezoneDatabase.js +574 -271
- package/core/timezone/TimezoneManager.js +422 -402
- package/core/types.js +1 -1
- package/package.json +1 -1
|
@@ -7,631 +7,605 @@ import { TimezoneManager } from '../timezone/TimezoneManager.js';
|
|
|
7
7
|
import { RRuleParser } from './RRuleParser.js';
|
|
8
8
|
|
|
9
9
|
export class RecurrenceEngineV2 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
constructor() {
|
|
11
|
+
// Use singleton to share cache across all components
|
|
12
|
+
this.tzManager = TimezoneManager.getInstance();
|
|
13
|
+
|
|
14
|
+
// Cache for expanded occurrences
|
|
15
|
+
this.occurrenceCache = new Map();
|
|
16
|
+
this.cacheSize = 100;
|
|
17
|
+
|
|
18
|
+
// Modified instances storage
|
|
19
|
+
this.modifiedInstances = new Map(); // eventId -> Map(occurrenceDate -> modifications)
|
|
20
|
+
|
|
21
|
+
// Exception storage with reasons
|
|
22
|
+
this.exceptionStore = new Map(); // eventId -> Map(date -> reason)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Expand recurring event with advanced handling
|
|
27
|
+
* @param {Event} event - Recurring event
|
|
28
|
+
* @param {Date} rangeStart - Start of expansion range
|
|
29
|
+
* @param {Date} rangeEnd - End of expansion range
|
|
30
|
+
* @param {Object} options - Expansion options
|
|
31
|
+
* @returns {Array} Expanded occurrences
|
|
32
|
+
*/
|
|
33
|
+
expandEvent(event, rangeStart, rangeEnd, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
maxOccurrences = 365,
|
|
36
|
+
includeModified = true,
|
|
37
|
+
includeCancelled = false,
|
|
38
|
+
timezone = event.timeZone || 'UTC',
|
|
39
|
+
handleDST = true
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
// Check cache
|
|
43
|
+
const cacheKey = this.getCacheKey(event.id, rangeStart, rangeEnd, options);
|
|
44
|
+
if (this.occurrenceCache.has(cacheKey)) {
|
|
45
|
+
return this.occurrenceCache.get(cacheKey);
|
|
23
46
|
}
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* @param {Date} rangeStart - Start of expansion range
|
|
29
|
-
* @param {Date} rangeEnd - End of expansion range
|
|
30
|
-
* @param {Object} options - Expansion options
|
|
31
|
-
* @returns {Array} Expanded occurrences
|
|
32
|
-
*/
|
|
33
|
-
expandEvent(event, rangeStart, rangeEnd, options = {}) {
|
|
34
|
-
const {
|
|
35
|
-
maxOccurrences = 365,
|
|
36
|
-
includeModified = true,
|
|
37
|
-
includeCancelled = false,
|
|
38
|
-
timezone = event.timeZone || 'UTC',
|
|
39
|
-
handleDST = true
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
42
|
-
// Check cache
|
|
43
|
-
const cacheKey = this.getCacheKey(event.id, rangeStart, rangeEnd, options);
|
|
44
|
-
if (this.occurrenceCache.has(cacheKey)) {
|
|
45
|
-
return this.occurrenceCache.get(cacheKey);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!event.recurring || !event.recurrenceRule) {
|
|
49
|
-
return [this.createOccurrence(event, event.start, event.end)];
|
|
50
|
-
}
|
|
48
|
+
if (!event.recurring || !event.recurrenceRule) {
|
|
49
|
+
return [this.createOccurrence(event, event.start, event.end)];
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rangeEnd,
|
|
69
|
-
timezone
|
|
70
|
-
);
|
|
71
|
-
}
|
|
52
|
+
const rule = RRuleParser.parse(event.recurrenceRule);
|
|
53
|
+
const occurrences = [];
|
|
54
|
+
const duration = event.end - event.start;
|
|
55
|
+
|
|
56
|
+
// Initialize expansion state
|
|
57
|
+
const state = {
|
|
58
|
+
currentDate: new Date(event.start),
|
|
59
|
+
count: 0,
|
|
60
|
+
tzOffsets: new Map(),
|
|
61
|
+
dstTransitions: []
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Pre-calculate DST transitions in range
|
|
65
|
+
if (handleDST) {
|
|
66
|
+
state.dstTransitions = this.findDSTTransitions(rangeStart, rangeEnd, timezone);
|
|
67
|
+
}
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
timezone
|
|
95
|
-
);
|
|
96
|
-
state.count++;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
occurrence.status = 'cancelled';
|
|
100
|
-
occurrence.cancellationReason = this.getExceptionReason(
|
|
101
|
-
event.id,
|
|
102
|
-
occurrence.start
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Apply modifications if any
|
|
107
|
-
if (includeModified) {
|
|
108
|
-
const modified = this.getModifiedInstance(
|
|
109
|
-
event.id,
|
|
110
|
-
occurrence.start
|
|
111
|
-
);
|
|
112
|
-
if (modified) {
|
|
113
|
-
Object.assign(occurrence, modified);
|
|
114
|
-
occurrence.isModified = true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
occurrences.push(occurrence);
|
|
119
|
-
}
|
|
69
|
+
// Expand occurrences
|
|
70
|
+
while (state.currentDate <= rangeEnd && state.count < maxOccurrences) {
|
|
71
|
+
if (state.currentDate >= rangeStart) {
|
|
72
|
+
const occurrence = this.generateOccurrence(
|
|
73
|
+
event,
|
|
74
|
+
state.currentDate,
|
|
75
|
+
duration,
|
|
76
|
+
timezone,
|
|
77
|
+
state
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Check exceptions and modifications
|
|
81
|
+
if (occurrence) {
|
|
82
|
+
const dateKey = this.getDateKey(occurrence.start);
|
|
83
|
+
|
|
84
|
+
// Skip if exception
|
|
85
|
+
if (this.isException(event.id, occurrence.start, rule)) {
|
|
86
|
+
if (!includeCancelled) {
|
|
87
|
+
state.currentDate = this.getNextDate(state.currentDate, rule, timezone);
|
|
88
|
+
state.count++;
|
|
89
|
+
continue;
|
|
120
90
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Check COUNT limit
|
|
132
|
-
if (rule.count && state.count >= rule.count) {
|
|
133
|
-
break;
|
|
91
|
+
occurrence.status = 'cancelled';
|
|
92
|
+
occurrence.cancellationReason = this.getExceptionReason(event.id, occurrence.start);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Apply modifications if any
|
|
96
|
+
if (includeModified) {
|
|
97
|
+
const modified = this.getModifiedInstance(event.id, occurrence.start);
|
|
98
|
+
if (modified) {
|
|
99
|
+
Object.assign(occurrence, modified);
|
|
100
|
+
occurrence.isModified = true;
|
|
134
101
|
}
|
|
102
|
+
}
|
|
135
103
|
|
|
136
|
-
|
|
137
|
-
if (rule.until && state.currentDate > rule.until) {
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
104
|
+
occurrences.push(occurrence);
|
|
140
105
|
}
|
|
106
|
+
}
|
|
141
107
|
|
|
142
|
-
|
|
143
|
-
|
|
108
|
+
// Get next occurrence date
|
|
109
|
+
state.currentDate = this.getNextDate(state.currentDate, rule, timezone, state);
|
|
110
|
+
state.count++;
|
|
144
111
|
|
|
145
|
-
|
|
146
|
-
|
|
112
|
+
// Check COUNT limit
|
|
113
|
+
if (rule.count && state.count >= rule.count) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
147
116
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const start = new Date(date);
|
|
153
|
-
const end = new Date(date.getTime() + duration);
|
|
154
|
-
|
|
155
|
-
// Handle DST transitions
|
|
156
|
-
if (state.dstTransitions.length > 0) {
|
|
157
|
-
const adjusted = this.adjustForDST(
|
|
158
|
-
start,
|
|
159
|
-
end,
|
|
160
|
-
timezone,
|
|
161
|
-
state.dstTransitions
|
|
162
|
-
);
|
|
163
|
-
start.setTime(adjusted.start.getTime());
|
|
164
|
-
end.setTime(adjusted.end.getTime());
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
id: `${event.id}_${start.getTime()}`,
|
|
169
|
-
recurringEventId: event.id,
|
|
170
|
-
title: event.title,
|
|
171
|
-
start,
|
|
172
|
-
end,
|
|
173
|
-
startUTC: this.tzManager.toUTC(start, timezone),
|
|
174
|
-
endUTC: this.tzManager.toUTC(end, timezone),
|
|
175
|
-
timezone,
|
|
176
|
-
originalStart: event.start,
|
|
177
|
-
allDay: event.allDay,
|
|
178
|
-
description: event.description,
|
|
179
|
-
location: event.location,
|
|
180
|
-
categories: event.categories,
|
|
181
|
-
status: 'confirmed',
|
|
182
|
-
isRecurring: true,
|
|
183
|
-
isModified: false
|
|
184
|
-
};
|
|
117
|
+
// Check UNTIL limit
|
|
118
|
+
if (rule.until && state.currentDate > rule.until) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
185
121
|
}
|
|
186
122
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*/
|
|
190
|
-
getNextDate(currentDate, rule, timezone, state = {}) {
|
|
191
|
-
const next = new Date(currentDate);
|
|
192
|
-
|
|
193
|
-
switch (rule.freq) {
|
|
194
|
-
case 'DAILY':
|
|
195
|
-
return this.getNextDaily(next, rule);
|
|
123
|
+
// Cache results
|
|
124
|
+
this.cacheOccurrences(cacheKey, occurrences);
|
|
196
125
|
|
|
197
|
-
|
|
198
|
-
|
|
126
|
+
return occurrences;
|
|
127
|
+
}
|
|
199
128
|
|
|
200
|
-
|
|
201
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Generate a single occurrence with timezone handling
|
|
131
|
+
*/
|
|
132
|
+
generateOccurrence(event, date, duration, timezone, state) {
|
|
133
|
+
const start = new Date(date);
|
|
134
|
+
const end = new Date(date.getTime() + duration);
|
|
202
135
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return next;
|
|
209
|
-
|
|
210
|
-
case 'MINUTELY':
|
|
211
|
-
next.setMinutes(next.getMinutes() + rule.interval);
|
|
212
|
-
return next;
|
|
213
|
-
|
|
214
|
-
default:
|
|
215
|
-
// Fallback to daily
|
|
216
|
-
next.setDate(next.getDate() + rule.interval);
|
|
217
|
-
return next;
|
|
218
|
-
}
|
|
136
|
+
// Handle DST transitions
|
|
137
|
+
if (state.dstTransitions.length > 0) {
|
|
138
|
+
const adjusted = this.adjustForDST(start, end, timezone, state.dstTransitions);
|
|
139
|
+
start.setTime(adjusted.start.getTime());
|
|
140
|
+
end.setTime(adjusted.end.getTime());
|
|
219
141
|
}
|
|
220
142
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
143
|
+
return {
|
|
144
|
+
id: `${event.id}_${start.getTime()}`,
|
|
145
|
+
recurringEventId: event.id,
|
|
146
|
+
title: event.title,
|
|
147
|
+
start,
|
|
148
|
+
end,
|
|
149
|
+
startUTC: this.tzManager.toUTC(start, timezone),
|
|
150
|
+
endUTC: this.tzManager.toUTC(end, timezone),
|
|
151
|
+
timezone,
|
|
152
|
+
originalStart: event.start,
|
|
153
|
+
allDay: event.allDay,
|
|
154
|
+
description: event.description,
|
|
155
|
+
location: event.location,
|
|
156
|
+
categories: event.categories,
|
|
157
|
+
status: 'confirmed',
|
|
158
|
+
isRecurring: true,
|
|
159
|
+
isModified: false
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get next occurrence date with complex pattern support
|
|
165
|
+
*/
|
|
166
|
+
getNextDate(currentDate, rule, timezone, state = {}) {
|
|
167
|
+
const next = new Date(currentDate);
|
|
168
|
+
|
|
169
|
+
switch (rule.freq) {
|
|
170
|
+
case 'DAILY':
|
|
171
|
+
return this.getNextDaily(next, rule);
|
|
172
|
+
|
|
173
|
+
case 'WEEKLY':
|
|
174
|
+
return this.getNextWeekly(next, rule, timezone);
|
|
175
|
+
|
|
176
|
+
case 'MONTHLY':
|
|
177
|
+
return this.getNextMonthly(next, rule, timezone);
|
|
178
|
+
|
|
179
|
+
case 'YEARLY':
|
|
180
|
+
return this.getNextYearly(next, rule, timezone);
|
|
181
|
+
|
|
182
|
+
case 'HOURLY':
|
|
183
|
+
next.setHours(next.getHours() + rule.interval);
|
|
241
184
|
return next;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Get next weekly occurrence with BYDAY support
|
|
246
|
-
*/
|
|
247
|
-
getNextWeekly(date, rule, timezone) {
|
|
248
|
-
const next = new Date(date);
|
|
249
|
-
|
|
250
|
-
if (rule.byDay && rule.byDay.length > 0) {
|
|
251
|
-
// Find next matching weekday
|
|
252
|
-
const dayMap = {
|
|
253
|
-
'SU': 0, 'MO': 1, 'TU': 2, 'WE': 3,
|
|
254
|
-
'TH': 4, 'FR': 5, 'SA': 6
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const currentDay = next.getDay();
|
|
258
|
-
let daysToAdd = null;
|
|
259
|
-
|
|
260
|
-
// Find next occurrence day
|
|
261
|
-
for (const byDay of rule.byDay) {
|
|
262
|
-
const targetDay = dayMap[byDay.weekday || byDay];
|
|
263
|
-
if (targetDay > currentDay) {
|
|
264
|
-
daysToAdd = targetDay - currentDay;
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
185
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
daysToAdd = 7 - currentDay + firstDay;
|
|
273
|
-
|
|
274
|
-
// Apply interval for weekly recurrence
|
|
275
|
-
if (rule.interval > 1) {
|
|
276
|
-
daysToAdd += 7 * (rule.interval - 1);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
281
|
-
} else {
|
|
282
|
-
// Simple weekly interval
|
|
283
|
-
next.setDate(next.getDate() + (7 * rule.interval));
|
|
284
|
-
}
|
|
186
|
+
case 'MINUTELY':
|
|
187
|
+
next.setMinutes(next.getMinutes() + rule.interval);
|
|
188
|
+
return next;
|
|
285
189
|
|
|
190
|
+
default:
|
|
191
|
+
// Fallback to daily
|
|
192
|
+
next.setDate(next.getDate() + rule.interval);
|
|
286
193
|
return next;
|
|
287
194
|
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get next daily occurrence
|
|
199
|
+
*/
|
|
200
|
+
getNextDaily(date, rule) {
|
|
201
|
+
const next = new Date(date);
|
|
202
|
+
next.setDate(next.getDate() + rule.interval);
|
|
203
|
+
|
|
204
|
+
// Apply BYHOUR, BYMINUTE, BYSECOND if specified
|
|
205
|
+
if (rule.byHour && rule.byHour.length > 0) {
|
|
206
|
+
const currentHour = next.getHours();
|
|
207
|
+
const nextHour = rule.byHour.find(h => h > currentHour);
|
|
208
|
+
if (nextHour !== undefined) {
|
|
209
|
+
next.setHours(nextHour);
|
|
210
|
+
} else {
|
|
211
|
+
// Move to next day and use first hour
|
|
212
|
+
next.setDate(next.getDate() + 1);
|
|
213
|
+
next.setHours(rule.byHour[0]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
288
216
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
} else if (rule.byDay && rule.byDay.length > 0) {
|
|
322
|
-
// Nth weekday of month (e.g., "2nd Tuesday")
|
|
323
|
-
const byDay = rule.byDay[0];
|
|
324
|
-
const nthOccurrence = byDay.nth || 1;
|
|
325
|
-
|
|
326
|
-
next.setMonth(next.getMonth() + rule.interval);
|
|
327
|
-
this.setToNthWeekdayOfMonth(next, byDay.weekday, nthOccurrence);
|
|
328
|
-
} else if (rule.bySetPos && rule.bySetPos.length > 0) {
|
|
329
|
-
// BYSETPOS for selecting from set
|
|
330
|
-
next.setMonth(next.getMonth() + rule.interval);
|
|
331
|
-
// Complex BYSETPOS logic would go here
|
|
332
|
-
} else {
|
|
333
|
-
// Same day of next month
|
|
334
|
-
const currentDay = next.getDate();
|
|
335
|
-
next.setMonth(next.getMonth() + rule.interval);
|
|
336
|
-
|
|
337
|
-
// Handle month-end edge cases
|
|
338
|
-
const lastDay = new Date(
|
|
339
|
-
next.getFullYear(),
|
|
340
|
-
next.getMonth() + 1,
|
|
341
|
-
0
|
|
342
|
-
).getDate();
|
|
343
|
-
if (currentDay > lastDay) {
|
|
344
|
-
next.setDate(lastDay);
|
|
345
|
-
}
|
|
217
|
+
return next;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get next weekly occurrence with BYDAY support
|
|
222
|
+
*/
|
|
223
|
+
getNextWeekly(date, rule, timezone) {
|
|
224
|
+
const next = new Date(date);
|
|
225
|
+
|
|
226
|
+
if (rule.byDay && rule.byDay.length > 0) {
|
|
227
|
+
// Find next matching weekday
|
|
228
|
+
const dayMap = {
|
|
229
|
+
SU: 0,
|
|
230
|
+
MO: 1,
|
|
231
|
+
TU: 2,
|
|
232
|
+
WE: 3,
|
|
233
|
+
TH: 4,
|
|
234
|
+
FR: 5,
|
|
235
|
+
SA: 6
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const currentDay = next.getDay();
|
|
239
|
+
let daysToAdd = null;
|
|
240
|
+
|
|
241
|
+
// Find next occurrence day
|
|
242
|
+
for (const byDay of rule.byDay) {
|
|
243
|
+
const targetDay = dayMap[byDay.weekday || byDay];
|
|
244
|
+
if (targetDay > currentDay) {
|
|
245
|
+
daysToAdd = targetDay - currentDay;
|
|
246
|
+
break;
|
|
346
247
|
}
|
|
248
|
+
}
|
|
347
249
|
|
|
348
|
-
|
|
349
|
-
|
|
250
|
+
// If no day found in current week, go to next week
|
|
251
|
+
if (daysToAdd === null) {
|
|
252
|
+
const firstDay = dayMap[rule.byDay[0].weekday || rule.byDay[0]];
|
|
253
|
+
daysToAdd = 7 - currentDay + firstDay;
|
|
350
254
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
getNextYearly(date, rule, timezone) {
|
|
355
|
-
const next = new Date(date);
|
|
356
|
-
|
|
357
|
-
if (rule.byMonth && rule.byMonth.length > 0) {
|
|
358
|
-
const currentMonth = next.getMonth();
|
|
359
|
-
const targetMonth = rule.byMonth.find(m => m - 1 > currentMonth);
|
|
360
|
-
|
|
361
|
-
if (targetMonth) {
|
|
362
|
-
// Found month in current year
|
|
363
|
-
next.setMonth(targetMonth - 1);
|
|
364
|
-
} else {
|
|
365
|
-
// Move to next year
|
|
366
|
-
next.setFullYear(next.getFullYear() + rule.interval);
|
|
367
|
-
next.setMonth(rule.byMonth[0] - 1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Apply BYMONTHDAY if specified
|
|
371
|
-
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
372
|
-
next.setDate(rule.byMonthDay[0]);
|
|
373
|
-
}
|
|
374
|
-
} else if (rule.byYearDay && rule.byYearDay.length > 0) {
|
|
375
|
-
// Nth day of year
|
|
376
|
-
next.setFullYear(next.getFullYear() + rule.interval);
|
|
377
|
-
const yearDay = rule.byYearDay[0];
|
|
378
|
-
|
|
379
|
-
if (yearDay > 0) {
|
|
380
|
-
// Count from start of year
|
|
381
|
-
next.setMonth(0, 1);
|
|
382
|
-
next.setDate(yearDay);
|
|
383
|
-
} else {
|
|
384
|
-
// Count from end of year
|
|
385
|
-
next.setMonth(11, 31);
|
|
386
|
-
next.setDate(next.getDate() + yearDay + 1);
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
// Same date next year
|
|
390
|
-
next.setFullYear(next.getFullYear() + rule.interval);
|
|
255
|
+
// Apply interval for weekly recurrence
|
|
256
|
+
if (rule.interval > 1) {
|
|
257
|
+
daysToAdd += 7 * (rule.interval - 1);
|
|
391
258
|
}
|
|
259
|
+
}
|
|
392
260
|
|
|
393
|
-
|
|
261
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
262
|
+
} else {
|
|
263
|
+
// Simple weekly interval
|
|
264
|
+
next.setDate(next.getDate() + 7 * rule.interval);
|
|
394
265
|
}
|
|
395
266
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
267
|
+
return next;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get next monthly occurrence with complex patterns
|
|
272
|
+
*/
|
|
273
|
+
getNextMonthly(date, rule, timezone) {
|
|
274
|
+
const next = new Date(date);
|
|
275
|
+
|
|
276
|
+
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
277
|
+
// Specific day(s) of month
|
|
278
|
+
const targetDays = rule.byMonthDay.sort((a, b) => a - b);
|
|
279
|
+
const currentDay = next.getDate();
|
|
280
|
+
|
|
281
|
+
let targetDay = targetDays.find(d => d > currentDay);
|
|
282
|
+
if (targetDay) {
|
|
283
|
+
// Found a day in current month
|
|
284
|
+
next.setDate(targetDay);
|
|
285
|
+
} else {
|
|
286
|
+
// Move to next month
|
|
287
|
+
next.setMonth(next.getMonth() + rule.interval);
|
|
288
|
+
|
|
289
|
+
// Handle negative days (from end of month)
|
|
290
|
+
targetDay = targetDays[0];
|
|
291
|
+
if (targetDay < 0) {
|
|
292
|
+
const lastDay = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate();
|
|
293
|
+
next.setDate(lastDay + targetDay + 1);
|
|
416
294
|
} else {
|
|
417
|
-
|
|
418
|
-
const lastDay = new Date(
|
|
419
|
-
date.getFullYear(),
|
|
420
|
-
date.getMonth() + 1,
|
|
421
|
-
0
|
|
422
|
-
).getDate();
|
|
423
|
-
|
|
424
|
-
// Find last occurrence
|
|
425
|
-
const temp = new Date(date);
|
|
426
|
-
temp.setDate(lastDay);
|
|
427
|
-
while (temp.getDay() !== targetDay) {
|
|
428
|
-
temp.setDate(temp.getDate() - 1);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Move back nth weeks
|
|
432
|
-
temp.setDate(temp.getDate() + (7 * (nth + 1)));
|
|
433
|
-
date.setTime(temp.getTime());
|
|
295
|
+
next.setDate(targetDay);
|
|
434
296
|
}
|
|
297
|
+
}
|
|
298
|
+
} else if (rule.byDay && rule.byDay.length > 0) {
|
|
299
|
+
// Nth weekday of month (e.g., "2nd Tuesday")
|
|
300
|
+
const byDay = rule.byDay[0];
|
|
301
|
+
const nthOccurrence = byDay.nth || 1;
|
|
302
|
+
|
|
303
|
+
next.setMonth(next.getMonth() + rule.interval);
|
|
304
|
+
this.setToNthWeekdayOfMonth(next, byDay.weekday, nthOccurrence);
|
|
305
|
+
} else if (rule.bySetPos && rule.bySetPos.length > 0) {
|
|
306
|
+
// BYSETPOS for selecting from set
|
|
307
|
+
next.setMonth(next.getMonth() + rule.interval);
|
|
308
|
+
// Complex BYSETPOS logic would go here
|
|
309
|
+
} else {
|
|
310
|
+
// Same day of next month
|
|
311
|
+
const currentDay = next.getDate();
|
|
312
|
+
next.setMonth(next.getMonth() + rule.interval);
|
|
313
|
+
|
|
314
|
+
// Handle month-end edge cases
|
|
315
|
+
const lastDay = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate();
|
|
316
|
+
if (currentDay > lastDay) {
|
|
317
|
+
next.setDate(lastDay);
|
|
318
|
+
}
|
|
435
319
|
}
|
|
436
320
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
321
|
+
return next;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get next yearly occurrence
|
|
326
|
+
*/
|
|
327
|
+
getNextYearly(date, rule, timezone) {
|
|
328
|
+
const next = new Date(date);
|
|
329
|
+
|
|
330
|
+
if (rule.byMonth && rule.byMonth.length > 0) {
|
|
331
|
+
const currentMonth = next.getMonth();
|
|
332
|
+
const targetMonth = rule.byMonth.find(m => m - 1 > currentMonth);
|
|
333
|
+
|
|
334
|
+
if (targetMonth) {
|
|
335
|
+
// Found month in current year
|
|
336
|
+
next.setMonth(targetMonth - 1);
|
|
337
|
+
} else {
|
|
338
|
+
// Move to next year
|
|
339
|
+
next.setFullYear(next.getFullYear() + rule.interval);
|
|
340
|
+
next.setMonth(rule.byMonth[0] - 1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Apply BYMONTHDAY if specified
|
|
344
|
+
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
345
|
+
next.setDate(rule.byMonthDay[0]);
|
|
346
|
+
}
|
|
347
|
+
} else if (rule.byYearDay && rule.byYearDay.length > 0) {
|
|
348
|
+
// Nth day of year
|
|
349
|
+
next.setFullYear(next.getFullYear() + rule.interval);
|
|
350
|
+
const yearDay = rule.byYearDay[0];
|
|
351
|
+
|
|
352
|
+
if (yearDay > 0) {
|
|
353
|
+
// Count from start of year
|
|
354
|
+
next.setMonth(0, 1);
|
|
355
|
+
next.setDate(yearDay);
|
|
356
|
+
} else {
|
|
357
|
+
// Count from end of year
|
|
358
|
+
next.setMonth(11, 31);
|
|
359
|
+
next.setDate(next.getDate() + yearDay + 1);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Same date next year
|
|
363
|
+
next.setFullYear(next.getFullYear() + rule.interval);
|
|
464
364
|
}
|
|
465
365
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
start.setMinutes(start.getMinutes() - offsetDiff);
|
|
490
|
-
end.setMinutes(end.getMinutes() - offsetDiff);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return { start, end };
|
|
366
|
+
return next;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Set date to Nth weekday of month
|
|
371
|
+
*/
|
|
372
|
+
setToNthWeekdayOfMonth(date, weekday, nth) {
|
|
373
|
+
const dayMap = {
|
|
374
|
+
SU: 0,
|
|
375
|
+
MO: 1,
|
|
376
|
+
TU: 2,
|
|
377
|
+
WE: 3,
|
|
378
|
+
TH: 4,
|
|
379
|
+
FR: 5,
|
|
380
|
+
SA: 6
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const targetDay = dayMap[weekday];
|
|
384
|
+
date.setDate(1); // Start at first of month
|
|
385
|
+
|
|
386
|
+
// Find first occurrence
|
|
387
|
+
while (date.getDay() !== targetDay) {
|
|
388
|
+
date.setDate(date.getDate() + 1);
|
|
496
389
|
}
|
|
497
390
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
391
|
+
if (nth > 0) {
|
|
392
|
+
// Nth occurrence from start
|
|
393
|
+
date.setDate(date.getDate() + 7 * (nth - 1));
|
|
394
|
+
} else {
|
|
395
|
+
// Nth occurrence from end
|
|
396
|
+
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
|
|
397
|
+
|
|
398
|
+
// Find last occurrence
|
|
399
|
+
const temp = new Date(date);
|
|
400
|
+
temp.setDate(lastDay);
|
|
401
|
+
while (temp.getDay() !== targetDay) {
|
|
402
|
+
temp.setDate(temp.getDate() - 1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Move back nth weeks
|
|
406
|
+
temp.setDate(temp.getDate() + 7 * (nth + 1));
|
|
407
|
+
date.setTime(temp.getTime());
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Find DST transitions in date range
|
|
413
|
+
*/
|
|
414
|
+
findDSTTransitions(start, end, timezone) {
|
|
415
|
+
const transitions = [];
|
|
416
|
+
const current = new Date(start);
|
|
417
|
+
|
|
418
|
+
// Check each day for offset changes
|
|
419
|
+
let lastOffset = this.tzManager.getTimezoneOffset(current, timezone);
|
|
420
|
+
|
|
421
|
+
while (current <= end) {
|
|
422
|
+
const offset = this.tzManager.getTimezoneOffset(current, timezone);
|
|
423
|
+
|
|
424
|
+
if (offset !== lastOffset) {
|
|
425
|
+
transitions.push({
|
|
426
|
+
date: new Date(current),
|
|
427
|
+
oldOffset: lastOffset,
|
|
428
|
+
newOffset: offset,
|
|
429
|
+
type: offset < lastOffset ? 'spring-forward' : 'fall-back'
|
|
510
430
|
});
|
|
431
|
+
}
|
|
511
432
|
|
|
512
|
-
|
|
513
|
-
|
|
433
|
+
lastOffset = offset;
|
|
434
|
+
current.setDate(current.getDate() + 1);
|
|
514
435
|
}
|
|
515
436
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
437
|
+
return transitions;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Adjust occurrence for DST transitions
|
|
442
|
+
*/
|
|
443
|
+
adjustForDST(start, end, timezone, transitions) {
|
|
444
|
+
for (const transition of transitions) {
|
|
445
|
+
if (start >= transition.date) {
|
|
446
|
+
const offsetDiff = transition.oldOffset - transition.newOffset;
|
|
447
|
+
|
|
448
|
+
// Spring forward: skip the "lost" hour
|
|
449
|
+
if (transition.type === 'spring-forward') {
|
|
450
|
+
const lostHourStart = new Date(transition.date);
|
|
451
|
+
lostHourStart.setHours(2); // Typical transition time
|
|
452
|
+
const lostHourEnd = new Date(lostHourStart);
|
|
453
|
+
lostHourEnd.setHours(3);
|
|
454
|
+
|
|
455
|
+
if (start >= lostHourStart && start < lostHourEnd) {
|
|
456
|
+
start.setHours(start.getHours() + 1);
|
|
457
|
+
end.setHours(end.getHours() + 1);
|
|
458
|
+
}
|
|
522
459
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Add exception with reason
|
|
530
|
-
*/
|
|
531
|
-
addException(eventId, date, reason = 'Cancelled') {
|
|
532
|
-
if (!this.exceptionStore.has(eventId)) {
|
|
533
|
-
this.exceptionStore.set(eventId, new Map());
|
|
460
|
+
// Fall back: handle the "repeated" hour
|
|
461
|
+
else if (transition.type === 'fall-back') {
|
|
462
|
+
// Maintain wall clock time
|
|
463
|
+
start.setMinutes(start.getMinutes() - offsetDiff);
|
|
464
|
+
end.setMinutes(end.getMinutes() - offsetDiff);
|
|
534
465
|
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
535
468
|
|
|
536
|
-
|
|
537
|
-
|
|
469
|
+
return { start, end };
|
|
470
|
+
}
|
|
538
471
|
|
|
539
|
-
|
|
540
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Add or update a modified instance
|
|
474
|
+
*/
|
|
475
|
+
addModifiedInstance(eventId, occurrenceDate, modifications) {
|
|
476
|
+
if (!this.modifiedInstances.has(eventId)) {
|
|
477
|
+
this.modifiedInstances.set(eventId, new Map());
|
|
541
478
|
}
|
|
542
479
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
480
|
+
const dateKey = this.getDateKey(occurrenceDate);
|
|
481
|
+
this.modifiedInstances.get(eventId).set(dateKey, {
|
|
482
|
+
...modifications,
|
|
483
|
+
modifiedAt: new Date()
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Clear cache for this event
|
|
487
|
+
this.clearEventCache(eventId);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get modified instance data
|
|
492
|
+
*/
|
|
493
|
+
getModifiedInstance(eventId, occurrenceDate) {
|
|
494
|
+
if (!this.modifiedInstances.has(eventId)) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
555
497
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const exDate = ex instanceof Date ? ex : new Date(ex.date || ex);
|
|
560
|
-
return this.getDateKey(exDate) === dateKey;
|
|
561
|
-
});
|
|
562
|
-
}
|
|
498
|
+
const dateKey = this.getDateKey(occurrenceDate);
|
|
499
|
+
return this.modifiedInstances.get(eventId).get(dateKey);
|
|
500
|
+
}
|
|
563
501
|
|
|
564
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Add exception with reason
|
|
504
|
+
*/
|
|
505
|
+
addException(eventId, date, reason = 'Cancelled') {
|
|
506
|
+
if (!this.exceptionStore.has(eventId)) {
|
|
507
|
+
this.exceptionStore.set(eventId, new Map());
|
|
565
508
|
}
|
|
566
509
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
*/
|
|
570
|
-
getExceptionReason(eventId, date) {
|
|
571
|
-
if (!this.exceptionStore.has(eventId)) {
|
|
572
|
-
return 'Cancelled';
|
|
573
|
-
}
|
|
510
|
+
const dateKey = this.getDateKey(date);
|
|
511
|
+
this.exceptionStore.get(eventId).set(dateKey, reason);
|
|
574
512
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
513
|
+
// Clear cache
|
|
514
|
+
this.clearEventCache(eventId);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Check if date is an exception
|
|
519
|
+
*/
|
|
520
|
+
isException(eventId, date, rule) {
|
|
521
|
+
const dateKey = this.getDateKey(date);
|
|
578
522
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
523
|
+
// Check enhanced exceptions
|
|
524
|
+
if (this.exceptionStore.has(eventId)) {
|
|
525
|
+
if (this.exceptionStore.get(eventId).has(dateKey)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
585
528
|
}
|
|
586
529
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return
|
|
530
|
+
// Check rule exceptions
|
|
531
|
+
if (rule && rule.exceptions) {
|
|
532
|
+
return rule.exceptions.some(ex => {
|
|
533
|
+
const exDate = ex instanceof Date ? ex : new Date(ex.date || ex);
|
|
534
|
+
return this.getDateKey(exDate) === dateKey;
|
|
535
|
+
});
|
|
592
536
|
}
|
|
593
537
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
*/
|
|
597
|
-
cacheOccurrences(key, occurrences) {
|
|
598
|
-
this.occurrenceCache.set(key, occurrences);
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
599
540
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
541
|
+
/**
|
|
542
|
+
* Get exception reason
|
|
543
|
+
*/
|
|
544
|
+
getExceptionReason(eventId, date) {
|
|
545
|
+
if (!this.exceptionStore.has(eventId)) {
|
|
546
|
+
return 'Cancelled';
|
|
605
547
|
}
|
|
606
548
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
549
|
+
const dateKey = this.getDateKey(date);
|
|
550
|
+
return this.exceptionStore.get(eventId).get(dateKey) || 'Cancelled';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Create date key for indexing
|
|
555
|
+
*/
|
|
556
|
+
getDateKey(date) {
|
|
557
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
558
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Create cache key
|
|
563
|
+
*/
|
|
564
|
+
getCacheKey(eventId, start, end, options) {
|
|
565
|
+
return `${eventId}_${start.getTime()}_${end.getTime()}_${JSON.stringify(options)}`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Cache occurrences
|
|
570
|
+
*/
|
|
571
|
+
cacheOccurrences(key, occurrences) {
|
|
572
|
+
this.occurrenceCache.set(key, occurrences);
|
|
573
|
+
|
|
574
|
+
// LRU eviction
|
|
575
|
+
if (this.occurrenceCache.size > this.cacheSize) {
|
|
576
|
+
const firstKey = this.occurrenceCache.keys().next().value;
|
|
577
|
+
this.occurrenceCache.delete(firstKey);
|
|
616
578
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
allDay: event.allDay,
|
|
628
|
-
description: event.description,
|
|
629
|
-
location: event.location,
|
|
630
|
-
categories: event.categories,
|
|
631
|
-
timezone: event.timeZone,
|
|
632
|
-
isRecurring: false
|
|
633
|
-
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Clear cache for specific event
|
|
583
|
+
*/
|
|
584
|
+
clearEventCache(eventId) {
|
|
585
|
+
for (const key of this.occurrenceCache.keys()) {
|
|
586
|
+
if (key.startsWith(`${eventId}_`)) {
|
|
587
|
+
this.occurrenceCache.delete(key);
|
|
588
|
+
}
|
|
634
589
|
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Create occurrence object
|
|
594
|
+
*/
|
|
595
|
+
createOccurrence(event, start, end) {
|
|
596
|
+
return {
|
|
597
|
+
id: event.id,
|
|
598
|
+
title: event.title,
|
|
599
|
+
start,
|
|
600
|
+
end,
|
|
601
|
+
allDay: event.allDay,
|
|
602
|
+
description: event.description,
|
|
603
|
+
location: event.location,
|
|
604
|
+
categories: event.categories,
|
|
605
|
+
timezone: event.timeZone,
|
|
606
|
+
isRecurring: false
|
|
607
|
+
};
|
|
608
|
+
}
|
|
635
609
|
}
|
|
636
610
|
|
|
637
|
-
export default RecurrenceEngineV2;
|
|
611
|
+
export default RecurrenceEngineV2;
|