Fix time zone search

This commit is contained in:
Sad Ellie 2023-10-08 19:07:21 +03:00
parent ce8bc5c738
commit bbacbf78f0
5 changed files with 102 additions and 62 deletions

View File

@ -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

View File

@ -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,
)

View File

@ -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<String, String> 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<SearchResultZone> =
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<Pair<SearchResultZone, Int>>()
val timeZonesWithDist = mutableSetOf<SearchResultZone>()
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
}
}

View File

@ -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
)
}
),

View File

@ -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()