1use arcweight::prelude::*;
24use std::collections::HashMap;
25
26fn build_dictionary_fst(words: &[&str]) -> VectorFst<TropicalWeight> {
34 let mut fst = VectorFst::new();
35 let start = fst.add_state();
36 fst.set_start(start);
37
38 let mut state_map: HashMap<Vec<char>, u32> = HashMap::new();
40 state_map.insert(vec![], start);
41
42 for word in words {
43 let chars: Vec<char> = word.chars().collect();
44 let mut prefix = vec![];
45
46 for (i, &ch) in chars.iter().enumerate() {
47 let current_state = *state_map.get(&prefix).unwrap();
48 prefix.push(ch);
49
50 if !state_map.contains_key(&prefix) {
51 let new_state = fst.add_state();
52 state_map.insert(prefix.clone(), new_state);
53 fst.add_arc(
54 current_state,
55 Arc::new(ch as u32, ch as u32, TropicalWeight::one(), new_state),
56 );
57 }
58
59 if i == chars.len() - 1 {
61 let final_state = *state_map.get(&prefix).unwrap();
62 fst.set_final(final_state, TropicalWeight::one());
63 }
64 }
65 }
66
67 fst
68}
69
70fn build_edit_distance_fst(target: &str, k: usize) -> VectorFst<TropicalWeight> {
79 let mut fst = VectorFst::new();
80 let target_chars: Vec<char> = target.chars().collect();
81 let n = target_chars.len();
82
83 let mut states = vec![vec![]; n + 1];
85 for (i, state_row) in states.iter_mut().enumerate().take(n + 1) {
86 for _j in 0..=k.min(i + k) {
87 state_row.push(fst.add_state());
88 }
89 }
90
91 fst.set_start(states[0][0]);
93
94 for j in 0..=k.min(n + k) {
96 if j < states[n].len() {
97 fst.set_final(states[n][j], TropicalWeight::new(j as f32));
98 }
99 }
100
101 for i in 0..n {
103 for j in 0..states[i].len() {
104 if j > i + k {
105 continue; }
107
108 let current = states[i][j];
109
110 if j < states[i + 1].len() {
112 fst.add_arc(
113 current,
114 Arc::new(
115 target_chars[i] as u32,
116 target_chars[i] as u32,
117 TropicalWeight::one(),
118 states[i + 1][j],
119 ),
120 );
121 }
122
123 if j < k {
125 if j + 1 < states[i + 1].len() {
127 for c in b'a'..=b'z' {
128 if c as char != target_chars[i] {
129 fst.add_arc(
130 current,
131 Arc::new(
132 c as u32,
133 c as u32,
134 TropicalWeight::new(1.0),
135 states[i + 1][j + 1],
136 ),
137 );
138 }
139 }
140 }
141
142 if j + 1 < states[i + 1].len() {
144 fst.add_arc(
145 current,
146 Arc::new(
147 0, 0, TropicalWeight::new(1.0),
150 states[i + 1][j + 1],
151 ),
152 );
153 }
154
155 if j + 1 < states[i].len() {
157 for c in b'a'..=b'z' {
158 fst.add_arc(
159 current,
160 Arc::new(
161 c as u32,
162 c as u32,
163 TropicalWeight::new(1.0),
164 states[i][j + 1],
165 ),
166 );
167 }
168 }
169 }
170 }
171 }
172
173 for j in 0..states[n].len() {
175 if j < k && j + 1 < states[n].len() {
176 let current = states[n][j];
177 for c in b'a'..=b'z' {
178 fst.add_arc(
179 current,
180 Arc::new(
181 c as u32,
182 c as u32,
183 TropicalWeight::new(1.0),
184 states[n][j + 1],
185 ),
186 );
187 }
188 }
189 }
190
191 fst
192}
193
194fn find_spelling_corrections(
196 dict_fst: &VectorFst<TropicalWeight>,
197 target: &str,
198 max_distance: usize,
199) -> Result<Vec<(String, f32)>> {
200 let edit_fst = build_edit_distance_fst(target, max_distance);
202
203 let composed: VectorFst<TropicalWeight> = compose_default(dict_fst, &edit_fst)?;
205
206 let config = ShortestPathConfig {
208 nshortest: 10,
209 ..Default::default()
210 };
211 let shortest: VectorFst<TropicalWeight> = shortest_path(&composed, config)?;
212
213 let mut results = Vec::new();
215
216 if let Some(start) = shortest.start() {
217 extract_paths(&shortest, start, &mut Vec::new(), 0.0, &mut results);
218 }
219
220 let mut word_scores: HashMap<String, f32> = HashMap::new();
222 for (word, score) in results {
223 word_scores
224 .entry(word)
225 .and_modify(|e| *e = e.min(score))
226 .or_insert(score);
227 }
228
229 let mut final_results: Vec<(String, f32)> = word_scores.into_iter().collect();
231 final_results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
232 Ok(final_results)
233}
234
235fn extract_paths(
237 fst: &VectorFst<TropicalWeight>,
238 state: u32,
239 path: &mut Vec<char>,
240 cost: f32,
241 results: &mut Vec<(String, f32)>,
242) {
243 if fst.is_final(state) {
244 let word: String = path.iter().collect();
245 if let Some(weight) = fst.final_weight(state) {
246 results.push((word, cost + weight.value()));
247 }
248 }
249
250 for arc in fst.arcs(state) {
251 if arc.olabel != 0 {
252 path.push(arc.olabel as u8 as char);
253 extract_paths(fst, arc.nextstate, path, cost + arc.weight.value(), results);
254 path.pop();
255 } else {
256 extract_paths(fst, arc.nextstate, path, cost + arc.weight.value(), results);
257 }
258 }
259}
260
261fn main() -> Result<()> {
262 println!("Spell Checking Example");
263 println!("======================\n");
264
265 let dictionary = vec![
267 "hello",
268 "world",
269 "help",
270 "held",
271 "hell",
272 "hold",
273 "hero",
274 "here",
275 "hear",
276 "heap",
277 "heal",
278 "health",
279 "helm",
280 "helps",
281 "friend",
282 "friends",
283 "friendship",
284 "friendly",
285 "fresh",
286 "spell",
287 "spelling",
288 "spelled",
289 "spells",
290 "special",
291 "check",
292 "checking",
293 "checked",
294 "checker",
295 "checks",
296 "correct",
297 "correction",
298 "corrected",
299 "correctly",
300 "corrects",
301 "example",
302 "examples",
303 "exemplary",
304 "exempt",
305 "exemplify",
306 ];
307
308 let dictionary_len = dictionary.len();
309 println!("Dictionary contains {dictionary_len} words\n");
310
311 let dict_fst = build_dictionary_fst(&dictionary);
313
314 let test_words = vec![
316 ("helo", 2), ("wrold", 2), ("frend", 2), ("chekc", 2), ("speling", 2), ("corect", 2), ("exmple", 2), ("healht", 2), ];
325
326 for (misspelled, max_distance) in test_words {
327 println!(
328 "Finding spelling corrections for '{misspelled}' (max edit distance: {max_distance}):"
329 );
330 let line = "-".repeat(50);
331 println!("{line}");
332
333 let corrections = find_spelling_corrections(&dict_fst, misspelled, max_distance)?;
334
335 if corrections.is_empty() {
336 println!(" No spelling corrections found within edit distance {max_distance}");
337 } else {
338 for (word, distance) in corrections.iter().take(5) {
339 println!(" {word} (distance: {distance})");
340 }
341 }
342 println!();
343 }
344
345 println!("\nWords within edit distance 1 of 'help':");
347 let line = "=".repeat(40);
348 println!("{line}");
349
350 let corrections = find_spelling_corrections(&dict_fst, "help", 1)?;
351 for (word, distance) in corrections {
352 if distance <= 1.0 {
353 println!(" {word} (distance: {distance})");
354 }
355 }
356
357 println!("\n\nEffect of different edit distances for 'wrld':");
359 let line = "=".repeat(50);
360 println!("{line}");
361
362 for k in 1..=3 {
363 println!("\nEdit distance <= {k}:");
364 let corrections = find_spelling_corrections(&dict_fst, "wrld", k)?;
365 for (word, distance) in corrections.iter().take(5) {
366 println!(" {word} (distance: {distance})");
367 }
368 }
369
370 Ok(())
371}