Recently Written · git

sbm-android

sbm bookmarks for Android: fuzzy search, share to add, sync with bm and the browser add-on

git clone https://github.com/equwal/sbm-android

Log | Files | Refs


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 }