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