From 40b4c9e2e4f133d234073a508fe30dc82d580ebc Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 22 Oct 2026 07:38:49 +0200 Subject: [PATCH] fix(worldclock): name offset-set zones by their real offset everywhere The Etc/GMT-n naming fix only covered the local row. A home that follows a device set by UTC offset still put "GMT-9" under a UTC+9 face. The rule now lives in the one place every row and the face take their city from. --- .../ui/worldclock/WorldClockRowState.kt | 43 +++++++++---------- .../ui/worldclock/WorldClockRowsTest.kt | 7 +++ docs/ARCHITECTURE.md | 4 +- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt index 0bded4d..fd1411a 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt @@ -1,6 +1,7 @@ package de.jeanlucmakiola.clockula.ui.worldclock import de.jeanlucmakiola.clockula.domain.WorldClock +import de.jeanlucmakiola.clockula.domain.Zones import de.jeanlucmakiola.clockula.domain.isKnownZone import de.jeanlucmakiola.clockula.domain.worldclock.DayLabel import de.jeanlucmakiola.clockula.domain.worldclock.DayNight @@ -88,7 +89,7 @@ internal object WorldClockRows { WorldClockRowState( id = clock.id, zoneId = clock.zoneId, - name = clock.label ?: cityOf(clock.zoneId, entries), + name = clock.label ?: cityOf(clock.zoneId, entries, now), time = comparison?.localDateTime?.toLocalTime()?.truncatedTo(ChronoUnit.MINUTES), offset = comparison?.let { ZoneOffsetFormat.labelFor(it.offsetMinutes) }, day = comparison?.let { ZoneOffsetFormat.dayLabelFor(it.dayDifference) }, @@ -101,10 +102,6 @@ internal object WorldClockRows { * The automatic local clock's row for [deviceZone], read against home like * any other row. Its [WorldClockRowState.id] is [LOCAL_ROW_ID], which no * stored row can have. - * - * A device set by UTC offset rather than by city (Android 13+ offers it) - * reports an id like `Etc/GMT-9`, whose last segment is no city and has - * its sign reversed, so such a row is named by its offset ("GMT+9"). */ fun local( deviceZone: ZoneId, @@ -112,20 +109,13 @@ internal object WorldClockRows { now: Instant, entries: Map, zoneNames: Map, - ): WorldClockRowState { - val name = if (ZoneCatalog.hasPlaceName(deviceZone.id)) { - null - } else { - ZoneOffsetFormat.gmtLabel(ZoneComparisons.utcOffsetMinutes(deviceZone, now)) - } - return from( - clocks = listOf(WorldClock(id = LOCAL_ROW_ID, zoneId = deviceZone.id, label = name, sortOrder = 0)), - homeZone = homeZone, - now = now, - entries = entries, - zoneNames = zoneNames, - ).single() - } + ): WorldClockRowState = from( + clocks = listOf(WorldClock(id = LOCAL_ROW_ID, zoneId = deviceZone.id, label = null, sortOrder = 0)), + homeZone = homeZone, + now = now, + entries = entries, + zoneNames = zoneNames, + ).single() /** Room ids start at 1, so a row with this id is never a stored one. */ const val LOCAL_ROW_ID: Long = -1L @@ -143,13 +133,22 @@ internal object WorldClockRows { val local = now.toJavaInstant().atZone(homeZone).toLocalTime().truncatedTo(ChronoUnit.SECONDS) return HomeFaceState( zoneId = homeZone.id, - city = cityOf(homeZone.id, entries), + city = cityOf(homeZone.id, entries, now), zoneName = zoneNames[homeZone.id], time = local, dayFraction = DayNight.fractionAt(local), ) } - private fun cityOf(zoneId: String, entries: Map): String = - entries[zoneId]?.city ?: ZoneCatalog.cityFallbackFor(zoneId) + /** + * A device set by UTC offset rather than by city (Android 13+ offers it) + * reports an id like `Etc/GMT-9`, whose last segment is no place and has + * its sign reversed, so such a zone is named by its real offset ("GMT+9"). + */ + private fun cityOf(zoneId: String, entries: Map, now: Instant): String = + if (!ZoneCatalog.hasPlaceName(zoneId) && Zones.isValid(zoneId)) { + ZoneOffsetFormat.gmtLabel(ZoneComparisons.utcOffsetMinutes(ZoneId.of(zoneId), now)) + } else { + entries[zoneId]?.city ?: ZoneCatalog.cityFallbackFor(zoneId) + } } diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowsTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowsTest.kt index d5af1b3..51d40bf 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowsTest.kt @@ -265,4 +265,11 @@ class WorldClockRowsTest { assertThat(local.name).isEqualTo("GMT+9") assertThat(local.offset).isEqualTo(OffsetLabel.Ahead(8, 0)) } + + @Test + fun `a home that follows a device set by UTC offset is named by its real offset`() { + val home = WorldClockRows.home(ZoneId.of("Etc/GMT-9"), T0, entries, emptyMap()) + + assertThat(home.city).isEqualTo("GMT+9") + } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 23fedc9..007bf77 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2406,8 +2406,8 @@ that city already shows the local time. Home that follows the device can never differ from it, so with no stored home the rule is always null, and Settings › Clock says so under the switch rather than hiding it. A device set by UTC offset rather than by city reports an id like `Etc/GMT-9`, whose last segment -has its sign reversed, so that row is named by its real offset ("GMT+9") -instead (`ZoneCatalog.hasPlaceName`). +has its sign reversed, so that zone is named by its real offset ("GMT+9") +instead, on the local row and on the face alike (`ZoneCatalog.hasPlaceName`). The row is not stored. It is `WorldClockUiState.Loaded.local`, built per tick by the source from the same `zones.current()` read the face uses, so a