/rust/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/src/lib.rs
Line | Count | Source |
1 | | //! This library implements string similarity metrics. |
2 | | |
3 | | #![forbid(unsafe_code)] |
4 | | #![allow( |
5 | | // these casts are sometimes needed. They restrict the length of input iterators |
6 | | // but there isn't really any way around this except for always working with |
7 | | // 128 bit types |
8 | | clippy::cast_possible_wrap, |
9 | | clippy::cast_sign_loss, |
10 | | clippy::cast_precision_loss, |
11 | | // not practical |
12 | | clippy::needless_pass_by_value, |
13 | | clippy::similar_names, |
14 | | // noisy |
15 | | clippy::missing_errors_doc, |
16 | | clippy::missing_panics_doc, |
17 | | clippy::must_use_candidate, |
18 | | // todo https://github.com/rapidfuzz/strsim-rs/issues/59 |
19 | | clippy::range_plus_one |
20 | | )] |
21 | | |
22 | | use std::char; |
23 | | use std::cmp::{max, min}; |
24 | | use std::collections::HashMap; |
25 | | use std::convert::TryFrom; |
26 | | use std::error::Error; |
27 | | use std::fmt::{self, Display, Formatter}; |
28 | | use std::hash::Hash; |
29 | | use std::mem; |
30 | | use std::str::Chars; |
31 | | |
32 | | #[derive(Debug, PartialEq)] |
33 | | pub enum StrSimError { |
34 | | DifferentLengthArgs, |
35 | | } |
36 | | |
37 | | impl Display for StrSimError { |
38 | 0 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { |
39 | 0 | let text = match self { |
40 | 0 | StrSimError::DifferentLengthArgs => "Differing length arguments provided", |
41 | | }; |
42 | | |
43 | 0 | write!(fmt, "{}", text) |
44 | 0 | } |
45 | | } |
46 | | |
47 | | impl Error for StrSimError {} |
48 | | |
49 | | pub type HammingResult = Result<usize, StrSimError>; |
50 | | |
51 | | /// Calculates the number of positions in the two sequences where the elements |
52 | | /// differ. Returns an error if the sequences have different lengths. |
53 | 0 | pub fn generic_hamming<Iter1, Iter2, Elem1, Elem2>(a: Iter1, b: Iter2) -> HammingResult |
54 | 0 | where |
55 | 0 | Iter1: IntoIterator<Item = Elem1>, |
56 | 0 | Iter2: IntoIterator<Item = Elem2>, |
57 | 0 | Elem1: PartialEq<Elem2>, |
58 | | { |
59 | 0 | let (mut ita, mut itb) = (a.into_iter(), b.into_iter()); |
60 | 0 | let mut count = 0; |
61 | | loop { |
62 | 0 | match (ita.next(), itb.next()) { |
63 | 0 | (Some(x), Some(y)) => { |
64 | 0 | if x != y { |
65 | 0 | count += 1; |
66 | 0 | } |
67 | | } |
68 | 0 | (None, None) => return Ok(count), |
69 | 0 | _ => return Err(StrSimError::DifferentLengthArgs), |
70 | | } |
71 | | } |
72 | 0 | } |
73 | | |
74 | | /// Calculates the number of positions in the two strings where the characters |
75 | | /// differ. Returns an error if the strings have different lengths. |
76 | | /// |
77 | | /// ``` |
78 | | /// use strsim::{hamming, StrSimError::DifferentLengthArgs}; |
79 | | /// |
80 | | /// assert_eq!(Ok(3), hamming("hamming", "hammers")); |
81 | | /// |
82 | | /// assert_eq!(Err(DifferentLengthArgs), hamming("hamming", "ham")); |
83 | | /// ``` |
84 | 0 | pub fn hamming(a: &str, b: &str) -> HammingResult { |
85 | 0 | generic_hamming(a.chars(), b.chars()) |
86 | 0 | } |
87 | | |
88 | | /// Calculates the Jaro similarity between two sequences. The returned value |
89 | | /// is between 0.0 and 1.0 (higher value means more similar). |
90 | 0 | pub fn generic_jaro<'a, 'b, Iter1, Iter2, Elem1, Elem2>(a: &'a Iter1, b: &'b Iter2) -> f64 |
91 | 0 | where |
92 | 0 | &'a Iter1: IntoIterator<Item = Elem1>, |
93 | 0 | &'b Iter2: IntoIterator<Item = Elem2>, |
94 | 0 | Elem1: PartialEq<Elem2>, |
95 | | { |
96 | 0 | let a_len = a.into_iter().count(); |
97 | 0 | let b_len = b.into_iter().count(); |
98 | | |
99 | 0 | if a_len == 0 && b_len == 0 { |
100 | 0 | return 1.0; |
101 | 0 | } else if a_len == 0 || b_len == 0 { |
102 | 0 | return 0.0; |
103 | 0 | } |
104 | | |
105 | 0 | let mut search_range = max(a_len, b_len) / 2; |
106 | 0 | search_range = search_range.saturating_sub(1); |
107 | | |
108 | | // combine memory allocations to reduce runtime |
109 | 0 | let mut flags_memory = vec![false; a_len + b_len]; |
110 | 0 | let (a_flags, b_flags) = flags_memory.split_at_mut(a_len); |
111 | | |
112 | 0 | let mut matches = 0_usize; |
113 | | |
114 | 0 | for (i, a_elem) in a.into_iter().enumerate() { |
115 | | // prevent integer wrapping |
116 | 0 | let min_bound = if i > search_range { |
117 | 0 | i - search_range |
118 | | } else { |
119 | 0 | 0 |
120 | | }; |
121 | | |
122 | 0 | let max_bound = min(b_len, i + search_range + 1); |
123 | | |
124 | 0 | for (j, b_elem) in b.into_iter().enumerate().take(max_bound) { |
125 | 0 | if min_bound <= j && a_elem == b_elem && !b_flags[j] { |
126 | 0 | a_flags[i] = true; |
127 | 0 | b_flags[j] = true; |
128 | 0 | matches += 1; |
129 | 0 | break; |
130 | 0 | } |
131 | | } |
132 | | } |
133 | | |
134 | 0 | let mut transpositions = 0_usize; |
135 | 0 | if matches != 0 { |
136 | 0 | let mut b_iter = b_flags.iter().zip(b); |
137 | 0 | for (a_flag, ch1) in a_flags.iter().zip(a) { |
138 | 0 | if *a_flag { |
139 | | loop { |
140 | 0 | if let Some((b_flag, ch2)) = b_iter.next() { |
141 | 0 | if !*b_flag { |
142 | 0 | continue; |
143 | 0 | } |
144 | | |
145 | 0 | if ch1 != ch2 { |
146 | 0 | transpositions += 1; |
147 | 0 | } |
148 | 0 | break; |
149 | 0 | } |
150 | | } |
151 | 0 | } |
152 | | } |
153 | 0 | } |
154 | 0 | transpositions /= 2; |
155 | | |
156 | 0 | if matches == 0 { |
157 | 0 | 0.0 |
158 | | } else { |
159 | 0 | ((matches as f64 / a_len as f64) |
160 | 0 | + (matches as f64 / b_len as f64) |
161 | 0 | + ((matches - transpositions) as f64 / matches as f64)) |
162 | 0 | / 3.0 |
163 | | } |
164 | 0 | } |
165 | | |
166 | | struct StringWrapper<'a>(&'a str); |
167 | | |
168 | | impl<'a, 'b> IntoIterator for &'a StringWrapper<'b> { |
169 | | type Item = char; |
170 | | type IntoIter = Chars<'b>; |
171 | | |
172 | 0 | fn into_iter(self) -> Self::IntoIter { |
173 | 0 | self.0.chars() |
174 | 0 | } |
175 | | } |
176 | | |
177 | | /// Calculates the Jaro similarity between two strings. The returned value |
178 | | /// is between 0.0 and 1.0 (higher value means more similar). |
179 | | /// |
180 | | /// ``` |
181 | | /// use strsim::jaro; |
182 | | /// |
183 | | /// assert!((0.392 - jaro("Friedrich Nietzsche", "Jean-Paul Sartre")).abs() < |
184 | | /// 0.001); |
185 | | /// ``` |
186 | 0 | pub fn jaro(a: &str, b: &str) -> f64 { |
187 | 0 | generic_jaro(&StringWrapper(a), &StringWrapper(b)) |
188 | 0 | } |
189 | | |
190 | | /// Like Jaro but gives a boost to sequences that have a common prefix. |
191 | 0 | pub fn generic_jaro_winkler<'a, 'b, Iter1, Iter2, Elem1, Elem2>(a: &'a Iter1, b: &'b Iter2) -> f64 |
192 | 0 | where |
193 | 0 | &'a Iter1: IntoIterator<Item = Elem1>, |
194 | 0 | &'b Iter2: IntoIterator<Item = Elem2>, |
195 | 0 | Elem1: PartialEq<Elem2>, |
196 | | { |
197 | 0 | let sim = generic_jaro(a, b); |
198 | | |
199 | 0 | if sim > 0.7 { |
200 | 0 | let prefix_length = a |
201 | 0 | .into_iter() |
202 | 0 | .take(4) |
203 | 0 | .zip(b) |
204 | 0 | .take_while(|(a_elem, b_elem)| a_elem == b_elem) |
205 | 0 | .count(); |
206 | | |
207 | 0 | sim + 0.1 * prefix_length as f64 * (1.0 - sim) |
208 | | } else { |
209 | 0 | sim |
210 | | } |
211 | 0 | } |
212 | | |
213 | | /// Like Jaro but gives a boost to strings that have a common prefix. |
214 | | /// |
215 | | /// ``` |
216 | | /// use strsim::jaro_winkler; |
217 | | /// |
218 | | /// assert!((0.866 - jaro_winkler("cheeseburger", "cheese fries")).abs() < |
219 | | /// 0.001); |
220 | | /// ``` |
221 | 0 | pub fn jaro_winkler(a: &str, b: &str) -> f64 { |
222 | 0 | generic_jaro_winkler(&StringWrapper(a), &StringWrapper(b)) |
223 | 0 | } |
224 | | |
225 | | /// Calculates the minimum number of insertions, deletions, and substitutions |
226 | | /// required to change one sequence into the other. |
227 | | /// |
228 | | /// ``` |
229 | | /// use strsim::generic_levenshtein; |
230 | | /// |
231 | | /// assert_eq!(3, generic_levenshtein(&[1,2,3], &[1,2,3,4,5,6])); |
232 | | /// ``` |
233 | 0 | pub fn generic_levenshtein<'a, 'b, Iter1, Iter2, Elem1, Elem2>(a: &'a Iter1, b: &'b Iter2) -> usize |
234 | 0 | where |
235 | 0 | &'a Iter1: IntoIterator<Item = Elem1>, |
236 | 0 | &'b Iter2: IntoIterator<Item = Elem2>, |
237 | 0 | Elem1: PartialEq<Elem2>, |
238 | | { |
239 | 0 | let b_len = b.into_iter().count(); |
240 | | |
241 | 0 | let mut cache: Vec<usize> = (1..b_len + 1).collect(); |
242 | | |
243 | 0 | let mut result = b_len; |
244 | | |
245 | 0 | for (i, a_elem) in a.into_iter().enumerate() { |
246 | 0 | result = i + 1; |
247 | 0 | let mut distance_b = i; |
248 | | |
249 | 0 | for (j, b_elem) in b.into_iter().enumerate() { |
250 | 0 | let cost = usize::from(a_elem != b_elem); |
251 | 0 | let distance_a = distance_b + cost; |
252 | 0 | distance_b = cache[j]; |
253 | 0 | result = min(result + 1, min(distance_a, distance_b + 1)); |
254 | 0 | cache[j] = result; |
255 | 0 | } |
256 | | } |
257 | | |
258 | 0 | result |
259 | 0 | } |
260 | | |
261 | | /// Calculates the minimum number of insertions, deletions, and substitutions |
262 | | /// required to change one string into the other. |
263 | | /// |
264 | | /// ``` |
265 | | /// use strsim::levenshtein; |
266 | | /// |
267 | | /// assert_eq!(3, levenshtein("kitten", "sitting")); |
268 | | /// ``` |
269 | 0 | pub fn levenshtein(a: &str, b: &str) -> usize { |
270 | 0 | generic_levenshtein(&StringWrapper(a), &StringWrapper(b)) |
271 | 0 | } |
272 | | |
273 | | /// Calculates a normalized score of the Levenshtein algorithm between 0.0 and |
274 | | /// 1.0 (inclusive), where 1.0 means the strings are the same. |
275 | | /// |
276 | | /// ``` |
277 | | /// use strsim::normalized_levenshtein; |
278 | | /// |
279 | | /// assert!((normalized_levenshtein("kitten", "sitting") - 0.57142).abs() < 0.00001); |
280 | | /// assert!((normalized_levenshtein("", "") - 1.0).abs() < 0.00001); |
281 | | /// assert!(normalized_levenshtein("", "second").abs() < 0.00001); |
282 | | /// assert!(normalized_levenshtein("first", "").abs() < 0.00001); |
283 | | /// assert!((normalized_levenshtein("string", "string") - 1.0).abs() < 0.00001); |
284 | | /// ``` |
285 | 0 | pub fn normalized_levenshtein(a: &str, b: &str) -> f64 { |
286 | 0 | if a.is_empty() && b.is_empty() { |
287 | 0 | return 1.0; |
288 | 0 | } |
289 | 0 | 1.0 - (levenshtein(a, b) as f64) / (a.chars().count().max(b.chars().count()) as f64) |
290 | 0 | } |
291 | | |
292 | | /// Like Levenshtein but allows for adjacent transpositions. Each substring can |
293 | | /// only be edited once. |
294 | | /// |
295 | | /// ``` |
296 | | /// use strsim::osa_distance; |
297 | | /// |
298 | | /// assert_eq!(3, osa_distance("ab", "bca")); |
299 | | /// ``` |
300 | 0 | pub fn osa_distance(a: &str, b: &str) -> usize { |
301 | 0 | let b_len = b.chars().count(); |
302 | | // 0..=b_len behaves like 0..b_len.saturating_add(1) which could be a different size |
303 | | // this leads to significantly worse code gen when swapping the vectors below |
304 | 0 | let mut prev_two_distances: Vec<usize> = (0..b_len + 1).collect(); |
305 | 0 | let mut prev_distances: Vec<usize> = (0..b_len + 1).collect(); |
306 | 0 | let mut curr_distances: Vec<usize> = vec![0; b_len + 1]; |
307 | | |
308 | 0 | let mut prev_a_char = char::MAX; |
309 | 0 | let mut prev_b_char = char::MAX; |
310 | | |
311 | 0 | for (i, a_char) in a.chars().enumerate() { |
312 | 0 | curr_distances[0] = i + 1; |
313 | | |
314 | 0 | for (j, b_char) in b.chars().enumerate() { |
315 | 0 | let cost = usize::from(a_char != b_char); |
316 | 0 | curr_distances[j + 1] = min( |
317 | 0 | curr_distances[j] + 1, |
318 | 0 | min(prev_distances[j + 1] + 1, prev_distances[j] + cost), |
319 | 0 | ); |
320 | 0 | if i > 0 && j > 0 && a_char != b_char && a_char == prev_b_char && b_char == prev_a_char |
321 | 0 | { |
322 | 0 | curr_distances[j + 1] = min(curr_distances[j + 1], prev_two_distances[j - 1] + 1); |
323 | 0 | } |
324 | | |
325 | 0 | prev_b_char = b_char; |
326 | | } |
327 | | |
328 | 0 | mem::swap(&mut prev_two_distances, &mut prev_distances); |
329 | 0 | mem::swap(&mut prev_distances, &mut curr_distances); |
330 | 0 | prev_a_char = a_char; |
331 | | } |
332 | | |
333 | | // access prev_distances instead of curr_distances since we swapped |
334 | | // them above. In case a is empty this would still contain the correct value |
335 | | // from initializing the last element to b_len |
336 | 0 | prev_distances[b_len] |
337 | 0 | } |
338 | | |
339 | | /* Returns the final index for a value in a single vector that represents a fixed |
340 | | 2d grid */ |
341 | 0 | fn flat_index(i: usize, j: usize, width: usize) -> usize { |
342 | 0 | j * width + i |
343 | 0 | } |
344 | | |
345 | | /// Like optimal string alignment, but substrings can be edited an unlimited |
346 | | /// number of times, and the triangle inequality holds. |
347 | | /// |
348 | | /// ``` |
349 | | /// use strsim::generic_damerau_levenshtein; |
350 | | /// |
351 | | /// assert_eq!(2, generic_damerau_levenshtein(&[1,2], &[2,3,1])); |
352 | | /// ``` |
353 | 0 | pub fn generic_damerau_levenshtein<Elem>(a_elems: &[Elem], b_elems: &[Elem]) -> usize |
354 | 0 | where |
355 | 0 | Elem: Eq + Hash + Clone, |
356 | | { |
357 | 0 | let a_len = a_elems.len(); |
358 | 0 | let b_len = b_elems.len(); |
359 | | |
360 | 0 | if a_len == 0 { |
361 | 0 | return b_len; |
362 | 0 | } |
363 | 0 | if b_len == 0 { |
364 | 0 | return a_len; |
365 | 0 | } |
366 | | |
367 | 0 | let width = a_len + 2; |
368 | 0 | let mut distances = vec![0; (a_len + 2) * (b_len + 2)]; |
369 | 0 | let max_distance = a_len + b_len; |
370 | 0 | distances[0] = max_distance; |
371 | | |
372 | 0 | for i in 0..(a_len + 1) { |
373 | 0 | distances[flat_index(i + 1, 0, width)] = max_distance; |
374 | 0 | distances[flat_index(i + 1, 1, width)] = i; |
375 | 0 | } |
376 | | |
377 | 0 | for j in 0..(b_len + 1) { |
378 | 0 | distances[flat_index(0, j + 1, width)] = max_distance; |
379 | 0 | distances[flat_index(1, j + 1, width)] = j; |
380 | 0 | } |
381 | | |
382 | 0 | let mut elems: HashMap<Elem, usize> = HashMap::with_capacity(64); |
383 | | |
384 | 0 | for i in 1..(a_len + 1) { |
385 | 0 | let mut db = 0; |
386 | | |
387 | 0 | for j in 1..(b_len + 1) { |
388 | 0 | let k = match elems.get(&b_elems[j - 1]) { |
389 | 0 | Some(&value) => value, |
390 | 0 | None => 0, |
391 | | }; |
392 | | |
393 | 0 | let insertion_cost = distances[flat_index(i, j + 1, width)] + 1; |
394 | 0 | let deletion_cost = distances[flat_index(i + 1, j, width)] + 1; |
395 | 0 | let transposition_cost = |
396 | 0 | distances[flat_index(k, db, width)] + (i - k - 1) + 1 + (j - db - 1); |
397 | | |
398 | 0 | let mut substitution_cost = distances[flat_index(i, j, width)] + 1; |
399 | 0 | if a_elems[i - 1] == b_elems[j - 1] { |
400 | 0 | db = j; |
401 | 0 | substitution_cost -= 1; |
402 | 0 | } |
403 | | |
404 | 0 | distances[flat_index(i + 1, j + 1, width)] = min( |
405 | 0 | substitution_cost, |
406 | 0 | min(insertion_cost, min(deletion_cost, transposition_cost)), |
407 | 0 | ); |
408 | | } |
409 | | |
410 | 0 | elems.insert(a_elems[i - 1].clone(), i); |
411 | | } |
412 | | |
413 | 0 | distances[flat_index(a_len + 1, b_len + 1, width)] |
414 | 0 | } |
415 | | |
416 | | #[derive(Clone, Copy, PartialEq, Eq)] |
417 | | struct RowId { |
418 | | val: isize, |
419 | | } |
420 | | |
421 | | impl Default for RowId { |
422 | 0 | fn default() -> Self { |
423 | 0 | Self { val: -1 } |
424 | 0 | } |
425 | | } |
426 | | |
427 | | #[derive(Default, Clone)] |
428 | | struct GrowingHashmapMapElemChar<ValueType> { |
429 | | key: u32, |
430 | | value: ValueType, |
431 | | } |
432 | | |
433 | | /// specialized hashmap to store user provided types |
434 | | /// this implementation relies on a couple of base assumptions in order to simplify the implementation |
435 | | /// - the hashmap does not have an upper limit of included items |
436 | | /// - the default value for the `ValueType` can be used as a dummy value to indicate an empty cell |
437 | | /// - elements can't be removed |
438 | | /// - only allocates memory on first write access. |
439 | | /// This improves performance for hashmaps that are never written to |
440 | | struct GrowingHashmapChar<ValueType> { |
441 | | used: i32, |
442 | | fill: i32, |
443 | | mask: i32, |
444 | | map: Option<Vec<GrowingHashmapMapElemChar<ValueType>>>, |
445 | | } |
446 | | |
447 | | impl<ValueType> Default for GrowingHashmapChar<ValueType> |
448 | | where |
449 | | ValueType: Default + Clone + Eq, |
450 | | { |
451 | 0 | fn default() -> Self { |
452 | 0 | Self { |
453 | 0 | used: 0, |
454 | 0 | fill: 0, |
455 | 0 | mask: -1, |
456 | 0 | map: None, |
457 | 0 | } |
458 | 0 | } |
459 | | } |
460 | | |
461 | | impl<ValueType> GrowingHashmapChar<ValueType> |
462 | | where |
463 | | ValueType: Default + Clone + Eq + Copy, |
464 | | { |
465 | 0 | fn get(&self, key: u32) -> ValueType { |
466 | 0 | self.map |
467 | 0 | .as_ref() |
468 | 0 | .map_or_else(|| Default::default(), |map| map[self.lookup(key)].value) |
469 | 0 | } |
470 | | |
471 | 0 | fn get_mut(&mut self, key: u32) -> &mut ValueType { |
472 | 0 | if self.map.is_none() { |
473 | 0 | self.allocate(); |
474 | 0 | } |
475 | | |
476 | 0 | let mut i = self.lookup(key); |
477 | 0 | if self |
478 | 0 | .map |
479 | 0 | .as_ref() |
480 | 0 | .expect("map should have been created above")[i] |
481 | 0 | .value |
482 | 0 | == Default::default() |
483 | | { |
484 | 0 | self.fill += 1; |
485 | | // resize when 2/3 full |
486 | 0 | if self.fill * 3 >= (self.mask + 1) * 2 { |
487 | 0 | self.grow((self.used + 1) * 2); |
488 | 0 | i = self.lookup(key); |
489 | 0 | } |
490 | | |
491 | 0 | self.used += 1; |
492 | 0 | } |
493 | | |
494 | 0 | let elem = &mut self |
495 | 0 | .map |
496 | 0 | .as_mut() |
497 | 0 | .expect("map should have been created above")[i]; |
498 | 0 | elem.key = key; |
499 | 0 | &mut elem.value |
500 | 0 | } |
501 | | |
502 | 0 | fn allocate(&mut self) { |
503 | 0 | self.mask = 8 - 1; |
504 | 0 | self.map = Some(vec![GrowingHashmapMapElemChar::default(); 8]); |
505 | 0 | } |
506 | | |
507 | | /// lookup key inside the hashmap using a similar collision resolution |
508 | | /// strategy to `CPython` and `Ruby` |
509 | 0 | fn lookup(&self, key: u32) -> usize { |
510 | 0 | let hash = key; |
511 | 0 | let mut i = hash as usize & self.mask as usize; |
512 | | |
513 | 0 | let map = self |
514 | 0 | .map |
515 | 0 | .as_ref() |
516 | 0 | .expect("callers have to ensure map is allocated"); |
517 | | |
518 | 0 | if map[i].value == Default::default() || map[i].key == key { |
519 | 0 | return i; |
520 | 0 | } |
521 | | |
522 | 0 | let mut perturb = key; |
523 | | loop { |
524 | 0 | i = (i * 5 + perturb as usize + 1) & self.mask as usize; |
525 | | |
526 | 0 | if map[i].value == Default::default() || map[i].key == key { |
527 | 0 | return i; |
528 | 0 | } |
529 | | |
530 | 0 | perturb >>= 5; |
531 | | } |
532 | 0 | } |
533 | | |
534 | 0 | fn grow(&mut self, min_used: i32) { |
535 | 0 | let mut new_size = self.mask + 1; |
536 | 0 | while new_size <= min_used { |
537 | 0 | new_size <<= 1; |
538 | 0 | } |
539 | | |
540 | 0 | self.fill = self.used; |
541 | 0 | self.mask = new_size - 1; |
542 | | |
543 | 0 | let old_map = std::mem::replace( |
544 | 0 | self.map |
545 | 0 | .as_mut() |
546 | 0 | .expect("callers have to ensure map is allocated"), |
547 | 0 | vec![GrowingHashmapMapElemChar::<ValueType>::default(); new_size as usize], |
548 | | ); |
549 | | |
550 | 0 | for elem in old_map { |
551 | 0 | if elem.value != Default::default() { |
552 | 0 | let j = self.lookup(elem.key); |
553 | 0 | let new_elem = &mut self.map.as_mut().expect("map created above")[j]; |
554 | 0 | new_elem.key = elem.key; |
555 | 0 | new_elem.value = elem.value; |
556 | 0 | self.used -= 1; |
557 | 0 | if self.used == 0 { |
558 | 0 | break; |
559 | 0 | } |
560 | 0 | } |
561 | | } |
562 | | |
563 | 0 | self.used = self.fill; |
564 | 0 | } |
565 | | } |
566 | | |
567 | | struct HybridGrowingHashmapChar<ValueType> { |
568 | | map: GrowingHashmapChar<ValueType>, |
569 | | extended_ascii: [ValueType; 256], |
570 | | } |
571 | | |
572 | | impl<ValueType> HybridGrowingHashmapChar<ValueType> |
573 | | where |
574 | | ValueType: Default + Clone + Copy + Eq, |
575 | | { |
576 | 0 | fn get(&self, key: char) -> ValueType { |
577 | 0 | let value = key as u32; |
578 | 0 | if value <= 255 { |
579 | 0 | let val_u8 = u8::try_from(value).expect("we check the bounds above"); |
580 | 0 | self.extended_ascii[usize::from(val_u8)] |
581 | | } else { |
582 | 0 | self.map.get(value) |
583 | | } |
584 | 0 | } |
585 | | |
586 | 0 | fn get_mut(&mut self, key: char) -> &mut ValueType { |
587 | 0 | let value = key as u32; |
588 | 0 | if value <= 255 { |
589 | 0 | let val_u8 = u8::try_from(value).expect("we check the bounds above"); |
590 | 0 | &mut self.extended_ascii[usize::from(val_u8)] |
591 | | } else { |
592 | 0 | self.map.get_mut(value) |
593 | | } |
594 | 0 | } |
595 | | } |
596 | | |
597 | | impl<ValueType> Default for HybridGrowingHashmapChar<ValueType> |
598 | | where |
599 | | ValueType: Default + Clone + Copy + Eq, |
600 | | { |
601 | 0 | fn default() -> Self { |
602 | 0 | HybridGrowingHashmapChar { |
603 | 0 | map: GrowingHashmapChar::default(), |
604 | 0 | extended_ascii: [Default::default(); 256], |
605 | 0 | } |
606 | 0 | } |
607 | | } |
608 | | |
609 | 0 | fn damerau_levenshtein_impl<Iter1, Iter2>(s1: Iter1, len1: usize, s2: Iter2, len2: usize) -> usize |
610 | 0 | where |
611 | 0 | Iter1: Iterator<Item = char> + Clone, |
612 | 0 | Iter2: Iterator<Item = char> + Clone, |
613 | | { |
614 | | // The implementations is based on the paper |
615 | | // `Linear space string correction algorithm using the Damerau-Levenshtein distance` |
616 | | // from Chunchun Zhao and Sartaj Sahni |
617 | | // |
618 | | // It has a runtime complexity of `O(N*M)` and a memory usage of `O(N+M)`. |
619 | 0 | let max_val = max(len1, len2) as isize + 1; |
620 | | |
621 | 0 | let mut last_row_id = HybridGrowingHashmapChar::<RowId>::default(); |
622 | | |
623 | 0 | let size = len2 + 2; |
624 | 0 | let mut fr = vec![max_val; size]; |
625 | 0 | let mut r1 = vec![max_val; size]; |
626 | 0 | let mut r: Vec<isize> = (max_val..max_val + 1) |
627 | 0 | .chain(0..(size - 1) as isize) |
628 | 0 | .collect(); |
629 | | |
630 | 0 | for (i, ch1) in s1.enumerate().map(|(i, ch1)| (i + 1, ch1)) { |
631 | 0 | mem::swap(&mut r, &mut r1); |
632 | 0 | let mut last_col_id: isize = -1; |
633 | 0 | let mut last_i2l1 = r[1]; |
634 | 0 | r[1] = i as isize; |
635 | 0 | let mut t = max_val; |
636 | | |
637 | 0 | for (j, ch2) in s2.clone().enumerate().map(|(j, ch2)| (j + 1, ch2)) { |
638 | 0 | let diag = r1[j] + isize::from(ch1 != ch2); |
639 | 0 | let left = r[j] + 1; |
640 | 0 | let up = r1[j + 1] + 1; |
641 | 0 | let mut temp = min(diag, min(left, up)); |
642 | | |
643 | 0 | if ch1 == ch2 { |
644 | 0 | last_col_id = j as isize; // last occurence of s1_i |
645 | 0 | fr[j + 1] = r1[j - 1]; // save H_k-1,j-2 |
646 | 0 | t = last_i2l1; // save H_i-2,l-1 |
647 | 0 | } else { |
648 | 0 | let k = last_row_id.get(ch2).val; |
649 | 0 | let l = last_col_id; |
650 | | |
651 | 0 | if j as isize - l == 1 { |
652 | 0 | let transpose = fr[j + 1] + (i as isize - k); |
653 | 0 | temp = min(temp, transpose); |
654 | 0 | } else if i as isize - k == 1 { |
655 | 0 | let transpose = t + (j as isize - l); |
656 | 0 | temp = min(temp, transpose); |
657 | 0 | } |
658 | | } |
659 | | |
660 | 0 | last_i2l1 = r[j + 1]; |
661 | 0 | r[j + 1] = temp; |
662 | | } |
663 | 0 | last_row_id.get_mut(ch1).val = i as isize; |
664 | | } |
665 | | |
666 | 0 | r[len2 + 1] as usize |
667 | 0 | } |
668 | | |
669 | | /// Like optimal string alignment, but substrings can be edited an unlimited |
670 | | /// number of times, and the triangle inequality holds. |
671 | | /// |
672 | | /// ``` |
673 | | /// use strsim::damerau_levenshtein; |
674 | | /// |
675 | | /// assert_eq!(2, damerau_levenshtein("ab", "bca")); |
676 | | /// ``` |
677 | 0 | pub fn damerau_levenshtein(a: &str, b: &str) -> usize { |
678 | 0 | damerau_levenshtein_impl(a.chars(), a.chars().count(), b.chars(), b.chars().count()) |
679 | 0 | } |
680 | | |
681 | | /// Calculates a normalized score of the Damerau–Levenshtein algorithm between |
682 | | /// 0.0 and 1.0 (inclusive), where 1.0 means the strings are the same. |
683 | | /// |
684 | | /// ``` |
685 | | /// use strsim::normalized_damerau_levenshtein; |
686 | | /// |
687 | | /// assert!((normalized_damerau_levenshtein("levenshtein", "löwenbräu") - 0.27272).abs() < 0.00001); |
688 | | /// assert!((normalized_damerau_levenshtein("", "") - 1.0).abs() < 0.00001); |
689 | | /// assert!(normalized_damerau_levenshtein("", "flower").abs() < 0.00001); |
690 | | /// assert!(normalized_damerau_levenshtein("tree", "").abs() < 0.00001); |
691 | | /// assert!((normalized_damerau_levenshtein("sunglasses", "sunglasses") - 1.0).abs() < 0.00001); |
692 | | /// ``` |
693 | 0 | pub fn normalized_damerau_levenshtein(a: &str, b: &str) -> f64 { |
694 | 0 | if a.is_empty() && b.is_empty() { |
695 | 0 | return 1.0; |
696 | 0 | } |
697 | | |
698 | 0 | let len1 = a.chars().count(); |
699 | 0 | let len2 = b.chars().count(); |
700 | 0 | let dist = damerau_levenshtein_impl(a.chars(), len1, b.chars(), len2); |
701 | 0 | 1.0 - (dist as f64) / (max(len1, len2) as f64) |
702 | 0 | } |
703 | | |
704 | | /// Returns an Iterator of char tuples. |
705 | 0 | fn bigrams(s: &str) -> impl Iterator<Item = (char, char)> + '_ { |
706 | 0 | s.chars().zip(s.chars().skip(1)) |
707 | 0 | } |
708 | | |
709 | | /// Calculates a Sørensen-Dice similarity distance using bigrams. |
710 | | /// See <https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient>. |
711 | | /// |
712 | | /// ``` |
713 | | /// use strsim::sorensen_dice; |
714 | | /// |
715 | | /// assert_eq!(1.0, sorensen_dice("", "")); |
716 | | /// assert_eq!(0.0, sorensen_dice("", "a")); |
717 | | /// assert_eq!(0.0, sorensen_dice("french", "quebec")); |
718 | | /// assert_eq!(1.0, sorensen_dice("ferris", "ferris")); |
719 | | /// assert_eq!(0.8888888888888888, sorensen_dice("feris", "ferris")); |
720 | | /// ``` |
721 | 0 | pub fn sorensen_dice(a: &str, b: &str) -> f64 { |
722 | | // implementation guided by |
723 | | // https://github.com/aceakash/string-similarity/blob/f83ba3cd7bae874c20c429774e911ae8cff8bced/src/index.js#L6 |
724 | | |
725 | 0 | let a: String = a.chars().filter(|&x| !char::is_whitespace(x)).collect(); |
726 | 0 | let b: String = b.chars().filter(|&x| !char::is_whitespace(x)).collect(); |
727 | | |
728 | 0 | if a == b { |
729 | 0 | return 1.0; |
730 | 0 | } |
731 | | |
732 | 0 | if a.len() < 2 || b.len() < 2 { |
733 | 0 | return 0.0; |
734 | 0 | } |
735 | | |
736 | 0 | let mut a_bigrams: HashMap<(char, char), usize> = HashMap::new(); |
737 | | |
738 | 0 | for bigram in bigrams(&a) { |
739 | 0 | *a_bigrams.entry(bigram).or_insert(0) += 1; |
740 | 0 | } |
741 | | |
742 | 0 | let mut intersection_size = 0_usize; |
743 | | |
744 | 0 | for bigram in bigrams(&b) { |
745 | 0 | a_bigrams.entry(bigram).and_modify(|bi| { |
746 | 0 | if *bi > 0 { |
747 | 0 | *bi -= 1; |
748 | 0 | intersection_size += 1; |
749 | 0 | } |
750 | 0 | }); |
751 | | } |
752 | | |
753 | 0 | (2 * intersection_size) as f64 / (a.len() + b.len() - 2) as f64 |
754 | 0 | } |
755 | | |
756 | | #[cfg(test)] |
757 | | mod tests { |
758 | | use super::*; |
759 | | |
760 | | macro_rules! assert_delta { |
761 | | ($x:expr, $y:expr) => { |
762 | | assert_delta!($x, $y, 1e-5); |
763 | | }; |
764 | | ($x:expr, $y:expr, $d:expr) => { |
765 | | if ($x - $y).abs() > $d { |
766 | | panic!( |
767 | | "assertion failed: actual: `{}`, expected: `{}`: \ |
768 | | actual not within < {} of expected", |
769 | | $x, $y, $d |
770 | | ); |
771 | | } |
772 | | }; |
773 | | } |
774 | | |
775 | | #[test] |
776 | | fn bigrams_iterator() { |
777 | | let mut bi = bigrams("abcde"); |
778 | | |
779 | | assert_eq!(Some(('a', 'b')), bi.next()); |
780 | | assert_eq!(Some(('b', 'c')), bi.next()); |
781 | | assert_eq!(Some(('c', 'd')), bi.next()); |
782 | | assert_eq!(Some(('d', 'e')), bi.next()); |
783 | | assert_eq!(None, bi.next()); |
784 | | } |
785 | | |
786 | | fn assert_hamming_dist(dist: usize, str1: &str, str2: &str) { |
787 | | assert_eq!(Ok(dist), hamming(str1, str2)); |
788 | | } |
789 | | |
790 | | #[test] |
791 | | fn hamming_empty() { |
792 | | assert_hamming_dist(0, "", "") |
793 | | } |
794 | | |
795 | | #[test] |
796 | | fn hamming_same() { |
797 | | assert_hamming_dist(0, "hamming", "hamming") |
798 | | } |
799 | | |
800 | | #[test] |
801 | | fn hamming_numbers() { |
802 | | assert_eq!(Ok(1), generic_hamming(&[1, 2, 4], &[1, 2, 3])); |
803 | | } |
804 | | |
805 | | #[test] |
806 | | fn hamming_diff() { |
807 | | assert_hamming_dist(3, "hamming", "hammers") |
808 | | } |
809 | | |
810 | | #[test] |
811 | | fn hamming_diff_multibyte() { |
812 | | assert_hamming_dist(2, "hamming", "h香mmüng"); |
813 | | } |
814 | | |
815 | | #[test] |
816 | | fn hamming_unequal_length() { |
817 | | assert_eq!( |
818 | | Err(StrSimError::DifferentLengthArgs), |
819 | | generic_hamming("ham".chars(), "hamming".chars()) |
820 | | ); |
821 | | } |
822 | | |
823 | | #[test] |
824 | | fn hamming_names() { |
825 | | assert_hamming_dist(14, "Friedrich Nietzs", "Jean-Paul Sartre") |
826 | | } |
827 | | |
828 | | #[test] |
829 | | fn jaro_both_empty() { |
830 | | assert_eq!(1.0, jaro("", "")); |
831 | | } |
832 | | |
833 | | #[test] |
834 | | fn jaro_first_empty() { |
835 | | assert_eq!(0.0, jaro("", "jaro")); |
836 | | } |
837 | | |
838 | | #[test] |
839 | | fn jaro_second_empty() { |
840 | | assert_eq!(0.0, jaro("distance", "")); |
841 | | } |
842 | | |
843 | | #[test] |
844 | | fn jaro_same() { |
845 | | assert_eq!(1.0, jaro("jaro", "jaro")); |
846 | | } |
847 | | |
848 | | #[test] |
849 | | fn jaro_multibyte() { |
850 | | assert_delta!(0.818, jaro("testabctest", "testöঙ香test"), 0.001); |
851 | | assert_delta!(0.818, jaro("testöঙ香test", "testabctest"), 0.001); |
852 | | } |
853 | | |
854 | | #[test] |
855 | | fn jaro_diff_short() { |
856 | | assert_delta!(0.767, jaro("dixon", "dicksonx"), 0.001); |
857 | | } |
858 | | |
859 | | #[test] |
860 | | fn jaro_diff_one_character() { |
861 | | assert_eq!(0.0, jaro("a", "b")); |
862 | | } |
863 | | |
864 | | #[test] |
865 | | fn jaro_same_one_character() { |
866 | | assert_eq!(1.0, jaro("a", "a")); |
867 | | } |
868 | | |
869 | | #[test] |
870 | | fn generic_jaro_diff() { |
871 | | assert_eq!(0.0, generic_jaro(&[1, 2], &[3, 4])); |
872 | | } |
873 | | |
874 | | #[test] |
875 | | fn jaro_diff_one_and_two() { |
876 | | assert_delta!(0.83, jaro("a", "ab"), 0.01); |
877 | | } |
878 | | |
879 | | #[test] |
880 | | fn jaro_diff_two_and_one() { |
881 | | assert_delta!(0.83, jaro("ab", "a"), 0.01); |
882 | | } |
883 | | |
884 | | #[test] |
885 | | fn jaro_diff_no_transposition() { |
886 | | assert_delta!(0.822, jaro("dwayne", "duane"), 0.001); |
887 | | } |
888 | | |
889 | | #[test] |
890 | | fn jaro_diff_with_transposition() { |
891 | | assert_delta!(0.944, jaro("martha", "marhta"), 0.001); |
892 | | assert_delta!(0.6, jaro("a jke", "jane a k"), 0.001); |
893 | | } |
894 | | |
895 | | #[test] |
896 | | fn jaro_names() { |
897 | | assert_delta!( |
898 | | 0.392, |
899 | | jaro("Friedrich Nietzsche", "Jean-Paul Sartre"), |
900 | | 0.001 |
901 | | ); |
902 | | } |
903 | | |
904 | | #[test] |
905 | | fn jaro_winkler_both_empty() { |
906 | | assert_eq!(1.0, jaro_winkler("", "")); |
907 | | } |
908 | | |
909 | | #[test] |
910 | | fn jaro_winkler_first_empty() { |
911 | | assert_eq!(0.0, jaro_winkler("", "jaro-winkler")); |
912 | | } |
913 | | |
914 | | #[test] |
915 | | fn jaro_winkler_second_empty() { |
916 | | assert_eq!(0.0, jaro_winkler("distance", "")); |
917 | | } |
918 | | |
919 | | #[test] |
920 | | fn jaro_winkler_same() { |
921 | | assert_eq!(1.0, jaro_winkler("Jaro-Winkler", "Jaro-Winkler")); |
922 | | } |
923 | | |
924 | | #[test] |
925 | | fn jaro_winkler_multibyte() { |
926 | | assert_delta!(0.89, jaro_winkler("testabctest", "testöঙ香test"), 0.001); |
927 | | assert_delta!(0.89, jaro_winkler("testöঙ香test", "testabctest"), 0.001); |
928 | | } |
929 | | |
930 | | #[test] |
931 | | fn jaro_winkler_diff_short() { |
932 | | assert_delta!(0.813, jaro_winkler("dixon", "dicksonx"), 0.001); |
933 | | assert_delta!(0.813, jaro_winkler("dicksonx", "dixon"), 0.001); |
934 | | } |
935 | | |
936 | | #[test] |
937 | | fn jaro_winkler_diff_one_character() { |
938 | | assert_eq!(0.0, jaro_winkler("a", "b")); |
939 | | } |
940 | | |
941 | | #[test] |
942 | | fn jaro_winkler_same_one_character() { |
943 | | assert_eq!(1.0, jaro_winkler("a", "a")); |
944 | | } |
945 | | |
946 | | #[test] |
947 | | fn jaro_winkler_diff_no_transposition() { |
948 | | assert_delta!(0.84, jaro_winkler("dwayne", "duane"), 0.001); |
949 | | } |
950 | | |
951 | | #[test] |
952 | | fn jaro_winkler_diff_with_transposition() { |
953 | | assert_delta!(0.961, jaro_winkler("martha", "marhta"), 0.001); |
954 | | assert_delta!(0.6, jaro_winkler("a jke", "jane a k"), 0.001); |
955 | | } |
956 | | |
957 | | #[test] |
958 | | fn jaro_winkler_names() { |
959 | | assert_delta!( |
960 | | 0.452, |
961 | | jaro_winkler("Friedrich Nietzsche", "Fran-Paul Sartre"), |
962 | | 0.001 |
963 | | ); |
964 | | } |
965 | | |
966 | | #[test] |
967 | | fn jaro_winkler_long_prefix() { |
968 | | assert_delta!(0.866, jaro_winkler("cheeseburger", "cheese fries"), 0.001); |
969 | | } |
970 | | |
971 | | #[test] |
972 | | fn jaro_winkler_more_names() { |
973 | | assert_delta!(0.868, jaro_winkler("Thorkel", "Thorgier"), 0.001); |
974 | | } |
975 | | |
976 | | #[test] |
977 | | fn jaro_winkler_length_of_one() { |
978 | | assert_delta!(0.738, jaro_winkler("Dinsdale", "D"), 0.001); |
979 | | } |
980 | | |
981 | | #[test] |
982 | | fn jaro_winkler_very_long_prefix() { |
983 | | assert_delta!( |
984 | | 0.98519, |
985 | | jaro_winkler("thequickbrownfoxjumpedoverx", "thequickbrownfoxjumpedovery") |
986 | | ); |
987 | | } |
988 | | |
989 | | #[test] |
990 | | fn levenshtein_empty() { |
991 | | assert_eq!(0, levenshtein("", "")); |
992 | | } |
993 | | |
994 | | #[test] |
995 | | fn levenshtein_same() { |
996 | | assert_eq!(0, levenshtein("levenshtein", "levenshtein")); |
997 | | } |
998 | | |
999 | | #[test] |
1000 | | fn levenshtein_diff_short() { |
1001 | | assert_eq!(3, levenshtein("kitten", "sitting")); |
1002 | | } |
1003 | | |
1004 | | #[test] |
1005 | | fn levenshtein_diff_with_space() { |
1006 | | assert_eq!(5, levenshtein("hello, world", "bye, world")); |
1007 | | } |
1008 | | |
1009 | | #[test] |
1010 | | fn levenshtein_diff_multibyte() { |
1011 | | assert_eq!(3, levenshtein("öঙ香", "abc")); |
1012 | | assert_eq!(3, levenshtein("abc", "öঙ香")); |
1013 | | } |
1014 | | |
1015 | | #[test] |
1016 | | fn levenshtein_diff_longer() { |
1017 | | let a = "The quick brown fox jumped over the angry dog."; |
1018 | | let b = "Lorem ipsum dolor sit amet, dicta latine an eam."; |
1019 | | assert_eq!(37, levenshtein(a, b)); |
1020 | | } |
1021 | | |
1022 | | #[test] |
1023 | | fn levenshtein_first_empty() { |
1024 | | assert_eq!(7, levenshtein("", "sitting")); |
1025 | | } |
1026 | | |
1027 | | #[test] |
1028 | | fn levenshtein_second_empty() { |
1029 | | assert_eq!(6, levenshtein("kitten", "")); |
1030 | | } |
1031 | | |
1032 | | #[test] |
1033 | | fn normalized_levenshtein_diff_short() { |
1034 | | assert_delta!(0.57142, normalized_levenshtein("kitten", "sitting")); |
1035 | | } |
1036 | | |
1037 | | #[test] |
1038 | | fn normalized_levenshtein_for_empty_strings() { |
1039 | | assert_delta!(1.0, normalized_levenshtein("", "")); |
1040 | | } |
1041 | | |
1042 | | #[test] |
1043 | | fn normalized_levenshtein_first_empty() { |
1044 | | assert_delta!(0.0, normalized_levenshtein("", "second")); |
1045 | | } |
1046 | | |
1047 | | #[test] |
1048 | | fn normalized_levenshtein_second_empty() { |
1049 | | assert_delta!(0.0, normalized_levenshtein("first", "")); |
1050 | | } |
1051 | | |
1052 | | #[test] |
1053 | | fn normalized_levenshtein_identical_strings() { |
1054 | | assert_delta!(1.0, normalized_levenshtein("identical", "identical")); |
1055 | | } |
1056 | | |
1057 | | #[test] |
1058 | | fn osa_distance_empty() { |
1059 | | assert_eq!(0, osa_distance("", "")); |
1060 | | } |
1061 | | |
1062 | | #[test] |
1063 | | fn osa_distance_same() { |
1064 | | assert_eq!(0, osa_distance("damerau", "damerau")); |
1065 | | } |
1066 | | |
1067 | | #[test] |
1068 | | fn osa_distance_first_empty() { |
1069 | | assert_eq!(7, osa_distance("", "damerau")); |
1070 | | } |
1071 | | |
1072 | | #[test] |
1073 | | fn osa_distance_second_empty() { |
1074 | | assert_eq!(7, osa_distance("damerau", "")); |
1075 | | } |
1076 | | |
1077 | | #[test] |
1078 | | fn osa_distance_diff() { |
1079 | | assert_eq!(3, osa_distance("ca", "abc")); |
1080 | | } |
1081 | | |
1082 | | #[test] |
1083 | | fn osa_distance_diff_short() { |
1084 | | assert_eq!(3, osa_distance("damerau", "aderua")); |
1085 | | } |
1086 | | |
1087 | | #[test] |
1088 | | fn osa_distance_diff_reversed() { |
1089 | | assert_eq!(3, osa_distance("aderua", "damerau")); |
1090 | | } |
1091 | | |
1092 | | #[test] |
1093 | | fn osa_distance_diff_multibyte() { |
1094 | | assert_eq!(3, osa_distance("öঙ香", "abc")); |
1095 | | assert_eq!(3, osa_distance("abc", "öঙ香")); |
1096 | | } |
1097 | | |
1098 | | #[test] |
1099 | | fn osa_distance_diff_unequal_length() { |
1100 | | assert_eq!(6, osa_distance("damerau", "aderuaxyz")); |
1101 | | } |
1102 | | |
1103 | | #[test] |
1104 | | fn osa_distance_diff_unequal_length_reversed() { |
1105 | | assert_eq!(6, osa_distance("aderuaxyz", "damerau")); |
1106 | | } |
1107 | | |
1108 | | #[test] |
1109 | | fn osa_distance_diff_comedians() { |
1110 | | assert_eq!(5, osa_distance("Stewart", "Colbert")); |
1111 | | } |
1112 | | |
1113 | | #[test] |
1114 | | fn osa_distance_many_transpositions() { |
1115 | | assert_eq!(4, osa_distance("abcdefghijkl", "bacedfgihjlk")); |
1116 | | } |
1117 | | |
1118 | | #[test] |
1119 | | fn osa_distance_diff_longer() { |
1120 | | let a = "The quick brown fox jumped over the angry dog."; |
1121 | | let b = "Lehem ipsum dolor sit amet, dicta latine an eam."; |
1122 | | assert_eq!(36, osa_distance(a, b)); |
1123 | | } |
1124 | | |
1125 | | #[test] |
1126 | | fn osa_distance_beginning_transposition() { |
1127 | | assert_eq!(1, osa_distance("foobar", "ofobar")); |
1128 | | } |
1129 | | |
1130 | | #[test] |
1131 | | fn osa_distance_end_transposition() { |
1132 | | assert_eq!(1, osa_distance("specter", "spectre")); |
1133 | | } |
1134 | | |
1135 | | #[test] |
1136 | | fn osa_distance_restricted_edit() { |
1137 | | assert_eq!(4, osa_distance("a cat", "an abct")); |
1138 | | } |
1139 | | |
1140 | | #[test] |
1141 | | fn damerau_levenshtein_empty() { |
1142 | | assert_eq!(0, damerau_levenshtein("", "")); |
1143 | | } |
1144 | | |
1145 | | #[test] |
1146 | | fn damerau_levenshtein_same() { |
1147 | | assert_eq!(0, damerau_levenshtein("damerau", "damerau")); |
1148 | | } |
1149 | | |
1150 | | #[test] |
1151 | | fn damerau_levenshtein_first_empty() { |
1152 | | assert_eq!(7, damerau_levenshtein("", "damerau")); |
1153 | | } |
1154 | | |
1155 | | #[test] |
1156 | | fn damerau_levenshtein_second_empty() { |
1157 | | assert_eq!(7, damerau_levenshtein("damerau", "")); |
1158 | | } |
1159 | | |
1160 | | #[test] |
1161 | | fn damerau_levenshtein_diff() { |
1162 | | assert_eq!(2, damerau_levenshtein("ca", "abc")); |
1163 | | } |
1164 | | |
1165 | | #[test] |
1166 | | fn damerau_levenshtein_diff_short() { |
1167 | | assert_eq!(3, damerau_levenshtein("damerau", "aderua")); |
1168 | | } |
1169 | | |
1170 | | #[test] |
1171 | | fn damerau_levenshtein_diff_reversed() { |
1172 | | assert_eq!(3, damerau_levenshtein("aderua", "damerau")); |
1173 | | } |
1174 | | |
1175 | | #[test] |
1176 | | fn damerau_levenshtein_diff_multibyte() { |
1177 | | assert_eq!(3, damerau_levenshtein("öঙ香", "abc")); |
1178 | | assert_eq!(3, damerau_levenshtein("abc", "öঙ香")); |
1179 | | } |
1180 | | |
1181 | | #[test] |
1182 | | fn damerau_levenshtein_diff_unequal_length() { |
1183 | | assert_eq!(6, damerau_levenshtein("damerau", "aderuaxyz")); |
1184 | | } |
1185 | | |
1186 | | #[test] |
1187 | | fn damerau_levenshtein_diff_unequal_length_reversed() { |
1188 | | assert_eq!(6, damerau_levenshtein("aderuaxyz", "damerau")); |
1189 | | } |
1190 | | |
1191 | | #[test] |
1192 | | fn damerau_levenshtein_diff_comedians() { |
1193 | | assert_eq!(5, damerau_levenshtein("Stewart", "Colbert")); |
1194 | | } |
1195 | | |
1196 | | #[test] |
1197 | | fn damerau_levenshtein_many_transpositions() { |
1198 | | assert_eq!(4, damerau_levenshtein("abcdefghijkl", "bacedfgihjlk")); |
1199 | | } |
1200 | | |
1201 | | #[test] |
1202 | | fn damerau_levenshtein_diff_longer() { |
1203 | | let a = "The quick brown fox jumped over the angry dog."; |
1204 | | let b = "Lehem ipsum dolor sit amet, dicta latine an eam."; |
1205 | | assert_eq!(36, damerau_levenshtein(a, b)); |
1206 | | } |
1207 | | |
1208 | | #[test] |
1209 | | fn damerau_levenshtein_beginning_transposition() { |
1210 | | assert_eq!(1, damerau_levenshtein("foobar", "ofobar")); |
1211 | | } |
1212 | | |
1213 | | #[test] |
1214 | | fn damerau_levenshtein_end_transposition() { |
1215 | | assert_eq!(1, damerau_levenshtein("specter", "spectre")); |
1216 | | } |
1217 | | |
1218 | | #[test] |
1219 | | fn damerau_levenshtein_unrestricted_edit() { |
1220 | | assert_eq!(3, damerau_levenshtein("a cat", "an abct")); |
1221 | | } |
1222 | | |
1223 | | #[test] |
1224 | | fn normalized_damerau_levenshtein_diff_short() { |
1225 | | assert_delta!( |
1226 | | 0.27272, |
1227 | | normalized_damerau_levenshtein("levenshtein", "löwenbräu") |
1228 | | ); |
1229 | | } |
1230 | | |
1231 | | #[test] |
1232 | | fn normalized_damerau_levenshtein_for_empty_strings() { |
1233 | | assert_delta!(1.0, normalized_damerau_levenshtein("", "")); |
1234 | | } |
1235 | | |
1236 | | #[test] |
1237 | | fn normalized_damerau_levenshtein_first_empty() { |
1238 | | assert_delta!(0.0, normalized_damerau_levenshtein("", "flower")); |
1239 | | } |
1240 | | |
1241 | | #[test] |
1242 | | fn normalized_damerau_levenshtein_second_empty() { |
1243 | | assert_delta!(0.0, normalized_damerau_levenshtein("tree", "")); |
1244 | | } |
1245 | | |
1246 | | #[test] |
1247 | | fn normalized_damerau_levenshtein_identical_strings() { |
1248 | | assert_delta!( |
1249 | | 1.0, |
1250 | | normalized_damerau_levenshtein("sunglasses", "sunglasses") |
1251 | | ); |
1252 | | } |
1253 | | |
1254 | | #[test] |
1255 | | fn sorensen_dice_all() { |
1256 | | // test cases taken from |
1257 | | // https://github.com/aceakash/string-similarity/blob/f83ba3cd7bae874c20c429774e911ae8cff8bced/src/spec/index.spec.js#L11 |
1258 | | |
1259 | | assert_delta!(1.0, sorensen_dice("a", "a")); |
1260 | | assert_delta!(0.0, sorensen_dice("a", "b")); |
1261 | | assert_delta!(1.0, sorensen_dice("", "")); |
1262 | | assert_delta!(0.0, sorensen_dice("a", "")); |
1263 | | assert_delta!(0.0, sorensen_dice("", "a")); |
1264 | | assert_delta!(1.0, sorensen_dice("apple event", "apple event")); |
1265 | | assert_delta!(0.90909, sorensen_dice("iphone", "iphone x")); |
1266 | | assert_delta!(0.0, sorensen_dice("french", "quebec")); |
1267 | | assert_delta!(1.0, sorensen_dice("france", "france")); |
1268 | | assert_delta!(0.2, sorensen_dice("fRaNce", "france")); |
1269 | | assert_delta!(0.8, sorensen_dice("healed", "sealed")); |
1270 | | assert_delta!( |
1271 | | 0.78788, |
1272 | | sorensen_dice("web applications", "applications of the web") |
1273 | | ); |
1274 | | assert_delta!( |
1275 | | 0.92, |
1276 | | sorensen_dice( |
1277 | | "this will have a typo somewhere", |
1278 | | "this will huve a typo somewhere" |
1279 | | ) |
1280 | | ); |
1281 | | assert_delta!( |
1282 | | 0.60606, |
1283 | | sorensen_dice( |
1284 | | "Olive-green table for sale, in extremely good condition.", |
1285 | | "For sale: table in very good condition, olive green in colour." |
1286 | | ) |
1287 | | ); |
1288 | | assert_delta!( |
1289 | | 0.25581, |
1290 | | sorensen_dice( |
1291 | | "Olive-green table for sale, in extremely good condition.", |
1292 | | "For sale: green Subaru Impreza, 210,000 miles" |
1293 | | ) |
1294 | | ); |
1295 | | assert_delta!( |
1296 | | 0.14118, |
1297 | | sorensen_dice( |
1298 | | "Olive-green table for sale, in extremely good condition.", |
1299 | | "Wanted: mountain bike with at least 21 gears." |
1300 | | ) |
1301 | | ); |
1302 | | assert_delta!( |
1303 | | 0.77419, |
1304 | | sorensen_dice("this has one extra word", "this has one word") |
1305 | | ); |
1306 | | } |
1307 | | } |