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.LocaleDisplayNames
import android.icu.text.TimeZoneNames import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -44,6 +45,13 @@ fun TimeZone.regionName(
return "$location, $region" return "$location, $region"
} }
@RequiresApi(Build.VERSION_CODES.N)
fun TimeZone.displayName(
locale: ULocale,
): String {
return this.getDisplayName(locale) ?: id
}
private val TimeZone.fallbackRegion: String private val TimeZone.fallbackRegion: String
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
get() = id get() = id

View File

@ -21,9 +21,16 @@ package com.sadellie.unitto.data.model.timezone
import android.icu.util.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( data class SearchResultZone(
val timeZone: TimeZone, 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.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.sadellie.unitto.data.common.displayName
import com.sadellie.unitto.data.common.lev import com.sadellie.unitto.data.common.lev
import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.common.regionName
import com.sadellie.unitto.data.database.TimeZoneDao import com.sadellie.unitto.data.database.TimeZoneDao
@ -40,7 +41,7 @@ import javax.inject.Singleton
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
@Singleton @Singleton
class TimeZonesRepository @Inject constructor( 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 // Not implemented because it will take me too much time to map 600+ TimeZones and codes
// private val codeToTimeZoneId: HashMap<String, String> by lazy { // private val codeToTimeZoneId: HashMap<String, String> by lazy {
@ -66,13 +67,13 @@ class TimeZonesRepository @Inject constructor(
suspend fun moveTimeZone( suspend fun moveTimeZone(
timeZone: FavoriteZone, timeZone: FavoriteZone,
targetPosition: Int targetPosition: Int,
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
dao.moveMove(timeZone.timeZone.id, timeZone.position, targetPosition) dao.moveMove(timeZone.timeZone.id, timeZone.position, targetPosition)
} }
suspend fun addToFavorites( suspend fun addToFavorites(
timeZone: TimeZone timeZone: TimeZone,
) { ) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
dao.addToFavorites(timeZone.id) dao.addToFavorites(timeZone.id)
@ -80,37 +81,55 @@ class TimeZonesRepository @Inject constructor(
} }
suspend fun removeFromFavorites( suspend fun removeFromFavorites(
timeZone: FavoriteZone timeZone: FavoriteZone,
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
dao.removeFromFavorites(timeZone.timeZone.id) dao.removeFromFavorites(timeZone.timeZone.id)
} }
suspend fun updateLabel( suspend fun updateLabel(
timeZone: FavoriteZone, timeZone: FavoriteZone,
label: String label: String,
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
dao.updateLabel(timeZone.timeZone.id, label) dao.updateLabel(timeZone.timeZone.id, label)
} }
suspend fun filterAllTimeZones( suspend fun filter(
searchQuery: String, searchQuery: String,
locale: ULocale, locale: ULocale,
): List<SearchResultZone> = ): List<SearchResultZone> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val favorites = dao.getFavorites().first().map { it.id }
val timeZoneNames = TimeZoneNames.getInstance(locale) val timeZoneNames = TimeZoneNames.getInstance(locale)
val localeDisplayNames = LocaleDisplayNames.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 query = searchQuery.trim().lowercase()
val threshold: Int = query.length / 2 val threshold: Int = query.length / 2
val timeZonesWithDist = mutableListOf<Pair<SearchResultZone, Int>>() val timeZonesWithDist = mutableSetOf<SearchResultZone>()
TimeZone.getAvailableIDs().forEach { timeZoneId -> // Don't use map here so that only needed SearchResultZone objects will be created
if (timeZoneId in favorites) return@forEach timezones.forEach {
val displayName = it.displayName(locale)
val timeZone = TimeZone.getTimeZone(timeZoneId) val regionName = it.regionName(timeZoneNames, localeDisplayNames)
val displayName = timeZone.displayName
val regionName = timeZone.regionName(timeZoneNames, localeDisplayNames)
// // CODE Match // // CODE Match
// if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) { // if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) {
@ -118,49 +137,50 @@ class TimeZonesRepository @Inject constructor(
// return@forEach // return@forEach
// } // }
// Display name match val nameMatch = matchProperty(displayName, query, threshold)
when { if (nameMatch != null) {
// not zero, so that lev can have that timeZonesWithDist.add(
displayName.startsWith(query) -> { SearchResultZone(
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1) timeZone = it,
name = displayName,
region = regionName,
rank = nameMatch
)
)
return@forEach return@forEach
} }
displayName.contains(query) -> { val regionMatch = matchProperty(regionName, query, threshold)
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2) if (regionMatch != null) {
timeZonesWithDist.add(
SearchResultZone(
timeZone = it,
name = displayName,
region = regionName,
rank = regionMatch
)
)
return@forEach return@forEach
} }
} }
val displayNameLevDist = displayName
.substring(0, minOf(query.length, displayName.length)) 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) .lev(query)
if (displayNameLevDist < threshold) { if (levDist < levThreshold) return levDist
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to displayNameLevDist)
return@forEach
}
// ID Match return null
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)
return@forEach
}
}
return@withContext timeZonesWithDist.sortedBy { it.second }.map { it.first }
} }
} }

