Use kotlinx-datetime's YearMonth and java.time converters
This commit is contained in:
@@ -3,8 +3,9 @@ package de.jeanlucmakiola.calendula.ui.agenda
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.util.Locale
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.yearMonth
|
||||
|
||||
/**
|
||||
* How far ahead the agenda (screen or widget) shows events, starting at today.
|
||||
@@ -50,7 +51,7 @@ fun AgendaRange.dayCount(anchor: LocalDate, weekStart: DayOfWeek): Int = when (t
|
||||
DAYS_PER_WEEK - sinceWeekStart
|
||||
}
|
||||
AgendaRange.ThisMonth -> {
|
||||
val daysInMonth = YearMonth.of(anchor.year, anchor.month.ordinal + 1).lengthOfMonth()
|
||||
val daysInMonth = anchor.yearMonth.numberOfDays
|
||||
daysInMonth - anchor.day + 1
|
||||
}
|
||||
is AgendaRange.Custom -> days.coerceIn(AgendaRange.MIN_CUSTOM_DAYS, AgendaRange.MAX_CUSTOM_DAYS)
|
||||
@@ -105,8 +106,8 @@ fun agendaRangeWindowSummary(
|
||||
locale: Locale,
|
||||
monthAsSpan: Boolean = false,
|
||||
): String {
|
||||
val javaStart = java.time.LocalDate.of(start.year, start.month.ordinal + 1, start.day)
|
||||
val javaEnd = java.time.LocalDate.of(end.year, end.month.ordinal + 1, end.day)
|
||||
val javaStart = start.toJavaLocalDate()
|
||||
val javaEnd = end.toJavaLocalDate()
|
||||
val dayMonth = localizedDateFormatter(locale, "dMMM")
|
||||
val dayMonthYear = localizedDateFormatter(locale, "dMMMy")
|
||||
return when {
|
||||
|
||||
@@ -52,6 +52,7 @@ import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Instant
|
||||
import java.util.Locale
|
||||
@@ -210,7 +211,7 @@ private fun formatTime(
|
||||
|
||||
private fun formatAgendaDate(date: LocalDate): String {
|
||||
val locale = Locale.getDefault()
|
||||
val java = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day)
|
||||
val java = date.toJavaLocalDate()
|
||||
// Weekday + date in the locale's own field order (e.g. "Wed, Jun 17, 2026"
|
||||
// vs "Mi., 17. Juni 2026") rather than a hardcoded day-month-year layout.
|
||||
return localizedDateFormatter(locale, "EEEdMMMy").format(java)
|
||||
|
||||
@@ -192,10 +192,13 @@ import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.YearMonth
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlin.time.Instant
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
import java.util.Locale
|
||||
import kotlinx.datetime.toJavaMonth
|
||||
import kotlinx.datetime.yearMonth
|
||||
|
||||
private val LocalTodayPulse = staticCompositionLocalOf { 0 }
|
||||
|
||||
@@ -344,7 +347,7 @@ fun MonthScreen(
|
||||
if (dense) {
|
||||
val midweek = weekStartForIndex(listState.firstVisibleItemIndex + 1, weekStart)
|
||||
.plus(3, DateTimeUnit.DAY)
|
||||
YearMonth(midweek.year, midweek.month)
|
||||
midweek.yearMonth
|
||||
} else {
|
||||
yearMonthForIndex(monthIndexForItem(listState.firstVisibleItemIndex))
|
||||
}
|
||||
@@ -393,7 +396,7 @@ fun MonthScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val isOnCurrentMonth = titleMonth == YearMonth(today.year, today.month)
|
||||
val isOnCurrentMonth = titleMonth == today.yearMonth
|
||||
var splitExpanded by rememberSaveable(viewStyle) {
|
||||
mutableStateOf(viewStyle == MonthViewStyle.Paged)
|
||||
}
|
||||
@@ -422,7 +425,7 @@ fun MonthScreen(
|
||||
val jumpToToday: () -> Unit = {
|
||||
todayPulse++
|
||||
if (scrolling) {
|
||||
val targetMonth = YearMonth(today.year, today.month)
|
||||
val targetMonth = today.yearMonth
|
||||
pendingScrollMonth = targetMonth
|
||||
val requestId = ++scrollRequestId
|
||||
viewModel.goToToday()
|
||||
@@ -440,13 +443,13 @@ fun MonthScreen(
|
||||
}
|
||||
} else {
|
||||
viewModel.goToToday()
|
||||
jumpToMonth(YearMonth(today.year, today.month))
|
||||
jumpToMonth(today.yearMonth)
|
||||
}
|
||||
}
|
||||
// Drawer jump-to-date.
|
||||
val jumpToDate: (LocalDate) -> Unit = { target ->
|
||||
if (scrolling) {
|
||||
val targetMonth = YearMonth(target.year, target.month)
|
||||
val targetMonth = target.yearMonth
|
||||
pendingScrollMonth = targetMonth
|
||||
val requestId = ++scrollRequestId
|
||||
viewModel.goToDate(target)
|
||||
@@ -464,7 +467,7 @@ fun MonthScreen(
|
||||
}
|
||||
} else {
|
||||
viewModel.goToDate(target)
|
||||
jumpToMonth(YearMonth(target.year, target.month))
|
||||
jumpToMonth(target.yearMonth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +551,7 @@ fun MonthScreen(
|
||||
// still settling would land and take the tap back.
|
||||
onSelectDay = { date ->
|
||||
viewModel.selectDate(date)
|
||||
jumpToMonth(YearMonth(date.year, date.month))
|
||||
jumpToMonth(date.yearMonth)
|
||||
},
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
@@ -721,7 +724,7 @@ internal fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) {
|
||||
if (showWeekNumbers) Spacer(Modifier.width(rememberWeekNumberGutter()))
|
||||
days.forEach { dow ->
|
||||
val isWeekend = dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY
|
||||
val javaDow = java.time.DayOfWeek.of(dow.ordinal + 1)
|
||||
val javaDow = dow.toJavaDayOfWeek()
|
||||
Text(
|
||||
text = javaDow.getDisplayName(JavaTextStyle.SHORT, locale),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
@@ -940,7 +943,7 @@ internal fun ContinuousMonthGrid(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val monthCount = remember { continuousMonthCount() }
|
||||
val todayMonth = remember(state.today) { YearMonth(state.today.year, state.today.month) }
|
||||
val todayMonth = remember(state.today) { state.today.yearMonth }
|
||||
val timeChipWidth = rememberMonthTimeChipWidth()
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
@@ -2698,7 +2701,7 @@ private fun DayNumberCell(
|
||||
private fun shortMonthName(date: LocalDate): String {
|
||||
val locale = currentLocale()
|
||||
return remember(date.month, locale) {
|
||||
java.time.Month.of(date.month.ordinal + 1)
|
||||
date.month.toJavaMonth()
|
||||
.getDisplayName(JavaTextStyle.SHORT, locale)
|
||||
}
|
||||
}
|
||||
@@ -2861,7 +2864,7 @@ private fun formatMonthTitle(
|
||||
abbreviated: Boolean = false,
|
||||
): String =
|
||||
formatCalendarTitle(
|
||||
date = java.time.LocalDate.of(ym.year, ym.month.ordinal + 1, 1),
|
||||
date = ym.firstDay.toJavaLocalDate(),
|
||||
locale = locale,
|
||||
currentYear = currentYear,
|
||||
skeleton = if (abbreviated) "LLL" else "LLLL",
|
||||
|
||||
@@ -14,10 +14,10 @@ import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.YearMonth
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.datetime.yearMonth
|
||||
|
||||
/**
|
||||
* A live, scaled-down Month view in a given [MonthViewStyle], for the settings
|
||||
@@ -51,7 +51,7 @@ internal fun MonthStylePreview(
|
||||
state = sample.continuous,
|
||||
listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = itemIndexForMonth(
|
||||
monthIndexOf(YearMonth(today.year, today.month)),
|
||||
monthIndexOf(today.yearMonth),
|
||||
),
|
||||
),
|
||||
showWeekNumbers = false,
|
||||
@@ -109,7 +109,7 @@ private fun sampleMonthState(
|
||||
weekStart: DayOfWeek,
|
||||
zone: TimeZone,
|
||||
): SampleMonth {
|
||||
val ym = YearMonth(today.year, today.month)
|
||||
val ym = today.yearMonth
|
||||
val first = LocalDate(ym.year, ym.month, 1)
|
||||
val events = sampleMonthEvents(first, today, zone)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Instant
|
||||
import javax.inject.Inject
|
||||
import kotlinx.datetime.yearMonth
|
||||
|
||||
/** How many months either side of the requested one [MonthViewModel.month] keeps cached. */
|
||||
private const val MONTH_CACHE_SPAN = 2
|
||||
@@ -100,7 +101,7 @@ class MonthViewModel @Inject constructor(
|
||||
}
|
||||
.stateWhileSubscribed(viewModelScope, todayDate)
|
||||
|
||||
private val _month = MutableStateFlow(YearMonth(todayDate.year, todayDate.month))
|
||||
private val _month = MutableStateFlow(todayDate.yearMonth)
|
||||
val month: StateFlow<YearMonth> = _month
|
||||
|
||||
/**
|
||||
@@ -181,11 +182,11 @@ class MonthViewModel @Inject constructor(
|
||||
|
||||
/** Last reported visible range, so a widening step can re-centre on it. */
|
||||
@Volatile
|
||||
private var visibleMonths = monthIndexOf(YearMonth(todayDate.year, todayDate.month))
|
||||
private var visibleMonths = monthIndexOf(todayDate.yearMonth)
|
||||
.let { it..it }
|
||||
|
||||
private val _loadedMonths = MutableStateFlow(
|
||||
monthIndexOf(YearMonth(todayDate.year, todayDate.month))
|
||||
monthIndexOf(todayDate.yearMonth)
|
||||
.let { clampMonthWindow(it - INITIAL_PAD..it + INITIAL_PAD) },
|
||||
)
|
||||
|
||||
@@ -201,10 +202,8 @@ 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)).startOfWeek(ws)
|
||||
val last = firstOfMonth(yearMonthForIndex(window.last))
|
||||
.plus(1, DateTimeUnit.MONTH)
|
||||
.minus(1, DateTimeUnit.DAY)
|
||||
val first = yearMonthForIndex(window.first).firstDay.startOfWeek(ws)
|
||||
val last = yearMonthForIndex(window.last).lastDay
|
||||
.startOfWeek(ws)
|
||||
.plus(6, DateTimeUnit.DAY)
|
||||
val range = dayRange(first, last, zone)
|
||||
@@ -305,7 +304,7 @@ class MonthViewModel @Inject constructor(
|
||||
* silently list a date the grid isn't pointing at.
|
||||
*/
|
||||
fun selectDate(date: LocalDate) {
|
||||
val target = YearMonth(date.year, date.month)
|
||||
val target = date.yearMonth
|
||||
_selectedDate.value = if (target == _month.value && _selectedDate.value == date) null else date
|
||||
if (target != _month.value) _month.value = target
|
||||
}
|
||||
@@ -320,12 +319,12 @@ class MonthViewModel @Inject constructor(
|
||||
} else {
|
||||
val next = selected.plus(days, DateTimeUnit.DAY)
|
||||
_selectedDate.value = next
|
||||
_month.value = YearMonth(next.year, next.month)
|
||||
_month.value = next.yearMonth
|
||||
}
|
||||
}
|
||||
|
||||
fun goToToday() {
|
||||
_month.value = YearMonth(todayDate.year, todayDate.month)
|
||||
_month.value = todayDate.yearMonth
|
||||
_selectedDate.value = null
|
||||
}
|
||||
|
||||
@@ -336,8 +335,8 @@ class MonthViewModel @Inject constructor(
|
||||
*/
|
||||
private fun followToday(old: LocalDate, new: LocalDate) {
|
||||
if (viewStyle.value.isScrolling) return
|
||||
if (_month.value != YearMonth(old.year, old.month)) return
|
||||
_month.value = YearMonth(new.year, new.month)
|
||||
if (_month.value != old.yearMonth) return
|
||||
_month.value = new.yearMonth
|
||||
_selectedDate.value = null
|
||||
}
|
||||
|
||||
@@ -357,7 +356,7 @@ class MonthViewModel @Inject constructor(
|
||||
|
||||
/** Jump to the month containing [date] (drawer jump-to-date). */
|
||||
fun goToDate(date: LocalDate) {
|
||||
_month.value = YearMonth(date.year, date.month)
|
||||
_month.value = date.yearMonth
|
||||
_selectedDate.value = date
|
||||
}
|
||||
|
||||
@@ -407,7 +406,7 @@ internal fun layoutMonthWeeks(
|
||||
byDay: DayIndex,
|
||||
zone: TimeZone,
|
||||
): List<MonthWeek> {
|
||||
val gridStart = firstOfMonth(ym).startOfWeek(weekStart)
|
||||
val gridStart = ym.firstDay.startOfWeek(weekStart)
|
||||
val weekCount = weekRowsInMonth(ym, weekStart)
|
||||
|
||||
return (0 until weekCount).map { row ->
|
||||
@@ -528,8 +527,7 @@ internal fun monthGridRange(
|
||||
weekStart: DayOfWeek,
|
||||
zone: TimeZone,
|
||||
): ClosedRange<Instant> {
|
||||
val firstOfMonth = LocalDate(ym.year, ym.month, 1)
|
||||
val gridStart = firstOfMonth.startOfWeek(weekStart)
|
||||
val gridStart = ym.firstDay.startOfWeek(weekStart)
|
||||
return dayRange(gridStart, gridStart.plus(41, DateTimeUnit.DAY), zone)
|
||||
}
|
||||
|
||||
@@ -558,7 +556,7 @@ internal fun edgeForPad(pad: Int): Int = minOf(WINDOW_EDGE, pad - 1).coerceAtLea
|
||||
* the first of the visible month.
|
||||
*/
|
||||
internal fun agendaStartForMonth(month: YearMonth, today: LocalDate): LocalDate =
|
||||
if (YearMonth(today.year, today.month) == month) {
|
||||
if (today.yearMonth == month) {
|
||||
today
|
||||
} else {
|
||||
LocalDate(month.year, month.month, 1)
|
||||
@@ -626,8 +624,6 @@ internal fun clampMonthWindow(window: IntRange): IntRange {
|
||||
return window.first.coerceIn(0, last)..window.last.coerceIn(0, last)
|
||||
}
|
||||
|
||||
internal fun firstOfMonth(ym: YearMonth): LocalDate = LocalDate(ym.year, ym.month, 1)
|
||||
|
||||
/**
|
||||
* The Dense style addresses *weeks* by absolute index instead: it has no month
|
||||
* blocks to key by, and a row's identity has to survive the months around it
|
||||
@@ -658,7 +654,7 @@ internal fun monthIndexForWeek(weekIndex: Int, weekStart: DayOfWeek): Int {
|
||||
// The row's midweek day: a boundary week is reported as whichever month owns
|
||||
// most of it, the same rule the title uses.
|
||||
val midweek = weekStartForIndex(weekIndex, weekStart).plus(3, DateTimeUnit.DAY)
|
||||
return monthIndexOf(YearMonth(midweek.year, midweek.month))
|
||||
return monthIndexOf(midweek.yearMonth)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -667,19 +663,16 @@ internal fun monthIndexForWeek(weekIndex: Int, weekStart: DayOfWeek): Int {
|
||||
* laid out too, rather than flickering as placeholders.
|
||||
*/
|
||||
internal fun weekWindowFor(window: IntRange, weekStart: DayOfWeek): IntRange {
|
||||
val first = weekIndexOf(firstOfMonth(yearMonthForIndex(window.first)), weekStart) - 1
|
||||
val lastMonthEnd = firstOfMonth(yearMonthForIndex(window.last))
|
||||
.plus(1, DateTimeUnit.MONTH)
|
||||
.minus(1, DateTimeUnit.DAY)
|
||||
val first = weekIndexOf(yearMonthForIndex(window.first).firstDay, weekStart) - 1
|
||||
val lastMonthEnd = yearMonthForIndex(window.last).lastDay
|
||||
return first..(weekIndexOf(lastMonthEnd, weekStart) + 1)
|
||||
}
|
||||
|
||||
/** How many week rows [ym] needs under [weekStart] — 4 (a non-leap February), 5 or 6. */
|
||||
internal fun weekRowsInMonth(ym: YearMonth, weekStart: DayOfWeek): Int {
|
||||
val first = firstOfMonth(ym)
|
||||
val first = ym.firstDay
|
||||
val leadOffset = ((first.dayOfWeek.ordinal - weekStart.ordinal) + 7) % 7
|
||||
val daysInMonth = java.time.YearMonth.of(ym.year, ym.month.ordinal + 1).lengthOfMonth()
|
||||
return (leadOffset + daysInMonth + 6) / 7
|
||||
return (leadOffset + ym.numberOfDays + 6) / 7
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,7 @@ import de.jeanlucmakiola.floret.components.ReorderableRowHeight
|
||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
|
||||
/**
|
||||
* Views: everything that changes how a calendar view reads, grouped by the view
|
||||
@@ -504,7 +505,7 @@ private val WEEK_START_OPTIONS: List<WeekStartPref> =
|
||||
internal fun weekStartLabel(pref: WeekStartPref): String = when (pref) {
|
||||
WeekStartPref.Auto -> stringResource(R.string.settings_week_start_auto)
|
||||
// Localised weekday name, so no per-day string resource is needed.
|
||||
is WeekStartPref.Day -> java.time.DayOfWeek.of(pref.day.ordinal + 1)
|
||||
is WeekStartPref.Day -> pref.day.toJavaDayOfWeek()
|
||||
.getDisplayName(JavaTextStyle.FULL, currentLocale())
|
||||
}
|
||||
|
||||
|
||||
@@ -202,10 +202,12 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
import java.util.Locale
|
||||
import kotlinx.datetime.yearMonth
|
||||
|
||||
/** One lane of the all-day strip, sized to a trimmed bar line (#190). */
|
||||
private val ALL_DAY_ROW_HEIGHT = 20.dp
|
||||
@@ -1084,7 +1086,7 @@ private fun WeekDayHeader(
|
||||
.fillMaxWidth()
|
||||
.padding(top = HEADER_TOP_PADDING, bottom = HEADER_BOTTOM_PADDING, end = TIMELINE_CONTENT_END_INSET),
|
||||
) { date ->
|
||||
val javaDow = java.time.DayOfWeek.of(date.dayOfWeek.ordinal + 1)
|
||||
val javaDow = date.dayOfWeek.toJavaDayOfWeek()
|
||||
val isToday = date == today
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -1958,7 +1960,7 @@ private fun formatWeekTitle(
|
||||
abbreviated: Boolean = false,
|
||||
): String {
|
||||
return formatCalendarTitle(
|
||||
date = java.time.LocalDate.of(weekStart.year, weekStart.month.ordinal + 1, 1),
|
||||
date = weekStart.yearMonth.firstDay.toJavaLocalDate(),
|
||||
locale = locale,
|
||||
currentYear = currentYear,
|
||||
skeleton = if (abbreviated) "LLL" else "LLLL",
|
||||
@@ -1974,7 +1976,7 @@ private fun formatDayTitle(
|
||||
abbreviated: Boolean = false,
|
||||
): String =
|
||||
formatCalendarTitle(
|
||||
date = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day),
|
||||
date = date.toJavaLocalDate(),
|
||||
locale = locale,
|
||||
currentYear = currentYear,
|
||||
skeleton = if (abbreviated) "dMMM" else "EEEdMMM",
|
||||
|
||||
@@ -121,8 +121,8 @@ class ContinuousMonthIndexTest {
|
||||
val weeks = weekWindowFor(window, ws)
|
||||
window.forEach { index ->
|
||||
val ym = yearMonthForIndex(index)
|
||||
val first = weekIndexOf(firstOfMonth(ym), ws)
|
||||
val last = weekIndexOf(firstOfMonth(yearMonthForIndex(index + 1)).minus(1, DateTimeUnit.DAY), ws)
|
||||
val first = weekIndexOf(ym.firstDay, ws)
|
||||
val last = weekIndexOf(yearMonthForIndex(index + 1).firstDay.minus(1, DateTimeUnit.DAY), ws)
|
||||
assertThat(weeks.contains(first)).isTrue()
|
||||
assertThat(weeks.contains(last)).isTrue()
|
||||
}
|
||||
@@ -162,8 +162,8 @@ class ContinuousMonthIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstOfMonth is the 1st`() {
|
||||
assertThat(firstOfMonth(jun26)).isEqualTo(LocalDate(2026, 6, 1))
|
||||
fun `firstDay is the 1st`() {
|
||||
assertThat(jun26.firstDay).isEqualTo(LocalDate(2026, 6, 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user