From 08ded9fc1114c86b7a748c589aa2b6e8d8202c1c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 13 Jul 2026 18:17:06 +0000 Subject: docs: fix exclusive all-day end-date bug in multi-day plan's Android task MD-Task 2's review caught that Google Calendar's all-day End.Date is exclusive (a single-day all-day event's End is one day past its own day), which isMultiDayEvent/multiDayVariant didn't account for. Task 4 (Android, not yet implemented) had the same flaw copied into its Kotlin code -- fixed here before dispatching, mirroring the Go-side fix. --- .../plans/2026-07-13-multi-day-events.md | 46 +++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/superpowers/plans/2026-07-13-multi-day-events.md b/docs/superpowers/plans/2026-07-13-multi-day-events.md index 32f9d2e..4b8bce8 100644 --- a/docs/superpowers/plans/2026-07-13-multi-day-events.md +++ b/docs/superpowers/plans/2026-07-13-multi-day-events.md @@ -970,6 +970,26 @@ class DootWidgetMultiDayTest { val item = event("2026-07-13T17:35:00Z", "2026-07-30T22:30:00Z") assertEquals(MultiDayVariant.NONE, multiDayVariant(item, LocalDate.of(2026, 8, 1), zone)) } + + @Test + fun `isMultiDayEvent is false for a single-day all-day event (exclusive end-date)`() { + // Google Calendar's all-day End is EXCLUSIVE: a single-day all-day event on + // July 13 has start=Jul13 00:00, end=Jul14 00:00 (one day past its own day). + assertFalse(isMultiDayEvent(event("2026-07-13T00:00:00Z", "2026-07-14T00:00:00Z", isAllDay = true), zone)) + } + + @Test + fun `isMultiDayEvent is true for a 3-day all-day event`() { + // 3-day all-day event (Jul 13-15 inclusive) has end=Jul16 00:00 (exclusive). + assertTrue(isMultiDayEvent(event("2026-07-13T00:00:00Z", "2026-07-16T00:00:00Z", isAllDay = true), zone)) + } + + @Test + fun `multiDayVariant reports ENDS on the real last day of a multi-day all-day event, not the exclusive end date`() { + val item = event("2026-07-13T00:00:00Z", "2026-07-16T00:00:00Z", isAllDay = true) + assertEquals(MultiDayVariant.ENDS, multiDayVariant(item, LocalDate.of(2026, 7, 15), zone)) + assertEquals(MultiDayVariant.NONE, multiDayVariant(item, LocalDate.of(2026, 7, 16), zone)) + } } ``` @@ -997,12 +1017,28 @@ Then add, right after the `calendarViewIntent` helper near the top of the file: ```kotlin enum class MultiDayVariant { NONE, STARTS, ENDS, SPANS } -/** True for a calendar event whose start and end fall on different calendar days. */ +/** + * The last calendar day an item's occurrence actually covers. Google + * Calendar's all-day end date is EXCLUSIVE -- a single-day all-day event on + * July 13 has end=July14 00:00 (one day past its own day; mirrors the same + * convention already handled server-side, see effectiveEndDay in + * internal/handlers/timeline.go) -- so for an all-day item this subtracts + * one day to get the real last day. Timed events' end is already the real + * end instant, so no adjustment is needed there. + */ +private fun effectiveEndDay(end: Instant, zone: ZoneId, isAllDay: Boolean): LocalDate { + val day = end.atZone(zone).toLocalDate() + return if (isAllDay) day.minusDays(1) else day +} + +/** True for a calendar event whose start and (adjusted) end fall on different calendar days. */ internal fun isMultiDayEvent(item: WidgetItem, zone: ZoneId): Boolean { if (item.type != "event") return false val start = item.start?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return false val end = item.end?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return false - return start.atZone(zone).toLocalDate() != end.atZone(zone).toLocalDate() + val startDay = start.atZone(zone).toLocalDate() + val endDay = effectiveEndDay(end, zone, item.isAllDay) + return endDay.isAfter(startDay) } /** @@ -1011,7 +1047,7 @@ internal fun isMultiDayEvent(item: WidgetItem, zone: ZoneId): Boolean { */ internal fun multiDayVariant(item: WidgetItem, renderDay: LocalDate, zone: ZoneId): MultiDayVariant { val start = Instant.parse(item.start!!).atZone(zone).toLocalDate() - val end = Instant.parse(item.end!!).atZone(zone).toLocalDate() + val end = effectiveEndDay(Instant.parse(item.end!!), zone, item.isAllDay) return when { renderDay.isEqual(start) -> MultiDayVariant.STARTS renderDay.isEqual(end) -> MultiDayVariant.ENDS @@ -1024,7 +1060,7 @@ internal fun multiDayVariant(item: WidgetItem, renderDay: LocalDate, zone: ZoneI - [ ] **Step 4: Run tests to verify they pass** Run: `cd /workspace/doot/android && ./gradlew testDebugUnitTest --tests "org.terst.doot.widget.ui.DootWidgetMultiDayTest"` -Expected: PASS (8 tests). +Expected: PASS (11 tests). - [ ] **Step 5: Wire multi-day events into `WidgetRoot`, `AllDayRow`, and `TomorrowSection`** @@ -1219,7 +1255,7 @@ fun TomorrowSection( - [ ] **Step 6: Run the full unit test suite** Run: `cd /workspace/doot/android && ./gradlew testDebugUnitTest` -Expected: BUILD SUCCESSFUL, all tests pass (including this task's 8 new tests and every previously-existing test). +Expected: BUILD SUCCESSFUL, all tests pass (including this task's 11 new tests and every previously-existing test). - [ ] **Step 7: Commit** -- cgit v1.2.3