feat(timezone): let the picker search by abbreviation
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<TimeZoneOption>, query: String): List<TimeZoneOption> {
|
||||
val needle = query.normalizeForSearch()
|
||||
@@ -154,12 +157,15 @@ fun filterTimeZones(options: List<TimeZoneOption>, query: String): List<TimeZone
|
||||
val city = option.city.normalizeForSearch()
|
||||
val id = option.id.normalizeForSearch()
|
||||
val name = option.displayName.normalizeForSearch()
|
||||
val abbrev = option.shortName.normalizeForSearch()
|
||||
val rank = when {
|
||||
city.startsWith(needle) -> 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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user