From 629896b74d4bf4b52622288efa75272d1b183a5d Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 19 Jul 2026 11:01:25 +0200 Subject: [PATCH] fix(timezone): resolve the abbreviation via java.util, not java.time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The abbreviation came from java.time's "zzz" formatter, which on Android has no short specific-zone names and silently degrades every zone to a "GMT-4" form — so on device New York showed "GMT-04:00" instead of "EDT". Desktop couldn't catch it: there "zzz" and java.util.TimeZone agree, and that agreement is the whole trap. Resolve through java.util.TimeZone.getDisplayName(inDst, SHORT, locale) instead — ICU-backed, so it returns the real abbreviation on both Android and the JVM. Zones with no named abbreviation still fall back to a GMT form, which zoneDescriptor already collapses to a single token. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/domain/TimeZoneCatalog.kt | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalog.kt b/app/src/main/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalog.kt index b67af5d..97aa834 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalog.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalog.kt @@ -3,8 +3,6 @@ package de.jeanlucmakiola.calendula.domain import java.text.Normalizer import java.time.Instant import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter import java.time.format.TextStyle import java.util.Locale @@ -35,16 +33,24 @@ data class TimeZoneOption( val region: String get() = id.substringBeforeLast('/', missingDelimiterValue = "") } -private fun optionFor(zone: ZoneId, locale: Locale, at: Instant) = TimeZoneOption( - id = zone.id, - displayName = zone.getDisplayName(TextStyle.FULL, locale), - // "zzz" resolves the DST-correct abbreviation at [at] (CET vs CEST); zones - // with no named abbreviation fall back to a "GMT+05:30" form, which is - // exactly what we'd want to show them as anyway. - shortName = ZonedDateTime.ofInstant(at, zone) - .format(DateTimeFormatter.ofPattern("zzz", locale)), - offsetMinutes = zone.rules.getOffset(at).totalSeconds / 60, -) +private fun optionFor(zone: ZoneId, locale: Locale, at: Instant): TimeZoneOption { + val offsetSeconds = zone.rules.getOffset(at).totalSeconds + return TimeZoneOption( + id = zone.id, + displayName = zone.getDisplayName(TextStyle.FULL, locale), + // The abbreviation ("EDT"/"CEST", DST-correct at [at]). Resolved through + // java.util.TimeZone, NOT java.time's "zzz" formatter: on Android the + // latter has no short specific-zone names and silently degrades every + // zone to a "GMT-4" form (it agrees with java.util only on desktop), so + // "zzz" cost us the real abbreviation on the one platform that ships. + // java.util is ICU-backed and returns the abbreviation on both. Zones + // with genuinely no named abbreviation still fall back to a GMT form, + // which is what we'd show anyway. + shortName = java.util.TimeZone.getTimeZone(zone.id) + .getDisplayName(zone.rules.isDaylightSavings(at), java.util.TimeZone.SHORT, locale), + offsetMinutes = offsetSeconds / 60, + ) +} /** * Every zone the JVM knows, resolved at [at]. This is ~600 entries, so build it