Skip to content

Commit e37df6d

Browse files
fix(calendar): correct day-of-week for full-day recurring events across DST
Full-day recurring events were showing on wrong day after DST transitions. rrule.js returns UTC dates that shift calendar days when offset changes. Fix: Extract UTC date components and interpret as local calendar dates, preserving the intended day regardless of DST offset changes. Added test for weekly Monday event crossing DST boundary. Fixes: #4003 Regression from: c2ec6fc
1 parent 241921b commit e37df6d

File tree

2 files changed

+48
-2
lines changed

2 files changed

+48
-2
lines changed

modules/default/calendar/calendarfetcherutils.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ const CalendarFetcherUtils = {
8282
// rrule.js returns UTC dates with tzid cleared, so we interpret them in the event's original timezone
8383
return dates.map((date) => {
8484
if (isFullDayEvent) {
85-
// For all-day events, anchor to calendar day in event's timezone
86-
return moment.tz(date, eventTimezone).startOf("day");
85+
// For all-day events, extract UTC date components and interpret as local date
86+
// This prevents DST transitions from shifting the date to the previous/next day
87+
const utcYear = date.getUTCFullYear();
88+
const utcMonth = date.getUTCMonth();
89+
const utcDate = date.getUTCDate();
90+
return moment.tz([utcYear, utcMonth, utcDate], eventTimezone);
8791
}
8892
// For timed events, preserve the time in the event's original timezone
8993
return moment.tz(date, "UTC").tz(eventTimezone, true);

tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,47 @@ END:VEVENT`);
111111
expect(januaryFirst[0].toISOString(true)).toContain("09:00:00.000+01:00");
112112
expect(julyFirst[0].toISOString(true)).toContain("09:00:00.000+02:00");
113113
});
114+
115+
it("should return correct day-of-week for full-day recurring events across DST transitions", () => {
116+
// Test case for GitHub issue #3976: recurring full-day events showing on wrong day
117+
// This happens when DST transitions change the UTC offset between occurrences
118+
const data = ical.parseICS(`BEGIN:VCALENDAR
119+
BEGIN:VEVENT
120+
DTSTART;VALUE=DATE:20251027
121+
DTEND;VALUE=DATE:20251028
122+
RRULE:FREQ=WEEKLY;WKST=SU;COUNT=3
123+
DTSTAMP:20260103T123138Z
124+
125+
SUMMARY:Weekly Monday Event
126+
END:VEVENT
127+
END:VCALENDAR`);
128+
129+
const event = data["[email protected]"];
130+
131+
// Simulate calendar with timezone (e.g., from X-WR-TIMEZONE or user config)
132+
// This is how MagicMirror handles full-day events from calendars with timezones
133+
event.start.tz = "America/Chicago";
134+
135+
const pastMoment = moment("2025-10-01");
136+
const futureMoment = moment("2025-11-30");
137+
138+
// Get moments for the recurring event
139+
const moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastMoment, futureMoment, 0);
140+
141+
// All occurrences should be on Monday (day() === 1) at midnight
142+
// Oct 27, 2025 - Before DST ends
143+
// Nov 3, 2025 - After DST ends (this was showing as Sunday before the fix)
144+
// Nov 10, 2025 - After DST ends
145+
expect(moments).toHaveLength(3);
146+
expect(moments[0].day()).toBe(1); // Monday
147+
expect(moments[0].format("YYYY-MM-DD")).toBe("2025-10-27");
148+
expect(moments[0].hour()).toBe(0); // Midnight
149+
expect(moments[1].day()).toBe(1); // Monday (not Sunday!)
150+
expect(moments[1].format("YYYY-MM-DD")).toBe("2025-11-03");
151+
expect(moments[1].hour()).toBe(0); // Midnight
152+
expect(moments[2].day()).toBe(1); // Monday
153+
expect(moments[2].format("YYYY-MM-DD")).toBe("2025-11-10");
154+
expect(moments[2].hour()).toBe(0); // Midnight
155+
});
114156
});
115157
});

0 commit comments

Comments
 (0)