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 }