From bbacbf78f0621111e104a0f9bbb4ace79e857930 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Sun, 8 Oct 2023 19:07:21 +0300 Subject: [PATCH] Fix time zone search --- .../unitto/data/common/TimeZoneUtils.kt | 8 ++ .../data/model/timezone/SearchResultZone.kt | 11 +- .../data/timezone/TimeZonesRepository.kt | 122 ++++++++++-------- .../feature/timezone/AddTimeZoneScreen.kt | 21 +-- .../feature/timezone/AddTimeZoneViewModel.kt | 2 +- 5 files changed, 102 insertions(+), 62 deletions(-) diff --git a/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt index 89b00b71..5a42ccf6 100644 --- a/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt +++ b/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt @@ -21,6 +21,7 @@ package com.sadellie.unitto.data.common import android.icu.text.LocaleDisplayNames import android.icu.text.TimeZoneNames import android.icu.util.TimeZone +import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi import java.time.ZonedDateTime @@ -44,6 +45,13 @@ fun TimeZone.regionName( return "$location, $region" } +@RequiresApi(Build.VERSION_CODES.N) +fun TimeZone.displayName( + locale: ULocale, +): String { + return this.getDisplayName(locale) ?: id +} + private val TimeZone.fallbackRegion: String @RequiresApi(Build.VERSION_CODES.N) get() = id diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt index 3c4f3a32..f2239357 100644 --- a/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt +++ b/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt @@ -21,9 +21,16 @@ package com.sadellie.unitto.data.model.timezone import android.icu.util.TimeZone /** - * Don't get 'region' from [timeZone]. Use [formattedLabel] (same but cached) + * Use cached name and region properties! + * + * @property timeZone Same as [TimeZone] + * @property name Cached localized [TimeZone.getDisplayName] + * @property region Cached localized [TimeZone.getRegion] + * @property rank Higher is better, closer to the search query */ data class SearchResultZone( val timeZone: TimeZone, - val formattedLabel: String + val name: String, + val region: String, + val rank: Int, ) diff --git a/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt b/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt index 1ba9b7a5..d5519726 100644 --- a/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt +++ b/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt @@ -24,6 +24,7 @@ import android.icu.util.TimeZone import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi +import com.sadellie.unitto.data.common.displayName import com.sadellie.unitto.data.common.lev import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.database.TimeZoneDao @@ -40,7 +41,7 @@ import javax.inject.Singleton @RequiresApi(Build.VERSION_CODES.N) @Singleton class TimeZonesRepository @Inject constructor( - private val dao: TimeZoneDao + private val dao: TimeZoneDao, ) { // Not implemented because it will take me too much time to map 600+ TimeZones and codes // private val codeToTimeZoneId: HashMap by lazy { @@ -66,13 +67,13 @@ class TimeZonesRepository @Inject constructor( suspend fun moveTimeZone( timeZone: FavoriteZone, - targetPosition: Int + targetPosition: Int, ) = withContext(Dispatchers.IO) { dao.moveMove(timeZone.timeZone.id, timeZone.position, targetPosition) } suspend fun addToFavorites( - timeZone: TimeZone + timeZone: TimeZone, ) { withContext(Dispatchers.IO) { dao.addToFavorites(timeZone.id) @@ -80,37 +81,55 @@ class TimeZonesRepository @Inject constructor( } suspend fun removeFromFavorites( - timeZone: FavoriteZone + timeZone: FavoriteZone, ) = withContext(Dispatchers.IO) { dao.removeFromFavorites(timeZone.timeZone.id) } suspend fun updateLabel( timeZone: FavoriteZone, - label: String + label: String, ) = withContext(Dispatchers.IO) { dao.updateLabel(timeZone.timeZone.id, label) } - suspend fun filterAllTimeZones( + suspend fun filter( searchQuery: String, locale: ULocale, ): List = withContext(Dispatchers.IO) { - val favorites = dao.getFavorites().first().map { it.id } val timeZoneNames = TimeZoneNames.getInstance(locale) val localeDisplayNames = LocaleDisplayNames.getInstance(locale) + val favorites = dao.getFavorites().first().map { it.id } + val timezones = TimeZone.getAvailableIDs() + .filter { it !in favorites } + .map { TimeZone.getTimeZone(it) } + + if (searchQuery.isBlank()) { + return@withContext timezones + .map { + val displayName = it.displayName(locale) + val regionName = it.regionName(timeZoneNames, localeDisplayNames) + + SearchResultZone( + timeZone = it, + name = displayName, + region = regionName, + rank = 0, + ) + } + .sortedBy { it.name } + } + val query = searchQuery.trim().lowercase() val threshold: Int = query.length / 2 - val timeZonesWithDist = mutableListOf>() + val timeZonesWithDist = mutableSetOf() - TimeZone.getAvailableIDs().forEach { timeZoneId -> - if (timeZoneId in favorites) return@forEach - - val timeZone = TimeZone.getTimeZone(timeZoneId) - val displayName = timeZone.displayName - val regionName = timeZone.regionName(timeZoneNames, localeDisplayNames) + // Don't use map here so that only needed SearchResultZone objects will be created + timezones.forEach { + val displayName = it.displayName(locale) + val regionName = it.regionName(timeZoneNames, localeDisplayNames) // // CODE Match // if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) { @@ -118,49 +137,50 @@ class TimeZonesRepository @Inject constructor( // return@forEach // } - // Display name match - when { - // not zero, so that lev can have that - displayName.startsWith(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1) - return@forEach - } - - displayName.contains(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2) - return@forEach - } - } - val displayNameLevDist = displayName - .substring(0, minOf(query.length, displayName.length)) - .lev(query) - if (displayNameLevDist < threshold) { - timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to displayNameLevDist) + val nameMatch = matchProperty(displayName, query, threshold) + if (nameMatch != null) { + timeZonesWithDist.add( + SearchResultZone( + timeZone = it, + name = displayName, + region = regionName, + rank = nameMatch + ) + ) return@forEach } - // ID Match - when { - // not zero, so that lev can have that - regionName.startsWith(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1) - return@forEach - } - - regionName.contains(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2) - return@forEach - } - } - val regionNameLevDist = regionName - .substring(0, minOf(query.length, regionName.length)) - .lev(query) - if (regionNameLevDist < threshold) { - timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to regionNameLevDist) + val regionMatch = matchProperty(regionName, query, threshold) + if (regionMatch != null) { + timeZonesWithDist.add( + SearchResultZone( + timeZone = it, + name = displayName, + region = regionName, + rank = regionMatch + ) + ) return@forEach } } - return@withContext timeZonesWithDist.sortedBy { it.second }.map { it.first } + return@withContext timeZonesWithDist.sortedBy { it.rank } } + + private fun matchProperty( + prop: String, + query: String, + levThreshold: Int, + ): Int? { + if (prop.startsWith(query, true)) return 1 + + if (prop.contains(query, true)) return 2 + + val levDist = prop + .substring(0, minOf(query.length, prop.length)) + .lev(query) + if (levDist < levThreshold) return levDist + + return null + } } diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt index 5d74608d..5d1a4f07 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt @@ -24,7 +24,6 @@ import android.icu.util.TimeZone import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyColumn @@ -35,6 +34,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource @@ -48,6 +48,7 @@ import com.sadellie.unitto.core.ui.common.UnittoListItem import com.sadellie.unitto.core.ui.common.UnittoSearchBar import com.sadellie.unitto.core.ui.datetime.formatLocal import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall +import com.sadellie.unitto.data.common.displayName import com.sadellie.unitto.data.common.offset import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.model.timezone.SearchResultZone @@ -82,7 +83,6 @@ fun AddTimeZoneScreen( userTime: ZonedDateTime, ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -109,8 +109,8 @@ fun AddTimeZoneScreen( addToFavorites(it.timeZone) navigateUp() }, - headlineContent = { Text(it.timeZone.getDisplayName(locale)) }, - supportingContent = { Text(it.formattedLabel) }, + headlineContent = { Text(it.name) }, + supportingContent = { Text(it.region) }, trailingContent = { Text( text = it.timeZone.offset(userTime).formatLocal(), @@ -130,6 +130,9 @@ fun AddTimeZoneScreen( @Composable fun PreviewAddTimeZoneScreen() { val locale = ULocale.getDefault() + val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) } + val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) } + AddTimeZoneScreen( uiState = AddTimeZoneUIState.Ready( query = TextFieldValue(), @@ -141,10 +144,12 @@ fun PreviewAddTimeZoneScreen() { val zone = TimeZone.getTimeZone(it) SearchResultZone( timeZone = zone, - formattedLabel = zone.regionName( - timeZoneNames = TimeZoneNames.getInstance(locale), - localeDisplayNames = LocaleDisplayNames.getInstance(locale) - ) + region = zone.regionName( + timeZoneNames = timeZoneNames, + localeDisplayNames = localeDisplayNames + ), + name = zone.displayName(locale), + rank = 0 ) } ), diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt index 922d5d4a..996be5c2 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt @@ -58,7 +58,7 @@ class AddTimeZoneViewModel @Inject constructor( .mapLatest { ui -> viewModelScope.launch { _result.update { - timezonesRepository.filterAllTimeZones( + timezonesRepository.filter( searchQuery = ui.query.text, locale = ULocale.forLanguageTag( AppCompatDelegate.getApplicationLocales().toLanguageTags()