From aee26a28f398eeb8e2b11ee5bf7c0e41fa75562d Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 20:47:25 +0200 Subject: [PATCH] core-time: add formatDateTimeCompact for dense list rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../floret/time/DateTimeFormat.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core-time/src/main/kotlin/de/jeanlucmakiola/floret/time/DateTimeFormat.kt b/core-time/src/main/kotlin/de/jeanlucmakiola/floret/time/DateTimeFormat.kt index 9f1bc4a..3c10bf3 100644 --- a/core-time/src/main/kotlin/de/jeanlucmakiola/floret/time/DateTimeFormat.kt +++ b/core-time/src/main/kotlin/de/jeanlucmakiola/floret/time/DateTimeFormat.kt @@ -1,8 +1,13 @@ package de.jeanlucmakiola.floret.time +import java.time.LocalDate +import java.time.LocalTime import java.time.ZoneId +import java.time.chrono.IsoChronology import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder import java.time.format.FormatStyle +import java.util.Locale import kotlin.time.Instant /** @@ -26,3 +31,28 @@ fun Instant.formatTime(): String = atSystemZone().format(timeFormatter) /** Date alone for all-day items, otherwise date + time. */ fun Instant.formatDateTime(allDay: Boolean): String = 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()}" +}