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
|
* Ranking puts a city that *starts with* the query above one that merely
|
||||||
* contains it — typing "col" should reach Colombo before Turks_and_Caicos —
|
* 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
|
* 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> {
|
fun filterTimeZones(options: List<TimeZoneOption>, query: String): List<TimeZoneOption> {
|
||||||
val needle = query.normalizeForSearch()
|
val needle = query.normalizeForSearch()
|
||||||
@@ -154,12 +157,15 @@ fun filterTimeZones(options: List<TimeZoneOption>, query: String): List<TimeZone
|
|||||||
val city = option.city.normalizeForSearch()
|
val city = option.city.normalizeForSearch()
|
||||||
val id = option.id.normalizeForSearch()
|
val id = option.id.normalizeForSearch()
|
||||||
val name = option.displayName.normalizeForSearch()
|
val name = option.displayName.normalizeForSearch()
|
||||||
|
val abbrev = option.shortName.normalizeForSearch()
|
||||||
val rank = when {
|
val rank = when {
|
||||||
city.startsWith(needle) -> 0
|
city.startsWith(needle) -> 0
|
||||||
name.startsWith(needle) -> 1
|
abbrev == needle -> 1
|
||||||
city.contains(needle) -> 2
|
name.startsWith(needle) -> 2
|
||||||
id.contains(needle) -> 3
|
abbrev.startsWith(needle) -> 3
|
||||||
name.contains(needle) -> 4
|
city.contains(needle) -> 4
|
||||||
|
id.contains(needle) -> 5
|
||||||
|
name.contains(needle) -> 6
|
||||||
else -> return@mapNotNull null
|
else -> return@mapNotNull null
|
||||||
}
|
}
|
||||||
rank to option
|
rank to option
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ class TimeZoneCatalogTest {
|
|||||||
assertThat(filter("europe/berlin").map { it.id }).contains("Europe/Berlin")
|
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
|
@Test
|
||||||
fun `a city starting with the query outranks one merely containing it`() {
|
fun `a city starting with the query outranks one merely containing it`() {
|
||||||
val ids = filter("york").map { it.id }
|
val ids = filter("york").map { it.id }
|
||||||
|
|||||||
Reference in New Issue
Block a user