fix(reminders): show the day for reminders on another day (#46)
A reminder fired ahead of an event on a different day showed only the event's time (e.g. "09:30 – 10:00"), making it look like it was happening today. reminderTimeText now prefixes timed events with a relative day: "Tomorrow"/"Yesterday", the short weekday for another day this week, or the exact date for anything further out. The this-week boundary honours the user's "week starts on" setting: the resolved first day of week is threaded through from ReminderNotifier, so e.g. a Sunday reads as next week under a Sunday-start locale. All-day events keep their explicit date (never ambiguous), and cross-midnight timed events keep both explicit dates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package de.jeanlucmakiola.calendula.data.reminders
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
@@ -18,41 +19,110 @@ class ReminderTimeTextTest {
|
||||
private fun utcMidnight(date: LocalDate): Long =
|
||||
date.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()
|
||||
|
||||
/** Wrapper defaulting `today` to the event's own begin day, so unrelated tests read "today". */
|
||||
private fun text(
|
||||
beginMillis: Long,
|
||||
endMillis: Long,
|
||||
isAllDay: Boolean = false,
|
||||
zone: ZoneId = berlin,
|
||||
locale: Locale = Locale.GERMANY,
|
||||
is24Hour: Boolean = true,
|
||||
today: LocalDate? = null,
|
||||
firstDayOfWeek: DayOfWeek = DayOfWeek.MONDAY,
|
||||
): String = reminderTimeText(
|
||||
beginMillis = beginMillis,
|
||||
endMillis = endMillis,
|
||||
isAllDay = isAllDay,
|
||||
zone = zone,
|
||||
locale = locale,
|
||||
is24Hour = is24Hour,
|
||||
today = today ?: java.time.Instant.ofEpochMilli(beginMillis).atZone(zone).toLocalDate(),
|
||||
firstDayOfWeek = firstDayOfWeek,
|
||||
tomorrowLabel = "Tomorrow",
|
||||
yesterdayLabel = "Yesterday",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `timed event on one day shows just the time range`() {
|
||||
val text = reminderTimeText(
|
||||
fun `timed event today shows just the time range`() {
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 11, 9, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 11, 10, 0), berlin),
|
||||
isAllDay = false,
|
||||
zone = berlin,
|
||||
locale = Locale.GERMANY,
|
||||
is24Hour = true,
|
||||
)
|
||||
assertThat(text).isEqualTo("09:30 – 10:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `12-hour preference renders an am-pm time range`() {
|
||||
val text = reminderTimeText(
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 11, 14, 0), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 11, 15, 0), berlin),
|
||||
isAllDay = false,
|
||||
zone = berlin,
|
||||
locale = Locale.US,
|
||||
is24Hour = false,
|
||||
)
|
||||
assertThat(text).isEqualTo("2:00 PM – 3:00 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tomorrow's event is prefixed with the tomorrow label`() {
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 12, 9, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 12, 10, 0), berlin),
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("Tomorrow, 09:30 – 10:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `yesterday's event is prefixed with the yesterday label`() {
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 10, 9, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 10, 10, 0), berlin),
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("Yesterday, 09:30 – 10:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an event later this week is prefixed with the short weekday`() {
|
||||
// 2026-06-11 is a Thursday; +3 days lands on Sunday, still this (Mon-based) week.
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 14, 9, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 14, 10, 0), berlin),
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("So., 09:30 – 10:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the week boundary honours the week-start setting`() {
|
||||
// Today Thu 2026-06-11, event Sun 2026-06-14. With a Sunday-start week the
|
||||
// Thursday's week runs Sun 06-07..Sat 06-13, so 06-14 is already next week
|
||||
// and must render as a date — the opposite of the Monday-start case above.
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 14, 9, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 14, 10, 0), berlin),
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
firstDayOfWeek = DayOfWeek.SUNDAY,
|
||||
)
|
||||
assertThat(text).isEqualTo("14.06.2026, 09:30 – 10:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an event next week falls back to the exact date, not a weekday`() {
|
||||
// 2026-06-15 is the following Monday — a weekday alone would be ambiguous.
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 15, 9, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 15, 10, 0), berlin),
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("15.06.2026, 09:30 – 10:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timed event crossing midnight includes both dates`() {
|
||||
val text = reminderTimeText(
|
||||
val text = text(
|
||||
beginMillis = millisAt(LocalDateTime.of(2026, 6, 11, 23, 30), berlin),
|
||||
endMillis = millisAt(LocalDateTime.of(2026, 6, 12, 0, 30), berlin),
|
||||
isAllDay = false,
|
||||
zone = berlin,
|
||||
locale = Locale.GERMANY,
|
||||
is24Hour = true,
|
||||
)
|
||||
assertThat(text).contains("11.06.2026")
|
||||
assertThat(text).contains("12.06.2026")
|
||||
@@ -61,28 +131,35 @@ class ReminderTimeTextTest {
|
||||
|
||||
@Test
|
||||
fun `all-day single day shows one date, read in UTC`() {
|
||||
val text = reminderTimeText(
|
||||
val text = text(
|
||||
beginMillis = utcMidnight(LocalDate.of(2026, 6, 11)),
|
||||
endMillis = utcMidnight(LocalDate.of(2026, 6, 12)),
|
||||
isAllDay = true,
|
||||
// Zone must not matter for all-day events: UTC midnight is
|
||||
// 02:00 in Berlin — naive local reading would shift the day.
|
||||
zone = berlin,
|
||||
locale = Locale.GERMANY,
|
||||
is24Hour = true,
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("11.06.2026")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all-day event tomorrow still shows the exact date, no relative prefix`() {
|
||||
val text = text(
|
||||
beginMillis = utcMidnight(LocalDate.of(2026, 6, 12)),
|
||||
endMillis = utcMidnight(LocalDate.of(2026, 6, 13)),
|
||||
isAllDay = true,
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("12.06.2026")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all-day multi-day shows the last covered day, not the exclusive end`() {
|
||||
val text = reminderTimeText(
|
||||
val text = text(
|
||||
beginMillis = utcMidnight(LocalDate.of(2026, 6, 11)),
|
||||
endMillis = utcMidnight(LocalDate.of(2026, 6, 13)),
|
||||
isAllDay = true,
|
||||
zone = berlin,
|
||||
locale = Locale.GERMANY,
|
||||
is24Hour = true,
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("11.06.2026 – 12.06.2026")
|
||||
}
|
||||
@@ -90,13 +167,11 @@ class ReminderTimeTextTest {
|
||||
@Test
|
||||
fun `degenerate all-day range never renders an inverted span`() {
|
||||
val day = utcMidnight(LocalDate.of(2026, 6, 11))
|
||||
val text = reminderTimeText(
|
||||
val text = text(
|
||||
beginMillis = day,
|
||||
endMillis = day,
|
||||
isAllDay = true,
|
||||
zone = berlin,
|
||||
locale = Locale.GERMANY,
|
||||
is24Hour = true,
|
||||
today = LocalDate.of(2026, 6, 11),
|
||||
)
|
||||
assertThat(text).isEqualTo("11.06.2026")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user