Coverage Report

Created: 2026-08-14 08:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}