core-time: add formatDateTimeCompact for dense list rows

Drops the year in the current year and drops the time when it carries no information (all-day items or a midnight time), so list rows show glanceable dates like "20 Jun" or "20 Jun · 14:30".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 20:47:25 +02:00
parent 86f44805d1
commit aee26a28f3

View File

@@ -1,8 +1,13 @@
package de.jeanlucmakiola.floret.time package de.jeanlucmakiola.floret.time
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId import java.time.ZoneId
import java.time.chrono.IsoChronology
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale
import kotlin.time.Instant import kotlin.time.Instant
/** /**
@@ -26,3 +31,28 @@ fun Instant.formatTime(): String = atSystemZone().format(timeFormatter)
/** Date alone for all-day items, otherwise date + time. */ /** Date alone for all-day items, otherwise date + time. */
fun Instant.formatDateTime(allDay: Boolean): String = fun Instant.formatDateTime(allDay: Boolean): String =
if (allDay) formatDate() else "${formatDate()} · ${formatTime()}" if (allDay) formatDate() else "${formatDate()} · ${formatTime()}"
// The locale's medium date pattern with the year token stripped, e.g. "20 Jun" /
// "Jun 20" — derived from the pattern so day/month order still follows the locale.
// Built once; locale changes within a running process are rare enough to ignore.
private val dateNoYearFormatter: DateTimeFormatter = run {
val medium = DateTimeFormatterBuilder.getLocalizedDateTimePattern(
FormatStyle.MEDIUM, null, IsoChronology.INSTANCE, Locale.getDefault(),
)
val noYear = medium.replace(Regex("[\\s.,/'-]*y+[\\s.,/'-]*"), " ").trim()
DateTimeFormatter.ofPattern(noYear, Locale.getDefault())
}
/**
* Compact form for dense list rows: the year is dropped when the date is in the
* current year, and the time is dropped when it carries no information (all-day
* items, or a midnight time). Examples: "20 Jun", "20 Jun · 14:30", "20 Jun 2027".
*/
fun Instant.formatDateTimeCompact(allDay: Boolean): String {
val zdt = atSystemZone()
val datePart =
if (zdt.year == LocalDate.now(ZoneId.systemDefault()).year) zdt.format(dateNoYearFormatter)
else formatDate()
return if (allDay || zdt.toLocalTime() == LocalTime.MIDNIGHT) datePart
else "$datePart · ${formatTime()}"
}