app/src/main/java/com/equwal/sbm/Fuzzy.kt (1789 bytes)
1 package com.equwal.sbm 2 3 /** 4 * Search as in fzf. Each word of the query must occur in the text, with its 5 * letters in order but not always together. Case does not matter. 6 */ 7 object Fuzzy { 8 /** The score of [query] in [text], or null when a word does not occur. Higher is better. */ 9 fun score(query: String, text: String): Int? { 10 val hay = text.lowercase() 11 var total = 0 12 for (word in query.lowercase().split(' ', '\t')) { 13 if (word.isEmpty()) continue 14 total += scoreWord(word, hay) ?: return null 15 } 16 return total 17 } 18 19 private fun scoreWord(word: String, hay: String): Int? { 20 // The whole word in one place scores highest, most at the start of a word. 21 val at = hay.indexOf(word) 22 if (at >= 0) return 100 + word.length + (if (startsWord(hay, at)) 20 else 0) 23 var score = 0 24 var previous = -2 25 var from = 0 26 for (c in word) { 27 val i = hay.indexOf(c, from) 28 if (i < 0) return null 29 score += 1 30 if (i == previous + 1) score += 5 31 if (startsWord(hay, i)) score += 3 32 previous = i 33 from = i + 1 34 } 35 return score 36 } 37 38 private fun startsWord(hay: String, i: Int) = i == 0 || !hay[i - 1].isLetterOrDigit() 39 40 /** The items that match [query], best first. Items with the same score keep their order. */ 41 fun <T> filter(items: List<T>, query: String, text: (T) -> String): List<T> { 42 if (query.isBlank()) return items 43 return items.mapIndexedNotNull { i, item -> score(query, text(item))?.let { Triple(it, i, item) } } 44 .sortedWith(compareByDescending<Triple<Int, Int, T>> { it.first }.thenBy { it.second }) 45 .map { it.third } 46 } 47 }