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