summaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-07-13-multi-day-events.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-13 18:17:06 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:42:31 +0000
commit08ded9fc1114c86b7a748c589aa2b6e8d8202c1c (patch)
tree7f1bb40f062ce81416f762041c98a95add6b13c9 /docs/superpowers/plans/2026-07-13-multi-day-events.md
parentd0f207704a406d4fbc65e89ef37f19bacfa0deef (diff)
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.
Diffstat (limited to 'docs/superpowers/plans/2026-07-13-multi-day-events.md')
-rw-r--r--docs/superpowers/plans/2026-07-13-multi-day-events.md46
1 files changed, 41 insertions, 5 deletions
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**