1 /**
2  * Spelling suggestion based on the Spelling Corrector found at:
3  * norvig.com/spell-correct.html
4  * 
5  * A word list, big.txt can also be downloaded from the website:
6  * norvig.com/big.txt
7  *
8  * Synopsis:
9  * --------
10  * buildList(std.file.readText("big.txt").replace(regex(".?!"), " ").split);
11  *
12  * if(misspelled(args[1]))
13  *     std.stdio.writeln(giveWord(args[1]));
14  * --------
15  */
16 
17 module spelling.suggestion;
18 
19 import std.algorithm;
20 import std.array;
21 import std.file;
22 import std.functional;
23 import std.regex;
24 import std.string;
25 static import std.math;
26 
27 private int[string] model;
28 /**
29  * Give a suggested word based the given word.
30  *
31  * The selection is based on the fequency of words per the training
32  * and the edit distance of the given word to the suggestion.
33  * If there are no known words within an edit distance of two
34  * the given word is returned unchanged.
35  *
36  * The word with the fewest edits is returned unless the predicate is true.
37  * Where a is the fequency of edit distance one and b is the frequency of
38  * edit distance two.
39  */
40 public string giveWord(alias predicate = "a <= 1 && b > 4")(string word) {
41 	auto candy = candidates(word);
42 	if(candy[0].empty && candy[1].empty) return word;
43 
44 	auto ld1 = minPos!((a,b) => model[a] > model[b])(candy[0]);
45 	auto ld2 = minPos!((a,b) => model[a] > model[b])(candy[1]);
46 	if(ld1.empty) return ld2.front;
47 	if(ld2.empty) return ld1.front;
48 
49 	if(binaryFun!predicate(model[ld1.front], model[ld2.front]))
50 		return ld2.front;
51 
52 	return ld1.front;
53 }
54 
55 unittest {
56     scope(exit) model = null;
57     buildList(["hello","hello","held","hells","electronic"]);
58     auto hello = giveWord("helo");
59     assert(hello == "hello");
60     auto electronic = giveWord("ellectonic");
61     assert(electronic == "electronic");
62     auto uknwon = giveWord("uknwon");
63     assert(uknwon == "uknwon");
64 }
65 
66 /**
67  * Takes a Range of words and adds them to the dictionary
68  * while at the same time increasing frequency of use.
69  *
70  * This is similar to training the dictionary, but it assumes
71  * all words are spelled correctly.
72  */
73 public void buildList(Range)(Range words) {
74 	foreach(w; words)
75 		model[w] += 1;
76 }
77 
78 /**
79  * Takes a list of words and increases the frequency of use
80  * only if the word is spelled correctly
81  *
82  * You must first add words with buildList for this to work
83  * it does not add new words to the list.
84  */
85 public void train(Range)(Range words) {
86 	foreach(w; words)
87 		if(!misspelled(w))
88 			model[w] += 1;
89 }
90 
91 /// Tells if a given word is misspelled
92 public bool misspelled(string word) {
93 	if(word.empty) return false;
94 	if(word in model)
95 		return false;
96 	return true;
97 }
98 
99 unittest {
100     assert(!misspelled(""));
101     assert(!misspelled(null));
102 }
103 
104 /**
105  * Returns two arrays of words that could be used for spelling correction.
106  * The first list is of edit distence of one and the second of edit distance
107  * of two.
108  */
109 private string[][] candidates(string word) {
110 	string[][] candy = new string[][2];
111 	if(!misspelled(word))
112         return candy;
113     foreach(w, i; model) {
114         if(std.math.abs(cast(long)word.length - cast(long)w.length) > 2)
115         { /* a length difference greater than 2 is more than 2 edits */ }
116         else {
117             auto dist = levenshteinDistance(w, word);
118             if(dist == 1)
119                 candy[0] ~= w;
120             if(dist == 2)
121                 candy[1] ~= w;
122         }
123     }
124 	return candy;
125 }
126 
127 unittest {
128     scope(exit) model = null;
129     buildList(["hello","held","hells"]);
130     auto candy = candidates("helo");
131     assert(candy[0].sort.equal(["hello", "held"].sort));
132     assert(candy[1].equal(["hells"]));
133 }
134 
135 version(spelling_main) {
136 import std.stdio;
137 import std.datetime;
138 
139 alias std.regex.split split;
140 
141 	void main(string[] args) {
142 		if(args.length < 2) {
143 			std.stdio.writeln("Please pass a word to check");
144 			return;
145 		}
146 
147         StopWatch sw;
148         sw.start();
149 		buildList(std.file.readText("big.txt").toLower().match(regex(r"\w+", "g")).map!(a => a.hit)());
150 
151 		writefln("Build %s msecs", sw.peek().msecs);
152 
153         sw.reset();
154         sw.start();
155 		if(misspelled(args[1]))
156 			std.stdio.writeln(giveWord(args[1]));
157 		else
158 			std.stdio.writeln("Why you little");
159 
160 		writefln("Suggest %s msec", sw.peek().msecs);
161 	}
162 }