From 9c3b5b231e01322173edcf53a6175540433c1d5e Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:17:29 +0200 Subject: [PATCH] fix(calendar): correct multi-day slice day counts --- defaultmodules/calendar/calendar.js | 71 ++++++++++--------- .../sliceMultiDayEventsEndsMidnight.js | 33 +++++++++ tests/electron/modules/calendar_spec.js | 14 ++++ ...ndar_test_slice_multiday_ends_midnight.ics | 16 +++++ 4 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js create mode 100644 tests/mocks/calendar_test_slice_multiday_ends_midnight.ics diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 8930a96531..62eba7553b 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -465,6 +465,20 @@ Module.register("calendar", { return moment(timestamp, "x").tz(moment.tz.guess()); }, + /** + * Sets the relative day flags (today, yesterday, ...) on an event based on its day. + * @param {object} event The event to flag. + * @param {moment.Moment} dayMoment The day the event belongs to. + * @param {moment.Moment} now The current moment. + */ + setRelativeDayFlags (event, dayMoment, now) { + event.today = dayMoment.isSame(now, "d"); + event.dayBeforeYesterday = dayMoment.isSame(now.clone().subtract(2, "days"), "d"); + event.yesterday = dayMoment.isSame(now.clone().subtract(1, "days"), "d"); + event.tomorrow = dayMoment.isSame(now.clone().add(1, "days"), "d"); + event.dayAfterTomorrow = dayMoment.isSame(now.clone().add(2, "days"), "d"); + }, + /** * Creates the sorted list of all events. * @param {boolean} limitNumberOfEntries Whether to filter returned events for display. @@ -503,43 +517,36 @@ Module.register("calendar", { } event.url = calendarUrl; - event.today = eventStartDateMoment.isSame(now, "d"); - event.dayBeforeYesterday = eventStartDateMoment.isSame(now.clone().subtract(2, "days"), "d"); - event.yesterday = eventStartDateMoment.isSame(now.clone().subtract(1, "days"), "d"); - event.tomorrow = eventStartDateMoment.isSame(now.clone().add(1, "days"), "d"); - event.dayAfterTomorrow = eventStartDateMoment.isSame(now.clone().add(2, "days"), "d"); + this.setRelativeDayFlags(event, eventStartDateMoment, now); /* - * if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days, - * otherwise, esp. in dateheaders mode it is not clear how long these events are. + * If sliceMultiDayEvents is enabled, an event spanning several calendar days is split into one entry per day. + * Otherwise, esp. in dateheaders mode, it is not clear how long these events are. + * dayCount is the number of calendar days the event touches (an end exactly at midnight does not add a day). */ - const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days"); - if (this.config.sliceMultiDayEvents && maxCount > 1) { + const eventStartDay = eventStartDateMoment.clone().startOf("day"); + const eventEndDay = eventEndDateMoment.clone().startOf("day"); + const endsAtMidnight = !eventEndDateMoment.isAfter(eventEndDay); + const dayCount = eventEndDay.diff(eventStartDay, "days") + (endsAtMidnight ? 0 : 1); + if (this.config.sliceMultiDayEvents && dayCount > 1) { const splitEvents = []; - let midnight - = eventStartDateMoment - .clone() - .startOf("day") - .add(1, "day") - .endOf("day"); - let count = 1; - while (eventEndDateMoment.isAfter(midnight)) { - const thisEvent = JSON.parse(JSON.stringify(event)); // clone object - thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d"); - thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d"); - thisEvent.endDate = midnight.clone().subtract(1, "day").format("x"); - thisEvent.title += ` (${count}/${maxCount})`; - splitEvents.push(thisEvent); - - event.startDate = midnight.clone().startOf("day").format("x"); // start next slice at 00:00, not 23:59 - count += 1; - midnight = midnight.clone().add(1, "day").endOf("day"); // next day + // Each slice covers one day: it starts at the event start (first slice) or midnight, + // and ends at the event end (last slice) or one millisecond before the next midnight. + let sliceStart = eventStartDateMoment.clone(); + + for (let dayNumber = 1; dayNumber <= dayCount; dayNumber++) { + const isLastSlice = dayNumber === dayCount; + const nextMidnight = sliceStart.clone().startOf("day").add(1, "day"); + + const slice = JSON.parse(JSON.stringify(event)); // clone object + slice.startDate = sliceStart.format("x"); + slice.endDate = isLastSlice ? event.endDate : nextMidnight.clone().subtract(1, "millisecond").format("x"); + slice.title = `${event.title} (${dayNumber}/${dayCount})`; + this.setRelativeDayFlags(slice, sliceStart, now); + splitEvents.push(slice); + + sliceStart = nextMidnight; } - // Last day - event.title += ` (${count}/${maxCount})`; - event.today += this.timestampToMoment(event.startDate).isSame(now, "d"); - event.tomorrow = this.timestampToMoment(event.startDate).isSame(now.clone().add(1, "days"), "d"); - splitEvents.push(event); for (const splitEvent of splitEvents) { if (this.timestampToMoment(splitEvent.endDate).isAfter(now) && this.timestampToMoment(splitEvent.endDate).isSameOrBefore(future)) { diff --git a/tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js b/tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js new file mode 100644 index 0000000000..29171faa89 --- /dev/null +++ b/tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js @@ -0,0 +1,33 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 24, + modules: [ + { + module: "calendar", + position: "bottom_bar", + config: { + fade: false, + urgency: 0, + dateFormat: "Do.MMM, HH:mm", + fullDayEventDateFormat: "Do.MMM", + timeFormat: "absolute", + getRelative: 0, + maximumEntries: 100, + showEnd: true, + sliceMultiDayEvents: true, + calendars: [ + { + maximumEntries: 100, + url: "http://localhost:8080/tests/mocks/calendar_test_slice_multiday_ends_midnight.ics" + } + ] + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/electron/modules/calendar_spec.js b/tests/electron/modules/calendar_spec.js index dbee802006..3fb1392748 100644 --- a/tests/electron/modules/calendar_spec.js +++ b/tests/electron/modules/calendar_spec.js @@ -179,6 +179,20 @@ describe("Calendar module", () => { await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", [], "Europe/Berlin"); await expect(doTestCount()).resolves.toBe(6); }); + + it("counts all touched dates across DST, not just elapsed 24h blocks", async () => { + // Event runs from 2024-10-25 to 2024-10-28 in Europe/Berlin, crossing the DST change. + // It touches 4 calendar dates: Fri, Sat, Sun, Mon. + await startCalendarShowEndScenario("slice_multiday_timed_start_midnight", "25 Oct 2024 06:00:00 GMT", "Europe/Berlin"); + await expect(doTestCount()).resolves.toBe(4); + }); + + it("does not create an extra slice when an event ends exactly at 00:00", async () => { + // Event runs Fri 12:00 -> Mon 00:00. It should cover Fri, Sat, Sun only; Monday is not touched. + await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEventsEndsMidnight.js", "25 Oct 2024 06:00:00 GMT", [], "GMT"); + await expect(doTestCount()).resolves.toBe(3); + await expect(doTestTableContent(".calendar .event", ".title", "(3/3)", last)).resolves.toBe(true); + }); }); describe("sliceMultiDayEvents slice start time", () => { diff --git a/tests/mocks/calendar_test_slice_multiday_ends_midnight.ics b/tests/mocks/calendar_test_slice_multiday_ends_midnight.ics new file mode 100644 index 0000000000..20741765f6 --- /dev/null +++ b/tests/mocks/calendar_test_slice_multiday_ends_midnight.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//MagicMirror//slice regression//EN +CALSCALE:GREGORIAN +METHOD:PUBLISH +BEGIN:VEVENT +DTSTART:20241025T120000Z +DTEND:20241028T000000Z +DTSTAMP:20241024T153358Z +UID:slice-midnight-end-regression@magicmirror.test +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Slice +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR