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 alias bonusFn = long function(ref Input);
16 
17 private:
18 
19 struct Input
20 {
21     string value;
22     dchar i;
23     dchar p;
24     int col;
25     int row;
26     long[Tuple!(int, int)] history;
27     bool hasSequence = false;
28 
29     bool isMatch()
30     {
31         return i.toLower == p.toLower;
32     }
33 
34     bool isCaseSensitiveMatch()
35     {
36         return i.isUpper && p.isUpper && isMatch;
37     }
38 }
39 
40 long previousCharBonus(ref Input input)
41 {
42     long* bonus = tuple(input.row - 1, input.col - 1) in input.history;
43     if (bonus !is null)
44     {
45         *bonus = *bonus * 2;
46         input.hasSequence = true;
47         return *bonus;
48     }
49     return 0;
50 }
51 
52 long startBonus(ref Input input)
53 {
54     return (input.col == 0 && input.row == 0) ? 10 : 0;
55 }
56 
57 long caseMatchBonus(ref Input input)
58 {
59     return input.isCaseSensitiveMatch ? 15 : 0;
60 }
61 
62 public:
63 
64 /// fuzzy search result
65 struct FuzzyResult
66 {
67     string value; /// entry. e.g "Documents/foo/bar/"
68     long score; /// similarity metric. (Higher better)
69     int[] matches; /// index of matched characters (0 = miss , 1 = hit).
70     bool isMatch; /// return true if consecutive characters are matched.
71 }
72 
73 /**
74  * Fuzzy search
75  * Params:
76  *   db = Array of string containing the search list.
77  * Examples:
78  * --------------------
79  * auto result = new FuzzyResult[3];
80  * fuzzy(["foo", "bar", "baz"])("br", result);
81  * // => result
82    // [FuzzyResult("bar", 25, [0, 2]), FuzzyResult("baz", 20, [0]), FuzzyResult("foo", 0, [])]
83  * --------------------
84  */
85 fuzzyFn fuzzy(string[] db)
86 {
87     bonusFn[] bonusFns = [&previousCharBonus, &startBonus, &caseMatchBonus];
88 
89     long charScore(ref Input input)
90     {
91         return input.isMatch ? reduce!((acc, f) => acc + f(input))(10L, bonusFns) : 0;
92     }
93 
94     FuzzyResult score(string txt, string pattern)
95     {
96         long score = 0;
97         long simpleScore = 0;
98         auto input = Input(txt);
99         foreach (p; pattern.byCodePoint)
100         {
101             input.p = p;
102             foreach (i; txt.byCodePoint)
103             {
104                 input.i = i;
105                 const charScore = charScore(input);
106                 if (charScore >= 10)
107                 {
108                     input.history[tuple(input.row, input.col)] = charScore;
109                 }
110 
111                 if (charScore == 10)
112                     simpleScore += charScore;
113                 else
114                     score += charScore;
115 
116                 input.row++;
117             }
118             input.col++;
119             input.row = 0;
120         }
121 
122         const totalScore = score + (simpleScore / 2);
123         if (pattern.walkLength == 1 && totalScore > 0)
124             input.hasSequence = true;
125 
126         return FuzzyResult(txt, totalScore, input.history.keys.map!(x => x[0]).array, input.hasSequence);
127     }
128 
129     long search(string pattern, ref FuzzyResult[] result)
130     {
131         long totalMatches = 0;
132         for (long i = 0, max = result.length; i < max; i++)
133         {
134             result[i] = score(db[i], pattern);
135             if (result[i].isMatch)
136                 totalMatches++;
137         }
138         return totalMatches;
139     }
140 
141     return &search;
142 }