Skip to content

Commit 7adf558

Browse files
authored
Merge pull request #41 from Aylur/feat/fuzzy-search
apps: better fuzzy search algorithm
2 parents 27e76f4 + 856f6d0 commit 7adf558

File tree

4 files changed

+83
-43
lines changed

4 files changed

+83
-43
lines changed

lib/apps/application.vala

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ public class Application : Object {
3232
public Score fuzzy_match(string term) {
3333
var score = Score();
3434
if (name != null)
35-
score.name = levenshtein(term, name);
35+
score.name = fuzzy_match_string(term, name);
3636
if (entry != null)
37-
score.entry = levenshtein(term, entry);
37+
score.entry = fuzzy_match_string(term, entry);
3838
if (executable != null)
39-
score.executable = levenshtein(term, executable);
39+
score.executable = fuzzy_match_string(term, executable);
4040
if (description != null)
41-
score.description = levenshtein(term, description);
41+
score.description = fuzzy_match_string(term, description);
4242

4343
return score;
4444
}
@@ -75,44 +75,10 @@ int min3(int a, int b, int c) {
7575
return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c);
7676
}
7777

78-
double levenshtein(string s1, string s2) {
79-
int len1 = s1.length;
80-
int len2 = s2.length;
81-
82-
int[, ] d = new int[len1 + 1, len2 + 1];
83-
84-
for (int i = 0; i <= len1; i++) {
85-
d[i, 0] = i;
86-
}
87-
for (int j = 0; j <= len2; j++) {
88-
d[0, j] = j;
89-
}
90-
91-
for (int i = 1; i <= len1; i++) {
92-
for (int j = 1; j <= len2; j++) {
93-
int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1;
94-
d[i, j] = min3(
95-
d[i - 1, j] + 1, // deletion
96-
d[i, j - 1] + 1, // insertion
97-
d[i - 1, j - 1] + cost // substitution
98-
);
99-
}
100-
}
101-
102-
var distance = d[len1, len2];
103-
int max_len = len1 > len2 ? len1 : len2;
104-
105-
if (max_len == 0) {
106-
return 1.0;
107-
}
108-
109-
return 1.0 - ((double)distance / max_len);
110-
}
111-
11278
public struct Score {
113-
double name;
114-
double entry;
115-
double executable;
116-
double description;
79+
int name;
80+
int entry;
81+
int executable;
82+
int description;
11783
}
11884
}

lib/apps/apps.vala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class Apps : Object {
88
public bool show_hidden { get; set; }
99
public List<weak Application> list { owned get { return _list.copy(); } }
1010

11-
public double min_score { get; set; default = 0.5; }
11+
public int min_score { get; set; default = 0; }
1212

1313
public double name_multiplier { get; set; default = 2; }
1414
public double entry_multiplier { get; set; default = 1; }

lib/apps/fuzzy.vala

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
namespace AstalApps {
3+
4+
private int max(int a, int b) {
5+
return a > b ? a : b;
6+
}
7+
8+
public int fuzzy_match_string(string pattern, string str) {
9+
const int unmatched_letter_penalty = -1;
10+
int score = 100;
11+
12+
if (pattern.length == 0) return score;
13+
if (str.length < pattern.length) return int.MIN;
14+
15+
score += unmatched_letter_penalty * (str.length - pattern.length);
16+
score = fuzzy_match_recurse(pattern, str, score, true);
17+
18+
return score;
19+
}
20+
21+
private int fuzzy_match_recurse(string pattern, string str, int score, bool first_char) {
22+
if (pattern.length == 0) return score;
23+
24+
int match_idx = 0;
25+
int offset = 0;
26+
unichar search = pattern.casefold().get_char(0);
27+
int best_score = int.MIN;
28+
29+
while ((match_idx = str.casefold().substring(offset).index_of_char(search)) >= 0) {
30+
offset += match_idx;
31+
int subscore = fuzzy_match_recurse(
32+
pattern.substring(1),
33+
str.substring(offset + 1),
34+
compute_score(offset, first_char, str, offset), false);
35+
best_score = max(best_score, subscore);
36+
offset++;
37+
}
38+
39+
if (best_score == int.MIN) return int.MIN;
40+
return score + best_score;
41+
}
42+
43+
private int compute_score(int jump, bool first_char, string match, int idx) {
44+
const int adjacency_bonus = 15;
45+
const int separator_bonus = 30;
46+
const int camel_bonus = 30;
47+
const int first_letter_bonus = 15;
48+
const int leading_letter_penalty = -5;
49+
const int max_leading_letter_penalty = -15;
50+
51+
int score = 0;
52+
53+
if (!first_char && jump == 0) {
54+
score += adjacency_bonus;
55+
}
56+
if (!first_char || jump > 0) {
57+
if (match[idx].isupper() && match[idx-1].islower()) {
58+
score += camel_bonus;
59+
}
60+
if (match[idx].isalnum() && !match[idx-1].isalnum()) {
61+
score += separator_bonus;
62+
}
63+
}
64+
if (first_char && jump == 0) {
65+
score += first_letter_bonus;
66+
}
67+
if (first_char) {
68+
score += max(leading_letter_penalty * jump, max_leading_letter_penalty);
69+
}
70+
71+
return score;
72+
}
73+
}

lib/apps/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ sources = [
4444
'apps.vala',
4545
'application.vala',
4646
'cli.vala',
47+
'fuzzy.vala',
4748
]
4849

4950
if get_option('lib')

0 commit comments

Comments
 (0)