One dayRange and startOfWeek for the view models

This commit is contained in:
2026-10-16 13:35:34 +02:00
parent ee5b25e54f
commit 4041a56ac8
10 changed files with 50 additions and 48 deletions
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.agenda
import de.jeanlucmakiola.calendula.ui.common.dayRange
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -31,11 +32,8 @@ import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlin.time.Instant
import javax.inject.Inject
@@ -243,8 +241,5 @@ class AgendaViewModel @Inject constructor(
internal const val PAST_CHUNK_DAYS = 30
/** Inclusive instant range from the start of [anchor] through [days] days ahead. */
internal fun agendaRange(anchor: LocalDate, days: Int, zone: TimeZone): ClosedRange<Instant> {
val from = anchor.atStartOfDayIn(zone)
val to = anchor.plus(days, DateTimeUnit.DAY).atTime(23, 59, 59).toInstant(zone)
return from..to
}
internal fun agendaRange(anchor: LocalDate, days: Int, zone: TimeZone): ClosedRange<Instant> =
dayRange(anchor, anchor.plus(days, DateTimeUnit.DAY), zone)
@@ -0,0 +1,22 @@
package de.jeanlucmakiola.calendula.ui.common
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.minus
import kotlinx.datetime.toInstant
import kotlin.time.Instant
/** Instant range from the start of [first] through the last second of [last] in [zone]. */
internal fun dayRange(first: LocalDate, last: LocalDate, zone: TimeZone): ClosedRange<Instant> =
first.atStartOfDayIn(zone)..last.atTime(23, 59, 59).toInstant(zone)
/** Beginning of the week (at [weekStart]) that contains this date. */
internal fun LocalDate.startOfWeek(weekStart: DayOfWeek): LocalDate {
// DayOfWeek.ordinal: MONDAY=0..SUNDAY=6 → identical to ISO ordering.
val offset = ((dayOfWeek.ordinal - weekStart.ordinal) + 7) % 7
return minus(offset, DateTimeUnit.DAY)
}
@@ -1,5 +1,7 @@
package de.jeanlucmakiola.calendula.ui.month
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import de.jeanlucmakiola.calendula.ui.common.dayRange
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -39,12 +41,9 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.YearMonth
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.daysUntil
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Instant
import javax.inject.Inject
@@ -202,13 +201,13 @@ class MonthViewModel @Inject constructor(
// Widened to whole grid weeks at both ends: a month block still
// has to know about an event that starts in the boundary week's
// clipped-off days, or a bar running into the block would vanish.
val first = firstOfMonth(yearMonthForIndex(window.first)).startOfGridWeek(ws)
val first = firstOfMonth(yearMonthForIndex(window.first)).startOfWeek(ws)
val last = firstOfMonth(yearMonthForIndex(window.last))
.plus(1, DateTimeUnit.MONTH)
.minus(1, DateTimeUnit.DAY)
.startOfGridWeek(ws)
.startOfWeek(ws)
.plus(6, DateTimeUnit.DAY)
val range = first.atStartOfDayIn(zone)..last.atTime(23, 59, 59).toInstant(zone)
val range = dayRange(first, last, zone)
combine(
repository.calendars(),
repository.instances(range),
@@ -408,7 +407,7 @@ internal fun layoutMonthWeeks(
byDay: DayIndex,
zone: TimeZone,
): List<MonthWeek> {
val gridStart = firstOfMonth(ym).startOfGridWeek(weekStart)
val gridStart = firstOfMonth(ym).startOfWeek(weekStart)
val weekCount = weekRowsInMonth(ym, weekStart)
return (0 until weekCount).map { row ->
@@ -530,11 +529,8 @@ internal fun monthGridRange(
zone: TimeZone,
): ClosedRange<Instant> {
val firstOfMonth = LocalDate(ym.year, ym.month, 1)
val gridStart = firstOfMonth.startOfGridWeek(weekStart)
val gridEnd = gridStart.plus(41, DateTimeUnit.DAY)
val start = gridStart.atStartOfDayIn(zone)
val end = gridEnd.atTime(23, 59, 59).toInstant(zone)
return start..end
val gridStart = firstOfMonth.startOfWeek(weekStart)
return dayRange(gridStart, gridStart.plus(41, DateTimeUnit.DAY), zone)
}
/**
@@ -646,12 +642,12 @@ private val WEEK_INDEX_EPOCH = LocalDate(1900, 1, 1)
private val WEEK_INDEX_END = LocalDate(2100, 12, 31)
internal fun weekIndexOf(date: LocalDate, weekStart: DayOfWeek): Int {
val base = WEEK_INDEX_EPOCH.startOfGridWeek(weekStart)
return base.daysUntil(date.startOfGridWeek(weekStart)) / 7
val base = WEEK_INDEX_EPOCH.startOfWeek(weekStart)
return base.daysUntil(date.startOfWeek(weekStart)) / 7
}
internal fun weekStartForIndex(index: Int, weekStart: DayOfWeek): LocalDate =
WEEK_INDEX_EPOCH.startOfGridWeek(weekStart).plus(index * 7, DateTimeUnit.DAY)
WEEK_INDEX_EPOCH.startOfWeek(weekStart).plus(index * 7, DateTimeUnit.DAY)
/** Total weeks the Dense grid scrolls through (1900 → 2100). */
internal fun continuousWeekCount(weekStart: DayOfWeek): Int =
@@ -727,9 +723,3 @@ internal fun clipWeekToMonth(week: MonthWeek, month: YearMonth): MonthWeek {
countByDay = week.countByDay.filterKeys { it in own },
)
}
internal fun LocalDate.startOfGridWeek(weekStart: DayOfWeek): LocalDate {
// DayOfWeek.ordinal: MONDAY=0..SUNDAY=6 → identical to ISO ordering.
val offset = ((dayOfWeek.ordinal - weekStart.ordinal) + 7) % 7
return minus(offset, DateTimeUnit.DAY)
}
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.week
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.week
import de.jeanlucmakiola.calendula.ui.common.dayRange
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -41,11 +42,9 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.toKotlinUtcOffset
import kotlinx.datetime.toJavaZoneId
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.daysUntil
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import java.util.Locale
import kotlin.time.Instant
@@ -342,19 +341,9 @@ internal fun layoutAllDay(
private infix fun IntRange.overlaps(other: IntRange): Boolean =
first <= other.last && other.first <= last
/** Beginning of the week (at [weekStart]) that contains this date. */
internal fun LocalDate.startOfWeek(weekStart: DayOfWeek): LocalDate {
// DayOfWeek.ordinal: MONDAY=0..SUNDAY=6 → identical to ISO ordering.
val offset = ((dayOfWeek.ordinal - weekStart.ordinal) + 7) % 7
return minus(offset, DateTimeUnit.DAY)
}
/** Instant range covering the [count] days starting at [start]. */
internal fun spanRange(start: LocalDate, count: Int, zone: TimeZone): ClosedRange<Instant> {
val from = start.atStartOfDayIn(zone)
val to = start.plus(count - 1, DateTimeUnit.DAY).atTime(23, 59, 59).toInstant(zone)
return from..to
}
internal fun spanRange(start: LocalDate, count: Int, zone: TimeZone): ClosedRange<Instant> =
dayRange(start, start.plus(count - 1, DateTimeUnit.DAY), zone)
/**
* This instant's wall-clock time as minutes past [day]'s midnight in [zone] —
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.week
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.month
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import com.google.common.truth.Truth.assertThat
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
@@ -152,7 +153,7 @@ class ContinuousMonthIndexTest {
val wed = LocalDate(2026, 6, 10)
DayOfWeek.entries.forEach { ws ->
val index = weekIndexOf(wed, ws)
assertThat(weekStartForIndex(index, ws)).isEqualTo(wed.startOfGridWeek(ws))
assertThat(weekStartForIndex(index, ws)).isEqualTo(wed.startOfWeek(ws))
assertThat(weekIndexOf(LocalDate(1900, 1, 1), ws)).isAtLeast(0)
assertThat(index).isLessThan(continuousWeekCount(ws))
assertThat(weekIndexOf(LocalDate(2100, 12, 31), ws))
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.month
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.allDayEvent
import de.jeanlucmakiola.calendula.domain.EventResponse
@@ -51,12 +52,12 @@ class MonthLayoutTest {
) = allDayEvent(from, toInclusive, id = id, title = title, color = 0xFF445566.toInt())
@Test
fun `startOfGridWeek snaps back to the configured week start`() {
fun `startOfWeek snaps back to the configured week start`() {
val wed = LocalDate(2026, 6, 10)
assertThat(wed.startOfGridWeek(DayOfWeek.MONDAY)).isEqualTo(jun8)
assertThat(jun8.startOfGridWeek(DayOfWeek.MONDAY)).isEqualTo(jun8)
assertThat(wed.startOfWeek(DayOfWeek.MONDAY)).isEqualTo(jun8)
assertThat(jun8.startOfWeek(DayOfWeek.MONDAY)).isEqualTo(jun8)
// A Sunday-anchored week containing the 10th starts on the 7th.
assertThat(wed.startOfGridWeek(DayOfWeek.SUNDAY)).isEqualTo(LocalDate(2026, 6, 7))
assertThat(wed.startOfWeek(DayOfWeek.SUNDAY)).isEqualTo(LocalDate(2026, 6, 7))
}
@Test
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.week
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.testEvent
import kotlinx.datetime.DateTimeUnit
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.week
import de.jeanlucmakiola.calendula.ui.common.startOfWeek
import com.google.common.truth.Truth.assertThat
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek