Skip to content

Commit

Permalink
refactor: search using fuzzywuzzy (#446)
Browse files Browse the repository at this point in the history
  • Loading branch information
zyrouge committed Apr 19, 2024
1 parent 1d7089e commit 53b5a8c
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 42 deletions.
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,15 @@ dependencies {
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.material3)
implementation(libs.compose.navigation)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.core)
implementation(libs.core.splashscreen)
implementation(libs.fuzzywuzzy)
implementation(libs.jaudiotagger)
implementation(libs.lifecycle.runtime)
implementation(libs.media)
implementation(libs.okhttp3)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)

debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.ui.test.manifest)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.zyrouge.symphony.services.radio

import java.util.*
import java.util.Timer
import kotlin.math.max
import kotlin.math.min

Expand Down Expand Up @@ -32,8 +32,10 @@ object RadioEffects {
timer = kotlin.concurrent.timer(period = options.interval.toLong()) {
if (volume != options.to) {
onUpdate(volume)
volume = if (isReverse) max(options.to, volume + increments)
else min(options.to, volume + increments)
volume = when {
isReverse -> max(options.to, volume + increments)
else -> min(options.to, volume + increments)
}
} else {
ended = true
onFinish(true)
Expand Down
54 changes: 17 additions & 37 deletions app/src/main/java/io/github/zyrouge/symphony/utils/FuzzySearch.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package io.github.zyrouge.symphony.utils

import kotlin.math.min
import me.xdrop.fuzzywuzzy.FuzzySearch
import kotlin.math.max

class FuzzySearchComparator(val input: String) {
fun compareString(value: String) = Fuzzy.compare(input, value)

fun compareCollection(values: Collection<String>): Int {
if (values.isEmpty()) return 0
var score = Int.MAX_VALUE
fun compareCollection(values: Collection<String>): Int? {
if (values.isEmpty()) return null
var score = 0
values.forEach {
score = min(score, compareString(it))
score = max(score, compareString(it))
}
return score
}
Expand All @@ -33,55 +34,34 @@ class FuzzySearcher<T>(val options: List<FuzzySearchOption<T>>) {
): List<FuzzyResultEntity<T>> {
val results = entities
.map { compare(terms, it) }
.sortedBy { it.score }
.sortedByDescending { it.score }
return when {
maxLength > -1 -> results.subListNonStrict(maxLength)
else -> results
}
}

private fun compare(terms: String, entity: T): FuzzyResultEntity<T> {
var score = Int.MAX_VALUE
var score = 0
val comparator = FuzzySearchComparator(terms)
options.forEach { option ->
option.match.invoke(comparator, entity)?.let {
score = min(score, it)
score = max(score, it * option.weight)
}
}
return FuzzyResultEntity(score, entity)
}
}

object Fuzzy {
fun compare(input: String, against: String) =
compareStrict(normalizeTerms(input), normalizeTerms(against))

private const val MATCH_BONUS = 2f
private const val DISTANCE_PENALTY_MULTIPLIER = 0.15f
private const val NO_MATCH_PENALTY = -0.3f

// Source: https://gist.github.com/ademar111190/34d3de41308389a0d0d8?permalink_comment_id=3664644#gistcomment-3664644
private fun compareStrict(lhs: String, rhs: String): Int {
val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1
var cost = Array(lhsLength) { it }
var newCost = Array(lhsLength) { 0 }
for (i in 1 until rhsLength) {
newCost[0] = i
for (j in 1 until lhsLength) {
val match = if (lhs[j - 1] == rhs[i - 1]) 0 else 1
val costReplace = cost[j - 1] + match
val costInsert = cost[j] + 1
val costDelete = newCost[j - 1] + 1
newCost[j] = min(costInsert, costDelete).coerceAtMost(costReplace)
}
val swap = cost
cost = newCost
newCost = swap
}
return cost[lhsLength - 1]
}
fun compare(input: String, against: String) = FuzzySearch.tokenSetPartialRatio(
normalizeTerms(input),
normalizeTerms(against),
)

private val whitespaceRegex = Regex("""\s+""")
private fun normalizeTerms(terms: String) = terms.lowercase().replace(whitespaceRegex, " ")
private val alphaNumericRegex = Regex("""[^A-Za-z0-9]""")
private fun normalizeTerms(terms: String) = terms.lowercase()
.replace(whitespaceRegex, " ")
.replace(alphaNumericRegex, "")
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ compose-material3 = "1.3.0-alpha05"
compose-navigation = "2.7.7"
core = "1.13.0"
core-splashscreen = "1.0.1"
fuzzywuzzy = "1.4.0"
jaudiotagger = "3.0.1"
lifecycle-runtime = "2.7.0"
media = "1.7.0"
Expand All @@ -32,6 +33,7 @@ compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", versi
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
core = { group = "androidx.core", name = "core-ktx", version.ref = "core" }
core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "core-splashscreen" }
fuzzywuzzy = { group = "me.xdrop", name = "fuzzywuzzy", version.ref = "fuzzywuzzy" }
jaudiotagger = { group = "net.jthink", name = "jaudiotagger", version.ref = "jaudiotagger" }
lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" }
media = { group = "androidx.media", name = "media", version.ref = "media" }
Expand Down

0 comments on commit 53b5a8c

Please sign in to comment.