mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Added fuzzy satellite name search ignoring separators (#255)
Co-authored-by: atsunatsu <atsunatsu@users.noreply.github.com>
This commit is contained in:
1 parent
5e39c945b2
commit
de3d5a6d84
2 files changed
+78
-2
No files matched your search
+19
-2
@@ -114,12 +114,29 @@ class SelectionRepo(
|
|||||||
/**
|
/**
|
||||||
* Filters items by query. Uses toIntOrNull() instead of exception-based flow,
|
* Filters items by query. Uses toIntOrNull() instead of exception-based flow,
|
||||||
* and lowercases the query once up front instead of per-item.
|
* and lowercases the query once up front instead of per-item.
|
||||||
|
*
|
||||||
|
* Fuzzy search: the query is split into space-separated tokens and every
|
||||||
|
* token must appear in the satellite name after both sides are normalized
|
||||||
|
* (lowercased, non-alphanumeric separators such as dashes, spaces, brackets
|
||||||
|
* and dots stripped). This makes "ao7" match "AO-7 (AMSAT-OSCAR 7)" and
|
||||||
|
* "iss zarya" match "ISS (ZARYA)" — exact continuous-substring matching
|
||||||
|
* previously failed whenever the name contained a separator the query lacked.
|
||||||
*/
|
*/
|
||||||
private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
|
private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
|
||||||
if (query.isBlank()) return items
|
if (query.isBlank()) return items
|
||||||
val catnum = query.toIntOrNull()
|
val catnum = query.toIntOrNull()
|
||||||
if (catnum != null) return items.filter { it.catnum == catnum }
|
if (catnum != null) return items.filter { it.catnum == catnum }
|
||||||
val lowerQuery = query.lowercase()
|
val tokens = query.split(' ')
|
||||||
return items.filter { it.name.lowercase().contains(lowerQuery) }
|
.map { normalizeForSearch(it) }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
if (tokens.isEmpty()) return items
|
||||||
|
return items.filter { item ->
|
||||||
|
val normalizedName = normalizeForSearch(item.name)
|
||||||
|
tokens.all { normalizedName.contains(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lowercases and strips all non-alphanumeric chars for fuzzy matching. */
|
||||||
|
private fun normalizeForSearch(text: String): String =
|
||||||
|
text.lowercase().filter { it.isLetterOrDigit() }
|
||||||
}
|
}
|
||||||
+59
@@ -84,6 +84,65 @@ class SelectionRepoTest {
|
|||||||
assertEquals(listOf(true, false, false), items.map { it.isSelected })
|
assertEquals(listOf(true, false, false), items.map { it.isSelected })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `query without separators matches name with dashes spaces brackets`() = runTest(dispatcher) {
|
||||||
|
val repository = repoWithSearchEntries()
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setQuery("ao7")
|
||||||
|
val items = flow.first()
|
||||||
|
// "ao7" is a substring of the normalized "AO-73 (FUNcube-1)" too, so a
|
||||||
|
// fuzzy search legitimately returns both AO-7 (first) and AO-73. The key
|
||||||
|
// guarantee is that AO-7 — unreachable before because of dashes/brackets
|
||||||
|
// — is now found.
|
||||||
|
assertEquals(7530, items.first().catnum)
|
||||||
|
assertEquals(listOf(7530, 39444), items.map { it.catnum })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `query without separators matches name with only dashes`() = runTest(dispatcher) {
|
||||||
|
val repository = repoWithSearchEntries()
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setQuery("fo29")
|
||||||
|
assertEquals(listOf(99999), flow.first().map { it.catnum })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `space-separated tokens all must match in any order`() = runTest(dispatcher) {
|
||||||
|
val repository = repoWithSearchEntries()
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setQuery("zarya iss")
|
||||||
|
assertEquals(listOf(25544), flow.first().map { it.catnum })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `partial token query matches substring`() = runTest(dispatcher) {
|
||||||
|
val repository = repoWithSearchEntries()
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setQuery("funcube")
|
||||||
|
assertEquals(listOf(39444), flow.first().map { it.catnum })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unmatched query returns nothing`() = runTest(dispatcher) {
|
||||||
|
val repository = repoWithSearchEntries()
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setQuery("zzzznomatch")
|
||||||
|
assertEquals(emptyList<Int>(), flow.first().map { it.catnum })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun repoWithSearchEntries(): SelectionRepo {
|
||||||
|
val localSource = FakeLocalSource(
|
||||||
|
entries = listOf(
|
||||||
|
SatItem(25544, "ISS (ZARYA)", false),
|
||||||
|
SatItem(7530, "AO-7 (AMSAT-OSCAR 7)", false),
|
||||||
|
SatItem(39444, "AO-73 (FUNcube-1)", false),
|
||||||
|
SatItem(43803, "JO-97 (BIRDS-3)", false),
|
||||||
|
SatItem(99999, "FO-29", false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return SelectionRepo(dispatcher, localSource, FakeSettingsRepo(selectedModes = emptyList()))
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeLocalSource(
|
private class FakeLocalSource(
|
||||||
private val entries: List<SatItem>
|
private val entries: List<SatItem>
|
||||||
) : ILocalSource {
|
) : ILocalSource {
|
||||||
|
|||||||
Reference in new issue
Block a user