diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 36d30ca0e4..1819030911 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -267,9 +267,6 @@ dependencies { implementation(platform(kotlinx.coroutines.bom)) implementation(kotlinx.bundles.coroutines) - // Text distance - implementation(libs.java.string.similarity) - // TLS 1.3 support for Android < 10 implementation(libs.conscrypt) diff --git a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt index fc7dd754eb..7c9ba7b480 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt @@ -6,12 +6,12 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.toNormalized -import info.debatty.java.stringsimilarity.NormalizedLevenshtein import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.supervisorScope import uy.kohesive.injekt.injectLazy +import yokai.util.normalizedLevenshteinSimilarity import kotlin.coroutines.CoroutineContext class SmartSearchEngine( @@ -22,8 +22,6 @@ class SmartSearchEngine( private val db: DatabaseHelper by injectLazy() - private val normalizedLevenshtein = NormalizedLevenshtein() - /*suspend fun smartSearch(source: CatalogueSource, title: String): SManga? { val cleanedTitle = cleanSmartSearchTitle(title) @@ -40,7 +38,7 @@ class SmartSearchEngine( searchResults.mangas.map { val cleanedMangaTitle = cleanSmartSearchTitle(it.title) - val normalizedDistance = normalizedLevenshtein.similarity(cleanedTitle, cleanedMangaTitle) + val normalizedDistance = normalizedLevenshteinSimilarity(cleanedTitle, cleanedMangaTitle) SearchEntry(it, normalizedDistance) }.filter { (_, normalizedDistance) -> normalizedDistance >= MIN_SMART_ELIGIBLE_THRESHOLD @@ -68,7 +66,7 @@ class SmartSearchEngine( } searchResults.mangas.map { - val normalizedDistance = normalizedLevenshtein.similarity(titleNormalized, it.title.toNormalized()) + val normalizedDistance = normalizedLevenshteinSimilarity(titleNormalized, it.title.toNormalized()) SearchEntry(it, normalizedDistance) }.filter { (_, normalizedDistance) -> normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD @@ -77,6 +75,7 @@ class SmartSearchEngine( return eligibleManga.maxByOrNull { it.dist }?.manga } + private fun removeTextInBrackets(text: String, readForward: Boolean): String { val bracketPairs = listOf( '(' to ')', diff --git a/app/src/main/java/yokai/util/Levenshtein.kt b/app/src/main/java/yokai/util/Levenshtein.kt new file mode 100644 index 0000000000..3c998f1455 --- /dev/null +++ b/app/src/main/java/yokai/util/Levenshtein.kt @@ -0,0 +1,57 @@ +package yokai.util + +import kotlin.math.max +import kotlin.math.min + +/** + * Modified version of ademar111190's Levenshtein implementation + * + * REF: https://gist.github.com/ademar111190/34d3de41308389a0d0d8 + */ +fun levenshteinDistance(lhs : CharSequence, rhs : CharSequence): Int { + if (lhs == rhs) return 0 + if (lhs.isEmpty()) return rhs.length + if (rhs.isEmpty()) return lhs.length + + val lhsLength = lhs.length + 1 + val rhsLength = rhs.length + 1 + + var cost = Array(lhsLength) { it } + var newCost = Array(lhsLength) { 0 } + + for (i in 1..= Int.MAX_VALUE) return Int.MAX_VALUE + + val swap = cost + cost = newCost + newCost = swap + } + + return cost.last() +} + +fun normalizedLevenshteinSimilarity(lhs : CharSequence, rhs : CharSequence): Double { + val distance by lazy { + val maxLength = max(lhs.length, rhs.length) + if (maxLength == 0) return@lazy 0.0 + levenshteinDistance(lhs, rhs) / maxLength.toDouble() + } + + return 1.0 - distance +} diff --git a/app/src/test/java/yokai/util/LevenshteinTest.kt b/app/src/test/java/yokai/util/LevenshteinTest.kt new file mode 100644 index 0000000000..485c7b61a5 --- /dev/null +++ b/app/src/test/java/yokai/util/LevenshteinTest.kt @@ -0,0 +1,27 @@ +package yokai.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +// REF: https://gist.github.com/ademar111190/34d3de41308389a0d0d8?permalink_comment_id=4675859#gistcomment-4675859 +class LevenshteinTest { + @Test + fun `Distance Test`() { + testDistance("", "", 0) + testDistance("1", "1", 0) + testDistance("1", "2", 1) + testDistance("12", "12", 0) + testDistance("123", "12", 1) + testDistance("1234", "1", 3) + testDistance("1234", "1233", 1) + testDistance("", "12345", 5) + testDistance("kitten", "mittens", 2) + testDistance("canada", "canad", 1) + testDistance("canad", "canada", 1) + } + + private fun testDistance(a: String, b: String, expectedDistance: Int) { + val d = levenshteinDistance(a, b) + assertEquals(expectedDistance, d, "Distance did not match for `$a` and `$b`") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5115f5d3c9..7825d74b68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,6 @@ mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version = "v3.1 nucleus-support-v7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus" } nucleus = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus" } java-nat-sort = { module = "com.github.gpanther:java-nat-sort", version = "natural-comparator-1.1" } -java-string-similarity = { module = "info.debatty:java-string-similarity", version = "2.0.0" } jsoup = { module = "org.jsoup:jsoup", version = "1.17.1" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }