use arcweight::prelude::*;
use std::collections::HashMap;
fn build_dictionary_fst(words: &[&str]) -> VectorFst<TropicalWeight> {
let mut fst = VectorFst::new();
let start = fst.add_state();
fst.set_start(start);
let mut state_map: HashMap<Vec<char>, u32> = HashMap::new();
state_map.insert(vec![], start);
for word in words {
let chars: Vec<char> = word.chars().collect();
let mut prefix = vec![];
for (i, &ch) in chars.iter().enumerate() {
let current_state = *state_map.get(&prefix).unwrap();
prefix.push(ch);
if !state_map.contains_key(&prefix) {
let new_state = fst.add_state();
state_map.insert(prefix.clone(), new_state);
fst.add_arc(
current_state,
Arc::new(ch as u32, ch as u32, TropicalWeight::one(), new_state),
);
}
if i == chars.len() - 1 {
let final_state = *state_map.get(&prefix).unwrap();
fst.set_final(final_state, TropicalWeight::one());
}
}
}
fst
}
fn build_edit_distance_fst(target: &str, k: usize) -> VectorFst<TropicalWeight> {
let mut fst = VectorFst::new();
let target_chars: Vec<char> = target.chars().collect();
let n = target_chars.len();
let mut states = vec![vec![]; n + 1];
for (i, state_row) in states.iter_mut().enumerate().take(n + 1) {
for _j in 0..=k.min(i + k) {
state_row.push(fst.add_state());
}
}
fst.set_start(states[0][0]);
for j in 0..=k.min(n + k) {
if j < states[n].len() {
fst.set_final(states[n][j], TropicalWeight::new(j as f32));
}
}
for i in 0..n {
for j in 0..states[i].len() {
if j > i + k {
continue; }
let current = states[i][j];
if j < states[i + 1].len() {
fst.add_arc(
current,
Arc::new(
target_chars[i] as u32,
target_chars[i] as u32,
TropicalWeight::one(),
states[i + 1][j],
),
);
}
if j < k {
if j + 1 < states[i + 1].len() {
for c in b'a'..=b'z' {
if c as char != target_chars[i] {
fst.add_arc(
current,
Arc::new(
c as u32,
c as u32,
TropicalWeight::new(1.0),
states[i + 1][j + 1],
),
);
}
}
}
if j + 1 < states[i + 1].len() {
fst.add_arc(
current,
Arc::new(
0, 0, TropicalWeight::new(1.0),
states[i + 1][j + 1],
),
);
}
if j + 1 < states[i].len() {
for c in b'a'..=b'z' {
fst.add_arc(
current,
Arc::new(
c as u32,
c as u32,
TropicalWeight::new(1.0),
states[i][j + 1],
),
);
}
}
}
}
}
for j in 0..states[n].len() {
if j < k && j + 1 < states[n].len() {
let current = states[n][j];
for c in b'a'..=b'z' {
fst.add_arc(
current,
Arc::new(
c as u32,
c as u32,
TropicalWeight::new(1.0),
states[n][j + 1],
),
);
}
}
}
fst
}
fn find_spelling_corrections(
dict_fst: &VectorFst<TropicalWeight>,
target: &str,
max_distance: usize,
) -> Result<Vec<(String, f32)>> {
let edit_fst = build_edit_distance_fst(target, max_distance);
let composed: VectorFst<TropicalWeight> = compose_default(dict_fst, &edit_fst)?;
let config = ShortestPathConfig {
nshortest: 10,
..Default::default()
};
let shortest: VectorFst<TropicalWeight> = shortest_path(&composed, config)?;
let mut results = Vec::new();
if let Some(start) = shortest.start() {
extract_paths(&shortest, start, &mut Vec::new(), 0.0, &mut results);
}
let mut word_scores: HashMap<String, f32> = HashMap::new();
for (word, score) in results {
word_scores
.entry(word)
.and_modify(|e| *e = e.min(score))
.or_insert(score);
}
let mut final_results: Vec<(String, f32)> = word_scores.into_iter().collect();
final_results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
Ok(final_results)
}
fn extract_paths(
fst: &VectorFst<TropicalWeight>,
state: u32,
path: &mut Vec<char>,
cost: f32,
results: &mut Vec<(String, f32)>,
) {
if fst.is_final(state) {
let word: String = path.iter().collect();
if let Some(weight) = fst.final_weight(state) {
results.push((word, cost + weight.value()));
}
}
for arc in fst.arcs(state) {
if arc.olabel != 0 {
path.push(arc.olabel as u8 as char);
extract_paths(fst, arc.nextstate, path, cost + arc.weight.value(), results);
path.pop();
} else {
extract_paths(fst, arc.nextstate, path, cost + arc.weight.value(), results);
}
}
}
fn main() -> Result<()> {
println!("Spell Checking Example");
println!("======================\n");
let dictionary = vec![
"hello",
"world",
"help",
"held",
"hell",
"hold",
"hero",
"here",
"hear",
"heap",
"heal",
"health",
"helm",
"helps",
"friend",
"friends",
"friendship",
"friendly",
"fresh",
"spell",
"spelling",
"spelled",
"spells",
"special",
"check",
"checking",
"checked",
"checker",
"checks",
"correct",
"correction",
"corrected",
"correctly",
"corrects",
"example",
"examples",
"exemplary",
"exempt",
"exemplify",
];
let dictionary_len = dictionary.len();
println!("Dictionary contains {dictionary_len} words\n");
let dict_fst = build_dictionary_fst(&dictionary);
let test_words = vec![
("helo", 2), ("wrold", 2), ("frend", 2), ("chekc", 2), ("speling", 2), ("corect", 2), ("exmple", 2), ("healht", 2), ];
for (misspelled, max_distance) in test_words {
println!(
"Finding spelling corrections for '{misspelled}' (max edit distance: {max_distance}):"
);
let line = "-".repeat(50);
println!("{line}");
let corrections = find_spelling_corrections(&dict_fst, misspelled, max_distance)?;
if corrections.is_empty() {
println!(" No spelling corrections found within edit distance {max_distance}");
} else {
for (word, distance) in corrections.iter().take(5) {
println!(" {word} (distance: {distance})");
}
}
println!();
}
println!("\nWords within edit distance 1 of 'help':");
let line = "=".repeat(40);
println!("{line}");
let corrections = find_spelling_corrections(&dict_fst, "help", 1)?;
for (word, distance) in corrections {
if distance <= 1.0 {
println!(" {word} (distance: {distance})");
}
}
println!("\n\nEffect of different edit distances for 'wrld':");
let line = "=".repeat(50);
println!("{line}");
for k in 1..=3 {
println!("\nEdit distance <= {k}:");
let corrections = find_spelling_corrections(&dict_fst, "wrld", k)?;
for (word, distance) in corrections.iter().take(5) {
println!(" {word} (distance: {distance})");
}
}
Ok(())
}