diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 5071bbd..acbd8e1 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,26 +1,32 @@ plugins { - id("org.jetbrains.kotlin.jvm") version "1.4.32" - id("org.jetbrains.dokka") version "1.4.30" - id("com.vanniktech.maven.publish") version "0.13.0" + id("org.jetbrains.kotlin.jvm") version "1.4.32" + id("org.jetbrains.dokka") version "1.4.30" + id("com.vanniktech.maven.publish") version "0.13.0" + id("com.ncorti.ktfmt.gradle") version "0.5.0" } repositories { - mavenCentral() - maven("https://dl.bintray.com/kotlin/kotlinx") { - name = "KotlinX Bintray" - content { - includeModule("org.jetbrains.kotlinx", "kotlinx-html-jvm") - } + mavenCentral() + maven("https://dl.bintray.com/kotlin/kotlinx") { + name = "KotlinX Bintray" + content { + includeModule("org.jetbrains.kotlinx", "kotlinx-html-jvm") } + } } dependencies { - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") } signing { useGpgCmd() } + +ktfmt { + googleStyle() + maxWidth.set(100) +} diff --git a/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Constants.kt b/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Constants.kt index b176610..b365ffe 100644 --- a/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Constants.kt +++ b/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Constants.kt @@ -1,12 +1,12 @@ package dev.sphericalkat.sublimefuzzy object Constants { - const val SEQUENTIAL_BONUS = 15 - const val SEPARATOR_BONUS = 30 - const val CAMEL_BONUS = 30 - const val FIRST_LETTER_BONUS = 15 + const val SEQUENTIAL_BONUS = 15 + const val SEPARATOR_BONUS = 30 + const val CAMEL_BONUS = 30 + const val FIRST_LETTER_BONUS = 15 - const val LEADING_LETTER_PENALTY = -5 - const val MAX_LEADING_LETTER_PENALTY = -15 - const val UNMATCHED_LETTER_PENALTY = -1 -} \ No newline at end of file + const val LEADING_LETTER_PENALTY = -5 + const val MAX_LEADING_LETTER_PENALTY = -15 + const val UNMATCHED_LETTER_PENALTY = -1 +} diff --git a/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Fuzzy.kt b/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Fuzzy.kt index 294cf4b..df1ac14 100644 --- a/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Fuzzy.kt +++ b/lib/src/main/kotlin/dev/sphericalkat/sublimefuzzy/Fuzzy.kt @@ -1,193 +1,195 @@ package dev.sphericalkat.sublimefuzzy object Fuzzy { - /** - * Returns true if each character in pattern is found sequentially within str - * - * @param pattern the pattern to match - * @param str the string to search - * @return A boolean representing the match status - */ - fun fuzzyMatchSimple(pattern: String, str: String): Boolean { - var patternIdx = 0 - var strIdx = 0 - - val patternLength = pattern.length - val strLength = str.length - - while (patternIdx != patternLength && strIdx != strLength) { - val patternChar = pattern.toCharArray()[patternIdx].toLowerCase() - val strChar = str.toCharArray()[strIdx].toLowerCase() - if (patternChar == strChar) ++patternIdx - ++strIdx - } + /** + * Returns true if each character in pattern is found sequentially within str + * + * @param pattern the pattern to match + * @param str the string to search + * @return A boolean representing the match status + */ + fun fuzzyMatchSimple(pattern: String, str: String): Boolean { + var patternIdx = 0 + var strIdx = 0 + + val patternLength = pattern.length + val strLength = str.length + + while (patternIdx != patternLength && strIdx != strLength) { + val patternChar = pattern.toCharArray()[patternIdx].toLowerCase() + val strChar = str.toCharArray()[strIdx].toLowerCase() + if (patternChar == strChar) ++patternIdx + ++strIdx + } - return patternLength != 0 && strLength != 0 && patternIdx == patternLength + return patternLength != 0 && strLength != 0 && patternIdx == patternLength + } + + private fun fuzzyMatchRecursive( + pattern: String, + str: String, + patternCurIndexOut: Int, + strCurIndexOut: Int, + srcMatches: MutableList, + matches: MutableList, + maxMatches: Int, + nextMatchOut: Int, + recursionCountOut: Int, + recursionLimit: Int + ): Pair { + var outScore = 0 + var strCurIndex = strCurIndexOut + var patternCurIndex = patternCurIndexOut + var nextMatch = nextMatchOut + var recursionCount = recursionCountOut + + // return if recursion limit is reached + if (++recursionCount >= recursionLimit) { + return Pair(false, outScore) } - private fun fuzzyMatchRecursive( - pattern: String, - str: String, - patternCurIndexOut: Int, - strCurIndexOut: Int, - srcMatches: MutableList, - matches: MutableList, - maxMatches: Int, - nextMatchOut: Int, - recursionCountOut: Int, - recursionLimit: Int - ): Pair { - var outScore = 0 - var strCurIndex = strCurIndexOut - var patternCurIndex = patternCurIndexOut - var nextMatch = nextMatchOut - var recursionCount = recursionCountOut - - // return if recursion limit is reached - if (++recursionCount >= recursionLimit) { - return Pair(false, outScore) - } + // return if we reached end of strings + if (patternCurIndex == pattern.length || strCurIndex == str.length) { + return Pair(false, outScore) + } - // return if we reached end of strings - if (patternCurIndex == pattern.length || strCurIndex == str.length) { - return Pair(false, outScore) + // recursion params + var recursiveMatch = false + val bestRecursiveMatches = mutableListOf() + var bestRecursiveScore = 0 + + // loop through pattern and str looking for a match + var firstMatch = true + while (patternCurIndex < pattern.length && strCurIndex < str.length) { + // match found + if (pattern[patternCurIndex].equals(str[strCurIndex], ignoreCase = true)) { + if (nextMatch >= maxMatches) { + return Pair(false, outScore) } - // recursion params - var recursiveMatch = false - val bestRecursiveMatches = mutableListOf() - var bestRecursiveScore = 0 - - // loop through pattern and str looking for a match - var firstMatch = true - while (patternCurIndex < pattern.length && strCurIndex < str.length) { - // match found - if (pattern[patternCurIndex].equals(str[strCurIndex], ignoreCase = true)) { - if (nextMatch >= maxMatches) { - return Pair(false, outScore) - } - - if (firstMatch && srcMatches.isNotEmpty()) { - matches.clear() - matches.addAll(srcMatches) - firstMatch = false - } - - val recursiveMatches = mutableListOf() - val (matched, recursiveScore) = fuzzyMatchRecursive( - pattern, - str, - patternCurIndex, - strCurIndex + 1, - matches, - recursiveMatches, - maxMatches, - nextMatch, - recursionCount, - recursionLimit - ) - - if (matched) { - // pick best recursive score - if (!recursiveMatch || recursiveScore > bestRecursiveScore) { - bestRecursiveMatches.clear() - bestRecursiveMatches.addAll(recursiveMatches) - bestRecursiveScore = recursiveScore - } - recursiveMatch = true - } - - - matches.add(nextMatch++, strCurIndex) - ++patternCurIndex - } - ++strCurIndex + if (firstMatch && srcMatches.isNotEmpty()) { + matches.clear() + matches.addAll(srcMatches) + firstMatch = false } - val matched = patternCurIndex == pattern.length - - if (matched) { - outScore = 100 - - // apply leading letter penalty - val penalty = - (Constants.LEADING_LETTER_PENALTY * matches[0]).coerceAtLeast(Constants.MAX_LEADING_LETTER_PENALTY) - outScore += penalty - - // apply unmatched penalty - val unmatched = str.length - nextMatch - outScore += Constants.UNMATCHED_LETTER_PENALTY * unmatched - - // apply ordering bonuses - for (i in 0 until nextMatch) { - val currIdx = matches[i] - - if (i > 0) { - val prevIdx = matches[i - 1] - if (currIdx == prevIdx + 1) { - outScore += Constants.SEQUENTIAL_BONUS - } - } - - // check for bonuses based on neighbour character value - if (currIdx > 0) { - // camelcase - val neighbour = str[currIdx - 1] - val curr = str[currIdx] - if (neighbour != neighbour.toUpperCase() && curr != curr.toLowerCase()) { - outScore += Constants.CAMEL_BONUS - } - val isNeighbourSeparator = neighbour == '_' || neighbour == ' ' - if (isNeighbourSeparator) { - outScore += Constants.SEPARATOR_BONUS - } - } else { - // first letter - outScore += Constants.FIRST_LETTER_BONUS - } - } - - // return best result - return if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) { - // recursive score is better than "this" - matches.clear() - matches.addAll(bestRecursiveMatches) - outScore = bestRecursiveScore - Pair(true, outScore) - } else if (matched) { - // "this" score is better than recursive - Pair(true, outScore) - } else { - Pair(false, outScore) - } - } - - return Pair(false, outScore) - } - - /** - * Performs a fuzzy search to find pattern inside a string - * @param pattern the the pattern to match - * @param str the string to search - * @return a [Pair] containing the match status as a [Boolean] and match score as an [Int] - */ - fun fuzzyMatch(pattern: String, str: String): Pair { - val recursionCount = 0 - val recursionLimit = 10 - val matches = mutableListOf() - val maxMatches = 256 - - return fuzzyMatchRecursive( + val recursiveMatches = mutableListOf() + val (matched, recursiveScore) = + fuzzyMatchRecursive( pattern, str, - 0, - 0, - mutableListOf(), + patternCurIndex, + strCurIndex + 1, matches, + recursiveMatches, maxMatches, - 0, + nextMatch, recursionCount, recursionLimit + ) + + if (matched) { + // pick best recursive score + if (!recursiveMatch || recursiveScore > bestRecursiveScore) { + bestRecursiveMatches.clear() + bestRecursiveMatches.addAll(recursiveMatches) + bestRecursiveScore = recursiveScore + } + recursiveMatch = true + } + + matches.add(nextMatch++, strCurIndex) + ++patternCurIndex + } + ++strCurIndex + } + + val matched = patternCurIndex == pattern.length + + if (matched) { + outScore = 100 + + // apply leading letter penalty + val penalty = + (Constants.LEADING_LETTER_PENALTY * matches[0]).coerceAtLeast( + Constants.MAX_LEADING_LETTER_PENALTY ) + outScore += penalty + + // apply unmatched penalty + val unmatched = str.length - nextMatch + outScore += Constants.UNMATCHED_LETTER_PENALTY * unmatched + + // apply ordering bonuses + for (i in 0 until nextMatch) { + val currIdx = matches[i] + + if (i > 0) { + val prevIdx = matches[i - 1] + if (currIdx == prevIdx + 1) { + outScore += Constants.SEQUENTIAL_BONUS + } + } + + // check for bonuses based on neighbour character value + if (currIdx > 0) { + // camelcase + val neighbour = str[currIdx - 1] + val curr = str[currIdx] + if (neighbour != neighbour.toUpperCase() && curr != curr.toLowerCase()) { + outScore += Constants.CAMEL_BONUS + } + val isNeighbourSeparator = neighbour == '_' || neighbour == ' ' + if (isNeighbourSeparator) { + outScore += Constants.SEPARATOR_BONUS + } + } else { + // first letter + outScore += Constants.FIRST_LETTER_BONUS + } + } + + // return best result + return if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) { + // recursive score is better than "this" + matches.clear() + matches.addAll(bestRecursiveMatches) + outScore = bestRecursiveScore + Pair(true, outScore) + } else if (matched) { + // "this" score is better than recursive + Pair(true, outScore) + } else { + Pair(false, outScore) + } } + + return Pair(false, outScore) + } + + /** + * Performs a fuzzy search to find pattern inside a string + * @param pattern the the pattern to match + * @param str the string to search + * @return a [Pair] containing the match status as a [Boolean] and match score as an [Int] + */ + fun fuzzyMatch(pattern: String, str: String): Pair { + val recursionCount = 0 + val recursionLimit = 10 + val matches = mutableListOf() + val maxMatches = 256 + + return fuzzyMatchRecursive( + pattern, + str, + 0, + 0, + mutableListOf(), + matches, + maxMatches, + 0, + recursionCount, + recursionLimit + ) + } } diff --git a/lib/src/test/kotlin/dev/sphericalkat/sublimefuzzy/FuzzyTest.kt b/lib/src/test/kotlin/dev/sphericalkat/sublimefuzzy/FuzzyTest.kt index 1fba1f4..621ccac 100644 --- a/lib/src/test/kotlin/dev/sphericalkat/sublimefuzzy/FuzzyTest.kt +++ b/lib/src/test/kotlin/dev/sphericalkat/sublimefuzzy/FuzzyTest.kt @@ -6,29 +6,29 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class FuzzyTest { - @Test - fun `fuzzy match simple success`() { - val result = Fuzzy.fuzzyMatchSimple("test", "tests") - assertTrue(result, "Each character in pattern should be found sequentially in str") - } + @Test + fun `fuzzy match simple success`() { + val result = Fuzzy.fuzzyMatchSimple("test", "tests") + assertTrue(result, "Each character in pattern should be found sequentially in str") + } - @Test - fun `fuzzy match simple failure`() { - val result = Fuzzy.fuzzyMatchSimple("test", "tset") - assertFalse(result, "Each character in pattern should be found sequentially in str") - } + @Test + fun `fuzzy match simple failure`() { + val result = Fuzzy.fuzzyMatchSimple("test", "tset") + assertFalse(result, "Each character in pattern should be found sequentially in str") + } - @Test - fun `fuzzy match success`() { - val (match, score) = Fuzzy.fuzzyMatch("fot", "FbxImporter.h") - assertTrue(match) - assertEquals(105, score) - } + @Test + fun `fuzzy match success`() { + val (match, score) = Fuzzy.fuzzyMatch("fot", "FbxImporter.h") + assertTrue(match) + assertEquals(105, score) + } - @Test - fun `fuzzy match failure`() { - val (match, score) = Fuzzy.fuzzyMatch("fot", "kot") - assertFalse(match) - assertEquals(0, score) - } + @Test + fun `fuzzy match failure`() { + val (match, score) = Fuzzy.fuzzyMatch("fot", "kot") + assertFalse(match) + assertEquals(0, score) + } }