1 module fuzzyd.core;
2 
3 import std.stdio;
4 import std.array;
5 import std.range;
6 import std.math;
7 import std.conv;
8 import std.uni;
9 import std.algorithm.iteration;
10 import std.algorithm.sorting;
11 import std.algorithm : max;
12 import std.typecons;
13 
14 alias fuzzyFn = long delegate(string, ref FuzzyResult[]);
15 
16 private:
17 
18 long consecutiveBonus(ref long[] lidx, long cidx)
19 {
20     return lidx.length > 0 && lidx[$ - 1] == cidx - 1 ? 20 : 0;
21 }
22 
23 long caseMatchBonus(dchar x, dchar y)
24 {
25     return x.isUpper && y.isUpper ? 15 : 0;
26 }
27 
28 long firstIdx(long idx)
29 {
30     return idx == 0 ? 10 : 0;
31 }
32 
33 public:
34 
35 /// fuzzy search result
36 struct FuzzyResult
37 {
38     string value; /// entry. e.g "Documents/foo/bar/"
39     long score; /// similarity metric. (Higher better)
40     long[] matches; /// index of matched characters (0 = miss , 1 = hit).
41     bool isMatch; /// return true if consecutive characters are matched.
42 }
43 
44 /**
45  * Fuzzy search
46  * Params:
47  *   db = Array of string containing the search list.
48  * Examples:
49  * --------------------
50  * auto result = new FuzzyResult[3];
51  * fuzzy(["foo", "bar", "baz"])("br", result);
52  * // => result
53    // [FuzzyResult("foo", 0, [], false), FuzzyResult("bar", 1030, [0, 2], true), FuzzyResult("baz", 20, [0], false)]
54  * --------------------
55  */
56 fuzzyFn fuzzy(string[] db)
57 {
58 
59     FuzzyResult score(ref string txt, ref dchar[] pattern)
60     {
61         if (pattern == "")
62             return FuzzyResult(txt, 0, [], true);
63 
64         const patternLength = pattern.length;
65         long score = 0;
66         bool start, end;
67 
68         long[] lidx;
69         long j = 0;
70         long i = 0;
71         foreach (t; txt.byCodePoint)
72         {
73             if (t.toLower == pattern[j].toLower)
74             {
75                 score += 10;
76                 score += firstIdx(i) + consecutiveBonus(lidx, i) + caseMatchBonus(t, pattern[j]);
77                 if (j == 0)
78                     start = true;
79 
80                 j++;
81                 lidx ~= i;
82                 if (j == patternLength)
83                 {
84                     end = true;
85                     break;
86                 }
87             }
88             i++;
89         }
90         if (start && end)
91         {
92             score += 1000;
93         }
94         return FuzzyResult(txt, score, lidx, start && end);
95     }
96 
97     long search(string pattern, ref FuzzyResult[] result)
98     {
99         long totalMatches = 0;
100         auto p = pattern.byCodePoint.array;
101         for (long i = 0, max = result.length; i < max; i++)
102         {
103             result[i] = score(db[i], p);
104             if (result[i].isMatch)
105                 totalMatches++;
106         }
107         return totalMatches;
108     }
109 
110     return &search;
111 }