1 module fuzzyd.core; 2 3 import std.stdio; 4 import std.array; 5 import std.container.rbtree; 6 import std.container.binaryheap; 7 import std.math; 8 import std.conv; 9 import std.uni; 10 import std.algorithm.iteration; 11 12 alias fuzzyFn = FuzzyResult[]delegate(string); 13 alias bonusFn = double function(Input); 14 15 private: 16 17 struct Input 18 { 19 string input; 20 string pattern; 21 dchar i; 22 dchar p; 23 int col; 24 int row; 25 double[][] scoreMatrix; 26 27 bool isMatch() 28 { 29 return i.toLower == p.toLower; 30 } 31 32 bool isCaseSensitiveMatch() 33 { 34 return i.isUpper && p.isUpper && isMatch; 35 } 36 } 37 38 double previousCharBonus(Input input) 39 { 40 return (input.col > 0 && input.row > 0) ? 2.5 * input 41 .scoreMatrix[input.row - 1][input.col - 1] : 0; 42 } 43 44 double startBonus(Input input) 45 { 46 return (input.col == 0 && input.row == 0) ? 1 : 0; 47 } 48 49 double caseMatchBonus(Input input) 50 { 51 return input.isCaseSensitiveMatch ? 1.5 : 0; 52 } 53 54 double wordBoundaryBonus(Input input) 55 { 56 const isInputAt = input.row == 0 || input.row == input.input.length - 1 57 || isWhite(input.input[input.row - 1]) || isWhite(input.input[input.row + 1]); 58 return isInputAt ? 1.2 : 0; 59 } 60 61 FuzzyResult[] normalize(FuzzyResult[] result) 62 { 63 const maxScore = !result.empty ? result[0].score : 1; 64 for (long i = 0; i < result.length; i++) 65 { 66 result[i].score /= maxScore; 67 } 68 return result; 69 } 70 71 public: 72 73 /// fuzzy search result 74 struct FuzzyResult 75 { 76 string value; //// entry. e.g "Documents/foo/bar/" 77 double score; //// similarity metric. (Higher better) 78 RedBlackTree!(int, "a < b", false) matches; //// index of matched characters. 79 } 80 81 /** 82 * Fuzzy search 83 * Params: 84 * db = Array of string containing the search list. 85 * Examples: 86 * -------------------- 87 * fuzzy(["foo", "bar", "baz"])("br"); 88 * // => [FuzzyResult("bar", 1, RedBlackTree([0, 2])), FuzzyResult("baz", 0.592593, RedBlackTree([0])), FuzzyResult("foo", 0, RedBlackTree([]))] 89 * -------------------- 90 */ 91 fuzzyFn fuzzy(string[] db) 92 { 93 94 bonusFn[] bonusFns = [ 95 &previousCharBonus, &startBonus, &caseMatchBonus, &wordBoundaryBonus 96 ]; 97 98 double charScore(Input input) 99 { 100 return input.isMatch ? reduce!((acc, f) => acc + f(input))(1.0, bonusFns) : 0; 101 } 102 103 FuzzyResult score(string input, string pattern) 104 { 105 double score = 0; 106 double simpleMatchScore = 0; 107 double[][] scoreMatrix = new double[][](input.length, pattern.length); 108 auto matches = redBlackTree!int(); 109 110 int row, col; 111 foreach (p; pattern.byCodePoint) 112 { 113 row = 0; 114 foreach (i; input.byCodePoint) 115 { 116 const charScore = charScore(Input(input, pattern, i, p, col, row, scoreMatrix)); 117 if (charScore > 0) 118 matches.insert(row); 119 if (charScore is 1.0) 120 simpleMatchScore += 1; 121 else 122 score += charScore; 123 scoreMatrix[row][col] = charScore; 124 row++; 125 } 126 col++; 127 } 128 129 const totalScore = score + (simpleMatchScore / 2.0); 130 return FuzzyResult(input, totalScore, matches); 131 } 132 133 FuzzyResult[] search(string pattern) 134 { 135 auto maxpq = BinaryHeap!(FuzzyResult[], "a.score < b.score")(new FuzzyResult[db.length], 0); 136 foreach (e; db) 137 { 138 maxpq.insert(score(e, pattern)); 139 } 140 return normalize(maxpq.array()); 141 } 142 143 return &search; 144 }