1 module fuzzyd;
2 
3 import std.stdio;
4 import std.array;
5 import std.container.rbtree;
6 import std.container.binaryheap;
7 import std.ascii;
8 import std.algorithm.iteration;
9 
10 alias Result[] delegate(string) scoreFn;
11 alias int function(Input) bonusFn;
12 
13 export struct Result {
14   string value;
15   int score;
16   int[] matches;
17 }
18 
19 struct Input {
20   string input;
21   string pattern;
22   int col;
23   int row;
24   int[][] scoreMatrix;
25 
26   char inputAtIndex() {
27     return input[row];
28   }
29 
30   char patternAtIndex() {
31     return pattern[col];
32   }
33 
34   char isMatch() {
35     return toLower(inputAtIndex) == toLower(patternAtIndex);
36   }
37 
38   char isCaseSensitiveMatch() {
39     return isUpper(inputAtIndex) && isUpper(patternAtIndex) && isMatch ? 1 : 0;
40   }
41 }
42 
43 int previousCharBonus(Input input) {
44   return (input.col > 0 && input.row > 0) ?
45     2 * input.scoreMatrix[input.row-1][input.col-1] : 0;
46 }
47 
48 int startBonus(Input input) {
49   return (input.col == 0 && input.row == 0) ? 1 : 0;
50 }
51 
52 int caseMatchBonus(Input input) {
53   return input.isCaseSensitiveMatch ? 1 : 0;
54 }
55 
56 int wordBoundaryBonus(Input input) {
57   bool isInputAt = input.row == 0 ||
58     input.row == input.input.length-1 ||
59     isWhite(input.input[input.row-1]) ||
60     isWhite(input.input[input.row+1]);
61   return isInputAt ? 1 : 0;
62 }
63 
64 /**
65  * Fuzzy search
66  * Params:
67  *   db = Array of string containing the search list.
68  * Examples:
69  * --------------------
70  * auto f = fuzzy(["foo", "bar", "baz"]);
71  * f("br")
72  * // => [Result("bar", 5, [0, 2]), Result("baz", 3, [0]), Result("foo", 0, [])]
73  * --------------------
74  */
75 export scoreFn fuzzy(string[] db) {
76 
77   bonusFn[] bonusFns = [&previousCharBonus, &startBonus, &caseMatchBonus, &wordBoundaryBonus];
78 
79   int charScore(Input input) {
80     return input.isMatch ?
81       reduce!((acc, f) => acc + f(input))(1, bonusFns) : 0;
82   }
83 
84   Result score(string input, string pattern) {
85     int score = 0;
86     auto matches = redBlackTree!int();
87     int[][] scoreMatrix = new int[][](input.length, pattern.length);
88 
89     for (int col = 0; col < pattern.length; col++) {
90       for (int row = 0; row < input.length; row++) {
91         int charScore = charScore(Input(input, pattern, col, row, scoreMatrix));
92         if (charScore > 0) matches.insert(row);
93         score += charScore;
94         scoreMatrix[row][col] = charScore;
95       }
96     }
97 
98     return Result(input, score, matches.array());
99   }
100 
101   Result[] search(string pattern) {
102     auto maxpq = BinaryHeap!(Result[], "a.score < b.score")(new Result[db.length], 0);
103     foreach(e; db) {
104       maxpq.insert(score(e, pattern));
105     }
106     return maxpq.array();
107   }
108 
109   return &search;
110 }