@@ -8,81 +8,132 @@ import java.util.Locale
88
99object FuzzyFinder {
1010
11+ /* *
12+ * Scores an AppListItem based on its activity label.
13+ */
1114 fun scoreApp (app : AppListItem , searchChars : String , topScore : Int ): Int {
1215 val appLabel = app.activityLabel
13- val normalizedAppLabel = normalizeString (appLabel)
14- val normalizedSearchChars = normalizeString (searchChars)
16+ val normalizedAppLabel = normalizeTarget (appLabel)
17+ val normalizedSearchChars = normalizeSearch (searchChars)
1518
1619 val fuzzyScore = calculateFuzzyScore(normalizedAppLabel, normalizedSearchChars)
1720 return (fuzzyScore * topScore).toInt()
1821 }
1922
23+ /* *
24+ * Scores a ContactListItem based on its display name.
25+ */
2026 fun scoreContact (contact : ContactListItem , searchChars : String , topScore : Int ): Int {
2127 val contactLabel = contact.displayName
22- val normalizedContactLabel = normalizeString (contactLabel)
23- val normalizedSearchChars = normalizeString (searchChars)
28+ val normalizedContactLabel = normalizeTarget (contactLabel)
29+ val normalizedSearchChars = normalizeSearch (searchChars)
2430
2531 val fuzzyScore = calculateFuzzyScore(normalizedContactLabel, normalizedSearchChars)
2632 return (fuzzyScore * topScore).toInt()
2733 }
2834
29- fun scoreString (appLabel : String , searchChars : String , topScore : Int ): Int {
30- val normalizedAppLabel = normalizeString(appLabel)
31- val normalizedSearchChars = normalizeString(searchChars)
35+ /* *
36+ * Scores a raw string against a search query.
37+ */
38+ fun scoreString (target : String , searchChars : String , topScore : Int ): Int {
39+ val normalizedTarget = normalizeTarget(target)
40+ val normalizedSearchChars = normalizeSearch(searchChars)
3241
33- val fuzzyScore = calculateFuzzyScore(normalizedAppLabel , normalizedSearchChars)
42+ val fuzzyScore = calculateFuzzyScore(normalizedTarget , normalizedSearchChars)
3443 return (fuzzyScore * topScore).toInt()
3544 }
3645
37- // Simplified normalization for app label and search string
38- private fun normalizeString (input : String ): String {
39- return input
40- .uppercase(Locale .getDefault())
41- .let { normalizeDiacritics(it) }
42- .replace(Regex (" [-_+,. ]" ), emptyString())
43- }
46+ /* *
47+ * Advanced Fuzzy Matching Score.
48+ * Returns a float between 0.0 and 1.0.
49+ * 1.0 represents a perfect prefix match.
50+ */
51+ internal fun calculateFuzzyScore (haystack : String , needle : String ): Float {
52+ if (needle.isEmpty() || haystack.isEmpty()) return 0f
53+
54+ val n = needle.length
55+ val m = haystack.length
56+ if (n > m) return 0f
57+
58+ val scoreMatch = 100
59+ val scoreConsecutive = 90
60+ val scoreWordStart = 80
61+ val scoreStartIdxBonus = 15
62+
63+ var currentScore = 0
64+ var needleIdx = 0
65+ var prevHaystackIdx = - 1
66+
67+ // 1. Actual Score Loop
68+ for (i in 0 until m) {
69+ if (needleIdx >= n) break
70+ if (haystack[i] == needle[needleIdx]) {
71+ var charScore = scoreMatch
72+ if (prevHaystackIdx != - 1 && i == prevHaystackIdx + 1 ) charScore + = scoreConsecutive
73+
74+ val isWordStart = if (i == 0 ) true else {
75+ val prevChar = haystack[i - 1 ]
76+ prevChar == ' ' || prevChar == ' .' || prevChar == ' _' || prevChar == ' -' || prevChar == ' ,'
77+ }
78+ if (isWordStart) charScore + = scoreWordStart
79+ if (i < 3 ) charScore + = scoreStartIdxBonus
4480
45- // Remove diacritics from a string
46- private fun normalizeDiacritics ( input : String ): String {
47- return Normalizer .normalize(input, Normalizer . Form . NFD )
48- .replace( Regex ( " \\ p{InCombiningDiacriticalMarks}+ " ), emptyString())
49- }
81+ currentScore + = charScore
82+ prevHaystackIdx = i
83+ needleIdx ++
84+ }
85+ }
5086
51- // Function to check if normalized strings match
52- fun isMatch (appLabel : String , searchChars : String ): Boolean {
53- val normalizedAppLabel = normalizeString(appLabel)
54- val normalizedSearchChars = normalizeString(searchChars)
87+ if (needleIdx < n) return 0f
5588
56- return normalizedAppLabel.contains(normalizedSearchChars, ignoreCase = true )
57- }
89+ // 2. Corrected Perfect Max Score
90+ // We only calculate the potential score for characters that ARE NOT spaces.
91+ var perfectMaxScore = 0
92+ for (i in 0 until m) {
93+ if (haystack[i] == ' ' ) continue // <--- SKIP spaces in the denominator!
5894
59- // Fuzzy matching logic (kept as it is)
60- private fun calculateFuzzyScore (s1 : String , s2 : String ): Float {
61- val m = s1.length
62- val n = s2.length
63- var matchCount = 0
64- var s1Index = 0
65-
66- // Iterate over each character in s2 and check if it exists in s1
67- for (c2 in s2) {
68- var found = false
69-
70- // Start searching for c2 from the current s1Index
71- for (j in s1Index until m) {
72- if (s1[j] == c2) {
73- found = true
74- s1Index = j + 1 // Move to the next position in s1
75- break
76- }
95+ var potential = scoreMatch
96+ val isWordStart = if (i == 0 ) true else {
97+ val prevChar = haystack[i - 1 ]
98+ prevChar == ' ' || prevChar == ' .' || prevChar == ' _' || prevChar == ' -' || prevChar == ' ,'
7799 }
78100
79- // If the current character in s2 is not found in s1, return a score of 0
80- if (! found) return 0f
101+ potential + = if (isWordStart) scoreWordStart else scoreConsecutive
102+ if (i < 3 ) potential + = scoreStartIdxBonus
81103
82- matchCount ++
104+ perfectMaxScore + = potential
83105 }
84106
85- // Return score based on the ratio of matched characters to the longer string length
86- return matchCount.toFloat() / maxOf(m, n)
107+ return if (perfectMaxScore == 0 ) 0f else currentScore.toFloat() / perfectMaxScore
108+ }
109+
110+ /* *
111+ * Simple boolean match for legacy support or low-power filtering.
112+ */
113+ fun isMatch (target : String , searchChars : String ): Boolean {
114+ val normalizedTarget = normalizeString(target)
115+ val normalizedSearch = normalizeString(searchChars)
116+ return normalizedTarget.contains(normalizedSearch)
117+ }
118+
119+ // --- Normalization Helpers ---
120+ private fun normalizeString (input : String ): String {
121+ return normalizeDiacritics(input.uppercase(Locale .getDefault()))
122+ .replace(Regex (" [-_+,. ]" ), emptyString())
123+ }
124+
125+ private fun normalizeSearch (input : String ): String {
126+ return normalizeDiacritics(input.uppercase(Locale .getDefault()))
127+ .replace(Regex (" [-_+,. ]" ), emptyString())
128+ }
129+
130+ private fun normalizeTarget (input : String ): String {
131+ return normalizeDiacritics(input.uppercase(Locale .getDefault()))
132+ .replace(Regex (" [-_+,.]" ), " " ) // Keep spaces to detect word boundaries
133+ }
134+
135+ private fun normalizeDiacritics (input : String ): String {
136+ return Normalizer .normalize(input, Normalizer .Form .NFD )
137+ .replace(Regex (" \\ p{InCombiningDiacriticalMarks}+" ), emptyString())
87138 }
88139}
0 commit comments