@forcecalendar/core 2.1.12 → 2.1.14
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/events/Event.js +9 -1
- package/core/events/EventStore.js +38 -30
- package/core/events/RecurrenceEngine.js +12 -0
- package/package.json +1 -1
package/core/events/Event.js
CHANGED
|
@@ -154,12 +154,20 @@ export class Event {
|
|
|
154
154
|
// Validate timezone if provided
|
|
155
155
|
if (data.timeZone) {
|
|
156
156
|
try {
|
|
157
|
-
// Test if timezone is valid by trying to use it
|
|
158
157
|
new Intl.DateTimeFormat('en-US', { timeZone: data.timeZone });
|
|
159
158
|
} catch (e) {
|
|
160
159
|
throw new Error(`Invalid timezone: ${data.timeZone}`);
|
|
161
160
|
}
|
|
162
161
|
}
|
|
162
|
+
|
|
163
|
+
// Validate end timezone if provided
|
|
164
|
+
if (data.endTimeZone) {
|
|
165
|
+
try {
|
|
166
|
+
new Intl.DateTimeFormat('en-US', { timeZone: data.endTimeZone });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
throw new Error(`Invalid end timezone: ${data.endTimeZone}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
/**
|
|
@@ -415,33 +415,32 @@ export class EventStore {
|
|
|
415
415
|
events = events.filter(e => !e.allDay);
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
|
|
419
|
-
const processed = new Set();
|
|
420
|
-
|
|
421
|
-
events.forEach(event => {
|
|
422
|
-
if (processed.has(event.id)) return;
|
|
423
|
-
|
|
424
|
-
// Start a new group with this event
|
|
425
|
-
const group = [event];
|
|
426
|
-
processed.add(event.id);
|
|
418
|
+
if (events.length === 0) return [];
|
|
427
419
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const currentEvent = group[i];
|
|
432
|
-
|
|
433
|
-
events.forEach(otherEvent => {
|
|
434
|
-
if (!processed.has(otherEvent.id) && currentEvent.overlaps(otherEvent)) {
|
|
435
|
-
group.push(otherEvent);
|
|
436
|
-
processed.add(otherEvent.id);
|
|
437
|
-
}
|
|
438
|
-
});
|
|
420
|
+
// Sweep-line approach: sort by start, then merge overlapping intervals
|
|
421
|
+
// O(n log n) instead of O(n²)
|
|
422
|
+
events.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
439
423
|
|
|
440
|
-
|
|
424
|
+
const groups = [];
|
|
425
|
+
let currentGroup = [events[0]];
|
|
426
|
+
let groupEnd = events[0].end;
|
|
427
|
+
|
|
428
|
+
for (let i = 1; i < events.length; i++) {
|
|
429
|
+
const event = events[i];
|
|
430
|
+
if (event.start < groupEnd) {
|
|
431
|
+
// Overlaps with current group
|
|
432
|
+
currentGroup.push(event);
|
|
433
|
+
if (event.end > groupEnd) {
|
|
434
|
+
groupEnd = event.end;
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
// No overlap — start new group
|
|
438
|
+
groups.push(currentGroup);
|
|
439
|
+
currentGroup = [event];
|
|
440
|
+
groupEnd = event.end;
|
|
441
441
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
});
|
|
442
|
+
}
|
|
443
|
+
groups.push(currentGroup);
|
|
445
444
|
|
|
446
445
|
return groups;
|
|
447
446
|
}
|
|
@@ -504,15 +503,24 @@ export class EventStore {
|
|
|
504
503
|
* Get events for a date range
|
|
505
504
|
* @param {Date} start - Start date
|
|
506
505
|
* @param {Date} end - End date
|
|
507
|
-
* @param {boolean|
|
|
508
|
-
*
|
|
506
|
+
* @param {boolean|Object} [expandRecurringOrOptions=true] - Boolean to expand recurring events,
|
|
507
|
+
* or options object: { expandRecurring?: boolean, timezone?: string }
|
|
508
|
+
* @param {string} [timezone] - Timezone for the query
|
|
509
509
|
* @returns {Event[]}
|
|
510
510
|
*/
|
|
511
|
-
getEventsInRange(start, end,
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
511
|
+
getEventsInRange(start, end, expandRecurringOrOptions = true, timezone = null) {
|
|
512
|
+
let expandRecurring = true;
|
|
513
|
+
|
|
514
|
+
if (typeof expandRecurringOrOptions === 'object' && expandRecurringOrOptions !== null) {
|
|
515
|
+
// Options object form: getEventsInRange(start, end, { expandRecurring, timezone })
|
|
516
|
+
expandRecurring = expandRecurringOrOptions.expandRecurring !== false;
|
|
517
|
+
timezone = expandRecurringOrOptions.timezone || timezone;
|
|
518
|
+
} else if (typeof expandRecurringOrOptions === 'string') {
|
|
519
|
+
// Legacy overloaded form: string was treated as timezone (deprecated)
|
|
520
|
+
timezone = expandRecurringOrOptions;
|
|
515
521
|
expandRecurring = true;
|
|
522
|
+
} else {
|
|
523
|
+
expandRecurring = expandRecurringOrOptions;
|
|
516
524
|
}
|
|
517
525
|
|
|
518
526
|
timezone = timezone || this.defaultTimezone;
|
|
@@ -165,6 +165,18 @@ export class RecurrenceEngine {
|
|
|
165
165
|
const next = new Date(currentDate);
|
|
166
166
|
|
|
167
167
|
switch (rule.freq) {
|
|
168
|
+
case 'SECONDLY':
|
|
169
|
+
next.setSeconds(next.getSeconds() + rule.interval);
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case 'MINUTELY':
|
|
173
|
+
next.setMinutes(next.getMinutes() + rule.interval);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'HOURLY':
|
|
177
|
+
next.setHours(next.getHours() + rule.interval);
|
|
178
|
+
break;
|
|
179
|
+
|
|
168
180
|
case 'DAILY':
|
|
169
181
|
next.setDate(next.getDate() + rule.interval);
|
|
170
182
|
break;
|