dev.sphericalkat.sublimefuzzy.Fuzzy.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sublime-fuzzy Show documentation
Show all versions of sublime-fuzzy Show documentation
Kotlin implementation of Sublime Text's fuzzy search algorithm
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
*/
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
}
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)
}
// 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
}
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)
}
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
)
}
}