@forcecalendar/core 0.4.0 → 1.0.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.
|
@@ -294,34 +294,47 @@ export class EventStore {
|
|
|
294
294
|
// Use local date string for the query date (in the calendar's timezone)
|
|
295
295
|
const dateStr = DateUtils.getLocalDateString(date);
|
|
296
296
|
|
|
297
|
-
//
|
|
298
|
-
const
|
|
297
|
+
// Collect candidate event IDs from indices
|
|
298
|
+
const candidateIds = new Set();
|
|
299
299
|
|
|
300
|
-
//
|
|
301
|
-
// we need to check events from surrounding dates too
|
|
300
|
+
// Check byDate index for nearby dates (handles most events)
|
|
302
301
|
const checkDate = new Date(date);
|
|
303
302
|
for (let offset = -1; offset <= 1; offset++) {
|
|
304
303
|
const tempDate = new Date(checkDate);
|
|
305
304
|
tempDate.setDate(tempDate.getDate() + offset);
|
|
306
305
|
const tempDateStr = DateUtils.getLocalDateString(tempDate);
|
|
307
|
-
const eventIds = this.indices.byDate.get(tempDateStr)
|
|
306
|
+
const eventIds = this.indices.byDate.get(tempDateStr);
|
|
307
|
+
if (eventIds) {
|
|
308
|
+
eventIds.forEach(id => candidateIds.add(id));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
308
311
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
312
|
+
// Also check byMonth index to catch long-running events that might not be
|
|
313
|
+
// indexed in byDate for this specific date (lazy indexing only indexes
|
|
314
|
+
// first/last week of multi-week events)
|
|
315
|
+
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
316
|
+
const monthEventIds = this.indices.byMonth.get(monthKey);
|
|
317
|
+
if (monthEventIds) {
|
|
318
|
+
monthEventIds.forEach(id => candidateIds.add(id));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Filter candidates to events that actually overlap with the requested date
|
|
322
|
+
const allEvents = [];
|
|
323
|
+
const startOfDay = new Date(date);
|
|
324
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
325
|
+
const endOfDay = new Date(date);
|
|
326
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
327
|
+
|
|
328
|
+
for (const id of candidateIds) {
|
|
329
|
+
const event = this.events.get(id);
|
|
330
|
+
if (event) {
|
|
331
|
+
// Check if event actually occurs on the requested date in the given timezone
|
|
332
|
+
const eventStartLocal = event.getStartInTimezone(timezone);
|
|
333
|
+
const eventEndLocal = event.getEndInTimezone(timezone);
|
|
334
|
+
|
|
335
|
+
// Event overlaps with this day if it starts before end of day and ends after start of day
|
|
336
|
+
if (eventStartLocal <= endOfDay && eventEndLocal >= startOfDay) {
|
|
337
|
+
allEvents.push(event);
|
|
325
338
|
}
|
|
326
339
|
}
|
|
327
340
|
}
|
|
@@ -122,6 +122,7 @@ export class RRuleParser {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Parse BYDAY value
|
|
125
|
+
* Returns array of strings like ['MO', '2TU', '-1FR'] for compatibility with RecurrenceEngine
|
|
125
126
|
* @private
|
|
126
127
|
*/
|
|
127
128
|
static parseByDay(value) {
|
|
@@ -130,14 +131,14 @@ export class RRuleParser {
|
|
|
130
131
|
const result = [];
|
|
131
132
|
|
|
132
133
|
for (const day of days) {
|
|
133
|
-
const
|
|
134
|
+
const trimmed = day.trim().toUpperCase();
|
|
135
|
+
const match = trimmed.match(/^([+-]?\d*)([A-Z]{2})$/);
|
|
134
136
|
if (match) {
|
|
135
137
|
const [_, nth, weekday] = match;
|
|
136
138
|
if (weekDays.includes(weekday)) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
139
|
+
// Return string format for RecurrenceEngine compatibility
|
|
140
|
+
// e.g., 'MO', '2MO', '-1FR'
|
|
141
|
+
result.push(nth ? `${nth}${weekday}` : weekday);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
}
|
|
@@ -108,9 +108,18 @@ export class RecurrenceEngine {
|
|
|
108
108
|
case 'WEEKLY':
|
|
109
109
|
if (rule.byDay && rule.byDay.length > 0) {
|
|
110
110
|
// Find next day that matches byDay
|
|
111
|
+
// Limit iterations to prevent infinite loop with malformed byDay
|
|
112
|
+
const maxIterations = 8; // 7 days + 1 for safety
|
|
113
|
+
let iterations = 0;
|
|
111
114
|
next.setDate(next.getDate() + 1);
|
|
112
|
-
while (!this.matchesByDay(next, rule.byDay)) {
|
|
115
|
+
while (!this.matchesByDay(next, rule.byDay) && iterations < maxIterations) {
|
|
113
116
|
next.setDate(next.getDate() + 1);
|
|
117
|
+
iterations++;
|
|
118
|
+
}
|
|
119
|
+
// If no match found, fall back to simple weekly interval
|
|
120
|
+
if (iterations >= maxIterations) {
|
|
121
|
+
console.warn('RecurrenceEngine: Invalid byDay rule, falling back to weekly interval');
|
|
122
|
+
next.setDate(next.getDate() + (7 * rule.interval) - maxIterations);
|
|
114
123
|
}
|
|
115
124
|
} else {
|
|
116
125
|
// Simple weekly recurrence
|
|
@@ -51,8 +51,10 @@ export class TimezoneManager {
|
|
|
51
51
|
if (!date) return null;
|
|
52
52
|
if (timezone === 'UTC') return new Date(date);
|
|
53
53
|
|
|
54
|
+
// offset is positive for timezones behind UTC (e.g., NYC = +300)
|
|
55
|
+
// To convert local to UTC, we ADD the offset
|
|
54
56
|
const offset = this.getTimezoneOffset(date, timezone);
|
|
55
|
-
return new Date(date.getTime()
|
|
57
|
+
return new Date(date.getTime() + (offset * 60 * 1000));
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/**
|
|
@@ -65,8 +67,10 @@ export class TimezoneManager {
|
|
|
65
67
|
if (!utcDate) return null;
|
|
66
68
|
if (timezone === 'UTC') return new Date(utcDate);
|
|
67
69
|
|
|
70
|
+
// offset is positive for timezones behind UTC (e.g., NYC = +300)
|
|
71
|
+
// To convert UTC to local, we SUBTRACT the offset
|
|
68
72
|
const offset = this.getTimezoneOffset(utcDate, timezone);
|
|
69
|
-
return new Date(utcDate.getTime()
|
|
73
|
+
return new Date(utcDate.getTime() - (offset * 60 * 1000));
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
/**
|