Skip to main content

detcore/edit_distance/
damerau_levenshtein.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use core::hash::Hash;
10use std::collections::HashMap;
11
12fn index(row_length: usize, row: usize, col: usize) -> usize {
13    row_length * row + col
14}
15
16fn longest_common_prefix<T: PartialEq>(a: &[T], b: &[T]) -> usize {
17    let mut count = 0;
18
19    let mut first = a.iter();
20    let mut second = b.iter();
21
22    while first.next() == second.next() {
23        count += 1;
24    }
25
26    count
27}
28
29fn longest_common_suffix<T: PartialEq>(a: &[T], b: &[T]) -> usize {
30    let mut count = 0;
31
32    let mut first = a.iter().rev();
33    let mut second = b.iter().rev();
34
35    while first.next() == second.next() {
36        count += 1;
37    }
38
39    count
40}
41
42/// Finds the edit distance between `source` and `target`, taking into account
43/// additions, deletions, and swaps. This has a time complexity of O(n^2) and a
44/// space complexity of O(n^2).
45pub fn damerau_lev<T>(mut source: &[T], mut target: &[T]) -> usize
46where
47    T: PartialEq + Hash + Eq,
48{
49    let prefix = longest_common_prefix(source, target);
50    source = &source[prefix..];
51    target = &target[prefix..];
52
53    let suffix = longest_common_suffix(source, target);
54    source = &source[..source.len() - suffix];
55    target = &target[..target.len() - suffix];
56
57    let m = source.len();
58    let n = target.len();
59
60    if m == 0 {
61        return n;
62    }
63    if n == 0 {
64        return m;
65    }
66
67    let width = n + 2;
68    let height = m + 2;
69
70    let mut matrix: Vec<usize> = vec![0; width * height];
71
72    let inf: usize = m + n;
73
74    matrix[0] = inf;
75    for i in 0..=m {
76        matrix[index(width, i + 1, 1)] = i;
77        matrix[index(width, i + 1, 0)] = inf;
78    }
79
80    for j in 0..=n {
81        matrix[index(width, 1, j + 1)] = j;
82        matrix[index(width, 0, j + 1)] = inf;
83    }
84
85    let mut last_row = HashMap::<&T, usize>::new();
86
87    for row in 1..=m {
88        let ch_s = &source[row - 1];
89        let mut last_match_col = 0;
90
91        for col in 1..=n {
92            let ch_t = &target[col - 1];
93            let last_match_row = *last_row.get(&ch_t).unwrap_or(&0);
94
95            #[allow(clippy::bool_to_int_with_if)]
96            let cost = if ch_s == ch_t { 0 } else { 1 };
97
98            let dist_add = matrix[index(width, row, col + 1)] + 1;
99            let dist_del = matrix[index(width, row + 1, col)] + 1;
100            let dist_sub = matrix[index(width, row, col)] + cost;
101
102            let dist_trans = matrix[index(width, last_match_row, last_match_col)]
103                + (row - last_match_row - 1)
104                + 1
105                + (col - last_match_col - 1);
106
107            // Find the minimum of the distances
108            let mut min = dist_add;
109            if dist_del < min {
110                min = dist_del;
111            }
112            if dist_sub < min {
113                min = dist_sub;
114            }
115            if dist_trans < min {
116                min = dist_trans;
117            }
118
119            // Store the minimum as the cost for this cell
120            matrix[index(width, row + 1, col + 1)] = min;
121
122            // If there was a match, update the last matching column
123            if cost == 0 {
124                last_match_col = col;
125            }
126        }
127
128        // Update the entry for this row's character
129        last_row.insert(ch_s, row);
130    }
131
132    // Extract the bottom right-most cell -- that's the total distance
133    matrix[index(width, m + 1, n + 1)]
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[allow(unused)]
141    fn print_matrix(matrix: &[usize], cols: usize) {
142        let rows = matrix.len() / cols;
143        for i in 0..rows {
144            for j in 0..cols {
145                print!("{:#3}, ", matrix[index(cols, i, j)]);
146            }
147            println!();
148        }
149        println!();
150    }
151
152    // Test cases
153    //  * Two long strings with only one difference at the end
154    //  * One long, one short
155    //  * Two same-length, shuffled
156
157    #[test]
158    fn longest_common_prefix_test() {
159        let source = vec!['A'; 10_000];
160        let mut target = vec!['A'; 10_000];
161        *target.last_mut().unwrap() = 'B';
162        assert_eq!(longest_common_prefix(&source, &target), 9_999);
163    }
164
165    #[test]
166    fn longest_common_suffix_test() {
167        let source = vec!['A'; 10_000];
168        let mut target = vec!['A'; 10_000];
169        *target.last_mut().unwrap() = 'B';
170        assert_eq!(longest_common_suffix(&source, &target), 0);
171
172        let source = vec!['A'; 10_000];
173        let mut target = vec!['A'; 10_000];
174        *target.first_mut().unwrap() = 'B';
175        assert_eq!(longest_common_suffix(&source, &target), 9_999);
176    }
177
178    #[test]
179    fn smoke_test() {
180        assert_eq!(
181            damerau_lev(&['a', ' ', 'a', 'b', 'c', 't'], &['a', ' ', 'c', 'a', 't'],),
182            2
183        );
184    }
185
186    #[test]
187    fn long_common_prefix_same_len() {
188        let source = vec!['A'; 10_000];
189        let mut target = vec!['A'; 10_000];
190        *target.last_mut().unwrap() = 'B';
191        assert_eq!(damerau_lev(&source, &target), 1);
192    }
193
194    #[test]
195    fn long_common_suffix_same_len() {
196        let source = vec!['A'; 10_000];
197        let mut target = vec!['A'; 10_000];
198        *target.first_mut().unwrap() = 'B';
199        assert_eq!(damerau_lev(&source, &target), 1);
200    }
201
202    #[test]
203    fn long_common_suffix_diff_len() {
204        let source = vec!['A'; 10_000];
205
206        let mut target = vec!['B', 'C'];
207        target.append(&mut vec!['A'; 10_000]);
208
209        assert_eq!(damerau_lev(&source, &target), 2);
210    }
211
212    #[test]
213    fn same_length_shuffled() {
214        use rand::seq::SliceRandom;
215
216        const COUNT: usize = 1_000;
217
218        let mut rng = rand::rng();
219        let mut source: Vec<u8> = Vec::with_capacity(COUNT);
220
221        let mut x = 0;
222        for _ in 0..COUNT {
223            source.push(x);
224            x = x.wrapping_add(1)
225        }
226
227        let target = source.clone();
228
229        source.shuffle(&mut rng);
230
231        assert!(damerau_lev(&source, &target) > 500);
232    }
233}