View File

@ -24,7 +24,6 @@ import android.icu.util.TimeZone
import android.icu.util.ULocale import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -35,6 +34,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource 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.common.UnittoSearchBar
import com.sadellie.unitto.core.ui.datetime.formatLocal import com.sadellie.unitto.core.ui.datetime.formatLocal
import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall 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.offset
import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.common.regionName
import com.sadellie.unitto.data.model.timezone.SearchResultZone import com.sadellie.unitto.data.model.timezone.SearchResultZone
@ -82,7 +83,6 @@ fun AddTimeZoneScreen(
userTime: ZonedDateTime, userTime: ZonedDateTime,
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags())
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@ -109,8 +109,8 @@ fun AddTimeZoneScreen(
addToFavorites(it.timeZone) addToFavorites(it.timeZone)
navigateUp() navigateUp()
}, },
headlineContent = { Text(it.timeZone.getDisplayName(locale)) }, headlineContent = { Text(it.name) },
supportingContent = { Text(it.formattedLabel) }, supportingContent = { Text(it.region) },
trailingContent = { trailingContent = {
Text( Text(
text = it.timeZone.offset(userTime).formatLocal(), text = it.timeZone.offset(userTime).formatLocal(),
@ -130,6 +130,9 @@ fun AddTimeZoneScreen(
@Composable @Composable
fun PreviewAddTimeZoneScreen() { fun PreviewAddTimeZoneScreen() {
val locale = ULocale.getDefault() val locale = ULocale.getDefault()
val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) }
val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) }
AddTimeZoneScreen( AddTimeZoneScreen(
uiState = AddTimeZoneUIState.Ready( uiState = AddTimeZoneUIState.Ready(
query = TextFieldValue(), query = TextFieldValue(),
@ -141,10 +144,12 @@ fun PreviewAddTimeZoneScreen() {
val zone = TimeZone.getTimeZone(it) val zone = TimeZone.getTimeZone(it)
SearchResultZone( SearchResultZone(
timeZone = zone, timeZone = zone,
formattedLabel = zone.regionName( region = zone.regionName(
timeZoneNames = TimeZoneNames.getInstance(locale), timeZoneNames = timeZoneNames,
localeDisplayNames = LocaleDisplayNames.getInstance(locale) localeDisplayNames = localeDisplayNames
) ),
name = zone.displayName(locale),
rank = 0
) )
} }
), ),

View File

@ -58,7 +58,7 @@ class AddTimeZoneViewModel @Inject constructor(
.mapLatest { ui -> .mapLatest { ui ->
viewModelScope.launch { viewModelScope.launch {
_result.update { _result.update {
timezonesRepository.filterAllTimeZones( timezonesRepository.filter(
searchQuery = ui.query.text, searchQuery = ui.query.text,
locale = ULocale.forLanguageTag( locale = ULocale.forLanguageTag(
AppCompatDelegate.getApplicationLocales().toLanguageTags() AppCompatDelegate.getApplicationLocales().toLanguageTags()