1use arcweight::prelude::*;
20
21fn build_edit_distance_fst(
34 source: &str,
35 target: &str,
36 insertion_cost: f32,
37 deletion_cost: f32,
38 substitution_cost: f32,
39) -> VectorFst<TropicalWeight> {
40 let mut fst = VectorFst::new();
41 let source_chars: Vec<char> = source.chars().collect();
42 let target_chars: Vec<char> = target.chars().collect();
43 let m = source_chars.len();
44 let n = target_chars.len();
45
46 let mut states = vec![vec![]; m + 1];
49 for state_row in states.iter_mut().take(m + 1) {
50 for _j in 0..=n {
51 state_row.push(fst.add_state());
52 }
53 }
54
55 fst.set_start(states[0][0]);
57
58 fst.set_final(states[m][n], TropicalWeight::one());
60
61 #[allow(clippy::needless_range_loop)] for i in 0..=m {
64 for j in 0..=n {
65 let current = states[i][j];
66
67 if i < m && j < n {
69 let cost = if source_chars[i] == target_chars[j] {
70 0.0 } else {
72 substitution_cost };
74 fst.add_arc(
75 current,
76 Arc::new(
77 source_chars[i] as u32,
78 target_chars[j] as u32,
79 TropicalWeight::new(cost),
80 states[i + 1][j + 1],
81 ),
82 );
83 }
84
85 if i < m {
87 fst.add_arc(
88 current,
89 Arc::new(
90 source_chars[i] as u32,
91 0, TropicalWeight::new(deletion_cost),
93 states[i + 1][j],
94 ),
95 );
96 }
97
98 if j < n {
100 fst.add_arc(
101 current,
102 Arc::new(
103 0, target_chars[j] as u32,
105 TropicalWeight::new(insertion_cost),
106 states[i][j + 1],
107 ),
108 );
109 }
110 }
111 }
112
113 fst
114}
115
116fn compute_edit_distance(
118 source: &str,
119 target: &str,
120 insertion_cost: f32,
121 deletion_cost: f32,
122 substitution_cost: f32,
123) -> Result<f32> {
124 let edit_fst = build_edit_distance_fst(
126 source,
127 target,
128 insertion_cost,
129 deletion_cost,
130 substitution_cost,
131 );
132
133 let config = ShortestPathConfig::default();
135 let shortest: VectorFst<TropicalWeight> = shortest_path(&edit_fst, config)?;
136
137 if let Some(start) = shortest.start() {
139 let mut stack = vec![(start, 0.0)];
141 let mut visited = std::collections::HashSet::new();
142
143 while let Some((state, cost)) = stack.pop() {
144 if visited.contains(&state) {
145 continue;
146 }
147 visited.insert(state);
148
149 if shortest.is_final(state) {
150 return Ok(cost);
151 }
152
153 for arc in shortest.arcs(state) {
154 stack.push((arc.nextstate, cost + arc.weight.value()));
155 }
156 }
157 }
158
159 Ok(f32::INFINITY)
161}
162
163fn main() -> Result<()> {
164 println!("Edit Distance Computation Example");
165 println!("=================================\n");
166
167 let test_pairs = vec![
169 ("kitten", "sitting"),
170 ("saturday", "sunday"),
171 ("hello", "hallo"),
172 ("abc", "abc"),
173 ("abc", "def"),
174 ("", "abc"),
175 ("abc", ""),
176 ];
177
178 println!("1. Standard Levenshtein Distance (all operations cost 1.0):");
180 println!("-----------------------------------------------------------");
181 for (source, target) in &test_pairs {
182 let distance = compute_edit_distance(source, target, 1.0, 1.0, 1.0)?;
183 if distance.is_finite() {
184 println!(" '{source}' -> '{target}': {distance}");
185 } else {
186 println!(" '{source}' -> '{target}': No transformation found");
187 }
188 }
189
190 println!("\n2. Custom Weights (insert=0.5, delete=2.0, substitute=1.0):");
192 println!("------------------------------------------------------------");
193 for (source, target) in &test_pairs {
194 let distance = compute_edit_distance(source, target, 0.5, 2.0, 1.0)?;
195 if distance.is_finite() {
196 println!(" '{source}' -> '{target}': {distance}");
197 } else {
198 println!(" '{source}' -> '{target}': No transformation found");
199 }
200 }
201
202 println!("\n3. Expensive Substitutions (insert=1.0, delete=1.0, substitute=3.0):");
204 println!("--------------------------------------------------------------------");
205 for (source, target) in &test_pairs {
206 let distance = compute_edit_distance(source, target, 1.0, 1.0, 3.0)?;
207 if distance.is_finite() {
208 println!(" '{source}' -> '{target}': {distance}");
209 } else {
210 println!(" '{source}' -> '{target}': No transformation found");
211 }
212 }
213
214 println!("\n4. Detailed Analysis: 'kitten' -> 'sitting'");
216 println!("--------------------------------------------");
217 println!("This transformation requires:");
218 println!("- Substitute 'k' -> 's'");
219 println!("- Insert 'i' after 's'");
220 println!("- Keep 'itten' -> 'itting' (substitute 'e' -> 'i')");
221 println!("- Add 'g' at the end\n");
222
223 let configs = vec![
224 ("Uniform weights", 1.0, 1.0, 1.0),
225 ("Cheap insertions", 0.5, 1.0, 1.0),
226 ("Cheap deletions", 1.0, 0.5, 1.0),
227 ("Cheap substitutions", 1.0, 1.0, 0.5),
228 ];
229
230 for (name, ins, del, sub) in configs {
231 let distance = compute_edit_distance("kitten", "sitting", ins, del, sub)?;
232 if distance.is_finite() {
233 println!(" {name}: {distance} (ins={ins}, del={del}, sub={sub})");
234 } else {
235 println!(" {name}: No transformation found (ins={ins}, del={del}, sub={sub})");
236 }
237 }
238
239 println!("\n5. Step-by-step transformations:");
241 println!("---------------------------------");
242 let examples = vec![
243 ("cat", "cut", "Substitute 'a' -> 'u'"),
244 ("cat", "cats", "Insert 's' at end"),
245 ("cats", "cat", "Delete 's' from end"),
246 ("cat", "dog", "Replace all characters"),
247 ];
248
249 for (source, target, description) in examples {
250 let distance = compute_edit_distance(source, target, 1.0, 1.0, 1.0)?;
251 println!(" '{source}' -> '{target}': {description} (distance: {distance})");
252 }
253
254 Ok(())
255}