From 4ea68adf8b60b5702d6556b66b50a23ebae148ed Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 19 Jul 2026 11:25:22 +0200 Subject: [PATCH] feat(timezone): let the picker search by abbreviation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search matched city, id, and the long localized name but not the abbreviation, so typing "CEST" found nothing. Match it too: an exact abbreviation hit ranks just under a city prefix, so typing an abbreviation gathers every zone that shows it (all the CEST zones at once). It matches the region-resolved abbreviation — i.e. exactly what the row displays. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/domain/TimeZoneCatalog.kt | 16 +++++++++++----- .../calendula/domain/TimeZoneCatalogTest.kt | 13 +++++++++++++ 2 files changed, 24 insertions(+), 5 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 e688a66..c107099 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalog.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalog.kt @@ -144,7 +144,10 @@ fun zoneDescriptor(option: TimeZoneOption): String { * Ranking puts a city that *starts with* the query above one that merely * contains it — typing "col" should reach Colombo before Turks_and_Caicos — * and the id is matched ahead of the localized name so a user who knows the - * IANA id gets it first. + * IANA id gets it first. An exact abbreviation hit ("CEST") ranks just under a + * city prefix, so typing an abbreviation surfaces every zone that shows it + * (all the CEST zones together); the abbreviation is the one resolved for the + * display region, i.e. what the row actually shows. */ fun filterTimeZones(options: List, query: String): List { val needle = query.normalizeForSearch() @@ -154,12 +157,15 @@ fun filterTimeZones(options: List, query: String): List 0 - name.startsWith(needle) -> 1 - city.contains(needle) -> 2 - id.contains(needle) -> 3 - name.contains(needle) -> 4 + abbrev == needle -> 1 + name.startsWith(needle) -> 2 + abbrev.startsWith(needle) -> 3 + city.contains(needle) -> 4 + id.contains(needle) -> 5 + name.contains(needle) -> 6 else -> return@mapNotNull null } rank to option diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalogTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalogTest.kt index 1ef2512..56f6e8b 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalogTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/domain/TimeZoneCatalogTest.kt @@ -105,6 +105,19 @@ class TimeZoneCatalogTest { assertThat(filter("europe/berlin").map { it.id }).contains("Europe/Berlin") } + @Test + fun `query matches the abbreviation, case-insensitively`() { + // "cest" isn't a city, id, or substring of "Central European Summer + // Time", so the only way these match is via the abbreviation. + val hits = filter("cest") + assertThat(hits).isNotEmpty() + assertThat(hits.map { it.id }).contains("Europe/Berlin") + // Every hit genuinely carries that abbreviation — nothing bled in. + assertThat(hits.all { it.shortName.equals("CEST", ignoreCase = true) }).isTrue() + // Case doesn't matter. + assertThat(filter("CEST").map { it.id }).isEqualTo(hits.map { it.id }) + } + @Test fun `a city starting with the query outranks one merely containing it`() { val ids = filter("york").map { it.id }