//! This library implements string similarity metrics.
#![forbid(unsafe_code)] #![allow( // these casts are sometimes needed. They restrict the length of input iterators // but there isn't really any way around this except for always working with // 128 bit types
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_precision_loss, // not practical
clippy::needless_pass_by_value,
clippy::similar_names, // noisy
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate, // todo https://github.com/rapidfuzz/strsim-rs/issues/59
clippy::range_plus_one
)]
use std::char; use std::cmp::{max, min}; use std::collections::HashMap; use std::convert::TryFrom; use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::hash::Hash; use std::mem; use std::str::Chars;
/// Calculates the number of positions in the two sequences where the elements /// differ. Returns an error if the sequences have different lengths. pubfn generic_hamming<Iter1, Iter2, Elem1, Elem2>(a: Iter1, b: Iter2) -> HammingResult where
Iter1: IntoIterator<Item = Elem1>,
Iter2: IntoIterator<Item = Elem2>,
Elem1: PartialEq<Elem2>,
{ let (mut ita, mut itb) = (a.into_iter(), b.into_iter()); letmut count = 0; loop { match (ita.next(), itb.next()) {
(Some(x), Some(y)) => { if x != y {
count += 1;
}
}
(None, None) => return Ok(count),
_ => return Err(StrSimError::DifferentLengthArgs),
}
}
}
/// Calculates the number of positions in the two strings where the characters /// differ. Returns an error if the strings have different lengths. /// /// ``` /// use strsim::{hamming, StrSimError::DifferentLengthArgs}; /// /// assert_eq!(Ok(3), hamming("hamming", "hammers")); /// /// assert_eq!(Err(DifferentLengthArgs), hamming("hamming", "ham")); /// ``` pubfn hamming(a: &str, b: &str) -> HammingResult {
generic_hamming(a.chars(), b.chars())
}
/// Calculates the Jaro similarity between two sequences. The returned value /// is between 0.0 and 1.0 (higher value means more similar). pubfn generic_jaro<'a, 'b, Iter1, Iter2, Elem1, Elem2>(a: &'a Iter1, b: &'b Iter2) -> f64 where
&'a Iter1: IntoIterator<Item = Elem1>,
&'b Iter2: IntoIterator<Item = Elem2>,
Elem1: PartialEq<Elem2>,
{ let a_len = a.into_iter().count(); let b_len = b.into_iter().count();
/// Calculates the Jaro similarity between two strings. The returned value /// is between 0.0 and 1.0 (higher value means more similar). /// /// ``` /// use strsim::jaro; /// /// assert!((0.392 - jaro("Friedrich Nietzsche", "Jean-Paul Sartre")).abs() < /// 0.001); /// ``` pubfn jaro(a: &str, b: &str) -> f64 {
generic_jaro(&StringWrapper(a), &StringWrapper(b))
}
/// Like Jaro but gives a boost to sequences that have a common prefix. pubfn generic_jaro_winkler<'a, 'b, Iter1, Iter2, Elem1, Elem2>(a: &>'a Iter1, b: &'b Iter2) -> f64 where
&'a Iter1: IntoIterator<Item = Elem1>,
&'b Iter2: IntoIterator<Item = Elem2>,
Elem1: PartialEq<Elem2>,
{ let sim = generic_jaro(a, b);
if sim > 0.7 { let prefix_length = a
.into_iter()
.take(4)
.zip(b)
.take_while(|(a_elem, b_elem)| a_elem == b_elem)
.count();
/// Like Jaro but gives a boost to strings that have a common prefix. /// /// ``` /// use strsim::jaro_winkler; /// /// assert!((0.866 - jaro_winkler("cheeseburger", "cheese fries")).abs() < /// 0.001); /// ``` pubfn jaro_winkler(a: &str, b: &str) -> f64 {
generic_jaro_winkler(&StringWrapper(a), &StringWrapper(b))
}
/// Calculates the minimum number of insertions, deletions, and substitutions /// required to change one sequence into the other. /// /// ``` /// use strsim::generic_levenshtein; /// /// assert_eq!(3, generic_levenshtein(&[1,2,3], &[1,2,3,4,5,6])); /// ``` pubfn generic_levenshtein<'a, 'b, Iter1, Iter2, Elem1, Elem2>(a: &'a Iter1, b: &'b Iter2) -> usize where
&'a Iter1: IntoIterator<Item = Elem1>,
&'b Iter2: IntoIterator<Item = Elem2>,
Elem1: PartialEq<Elem2>,
{ let b_len = b.into_iter().count();
for (i, a_elem) in a.into_iter().enumerate() {
result = i + 1; letmut distance_b = i;
for (j, b_elem) in b.into_iter().enumerate() { let cost = usize::from(a_elem != b_elem); let distance_a = distance_b + cost;
distance_b = cache[j];
result = min(result + 1, min(distance_a, distance_b + 1));
cache[j] = result;
}
}
result
}
/// Calculates the minimum number of insertions, deletions, and substitutions /// required to change one string into the other. /// /// ``` /// use strsim::levenshtein; /// /// assert_eq!(3, levenshtein("kitten", "sitting")); /// ``` pubfn levenshtein(a: &str, b: &str) -> usize {
generic_levenshtein(&StringWrapper(a), &StringWrapper(b))
}
/// Calculates a normalized score of the Levenshtein algorithm between 0.0 and /// 1.0 (inclusive), where 1.0 means the strings are the same. /// /// ``` /// use strsim::normalized_levenshtein; /// /// assert!((normalized_levenshtein("kitten", "sitting") - 0.57142).abs() < 0.00001); /// assert!((normalized_levenshtein("", "") - 1.0).abs() < 0.00001); /// assert!(normalized_levenshtein("", "second").abs() < 0.00001); /// assert!(normalized_levenshtein("first", "").abs() < 0.00001); /// assert!((normalized_levenshtein("string", "string") - 1.0).abs() < 0.00001); /// ``` pubfn normalized_levenshtein(a: &str, b: &str) -> f64 { if a.is_empty() && b.is_empty() { return1.0;
} 1.0 - (levenshtein(a, b) as f64) / (a.chars().count().max(b.chars().count()) as f64)
}
/// Like Levenshtein but allows for adjacent transpositions. Each substring can /// only be edited once. /// /// ``` /// use strsim::osa_distance; /// /// assert_eq!(3, osa_distance("ab", "bca")); /// ``` pubfn osa_distance(a: &str, b: &str) -> usize { let b_len = b.chars().count(); // 0..=b_len behaves like 0..b_len.saturating_add(1) which could be a different size // this leads to significantly worse code gen when swapping the vectors below letmut prev_two_distances: Vec<usize> = (0..b_len + 1).collect(); letmut prev_distances: Vec<usize> = (0..b_len + 1).collect(); letmut curr_distances: Vec<usize> = vec![0; b_len + 1];
// access prev_distances instead of curr_distances since we swapped // them above. In case a is empty this would still contain the correct value // from initializing the last element to b_len
prev_distances[b_len]
}
/* Returns the final index for a value in a single vector that represents a fixed
2d grid */ fn flat_index(i: usize, j: usize, width: usize) -> usize {
j * width + i
}
/// Like optimal string alignment, but substrings can be edited an unlimited /// number of times, and the triangle inequality holds. /// /// ``` /// use strsim::generic_damerau_levenshtein; /// /// assert_eq!(2, generic_damerau_levenshtein(&[1,2], &[2,3,1])); /// ``` pubfn generic_damerau_levenshtein<Elem>(a_elems: &[Elem], b_elems: &[Elem]) -> usize where
Elem: Eq + Hash + Clone,
{ let a_len = a_elems.len(); let b_len = b_elems.len();
if a_len == 0 { return b_len;
} if b_len == 0 { return a_len;
}
/// specialized hashmap to store user provided types /// this implementation relies on a couple of base assumptions in order to simplify the implementation /// - the hashmap does not have an upper limit of included items /// - the default value for the `ValueType` can be used as a dummy value to indicate an empty cell /// - elements can't be removed /// - only allocates memory on first write access. /// This improves performance for hashmaps that are never written to struct GrowingHashmapChar<ValueType> {
used: i32,
fill: i32,
mask: i32,
map: Option<Vec<GrowingHashmapMapElemChar<ValueType>>>,
}
/// lookup key inside the hashmap using a similar collision resolution /// strategy to `CPython` and `Ruby` fn lookup(&self, key: u32) -> usize { let hash = key; letmut i = hash as usize & self.mask as usize;
let map = self
.map
.as_ref()
.expect("callers have to ensure map is allocated");
let old_map = std::mem::replace( self.map
.as_mut()
.expect("callers have to ensure map is allocated"),
vec![GrowingHashmapMapElemChar::<ValueType>::default(); new_size as usize],
);
for elem in old_map { if elem.value != Default::default() { let j = self.lookup(elem.key); let new_elem = &mutself.map.as_mut().expect("map created above")[j];
new_elem.key = elem.key;
new_elem.value = elem.value; self.used -= 1; ifself.used == 0 { break;
}
}
}
impl<ValueType> HybridGrowingHashmapChar<ValueType> where
ValueType: Default + Clone + Copy + Eq,
{ fn get(&self, key: char) -> ValueType { let value = key as u32; if value <= 255 { let val_u8 = u8::try_from(value).expect("we check the bounds above"); self.extended_ascii[usize::from(val_u8)]
} else { self.map.get(value)
}
}
fn get_mut(&mutself, key: char) -> &mut ValueType { let value = key as u32; if value <= 255 { let val_u8 = u8::try_from(value).expect("we check the bounds above");
&mutself.extended_ascii[usize::from(val_u8)]
} else { self.map.get_mut(value)
}
}
}
fn damerau_levenshtein_impl<Iter1, Iter2>(s1: Iter1, len1: usize, s2: Iter2, len2: usize) -> usize where
Iter1: Iterator<Item = char> + Clone,
Iter2: Iterator<Item = char> + Clone,
{ // The implementations is based on the paper // `Linear space string correction algorithm using the Damerau-Levenshtein distance` // from Chunchun Zhao and Sartaj Sahni // // It has a runtime complexity of `O(N*M)` and a memory usage of `O(N+M)`. let max_val = max(len1, len2) as isize + 1;
for (i, ch1) in s1.enumerate().map(|(i, ch1)| (i + 1, ch1)) {
mem::swap(&mut r, &mut r1); letmut last_col_id: isize = -1; letmut last_i2l1 = r[1];
r[1] = i as isize; letmut t = max_val;
for (j, ch2) in s2.clone().enumerate().map(|(j, ch2)| (j + 1, ch2)) { let diag = r1[j] + isize::from(ch1 != ch2); let left = r[j] + 1; let up = r1[j + 1] + 1; letmut temp = min(diag, min(left, up));
if ch1 == ch2 {
last_col_id = j as isize; // last occurence of s1_i
fr[j + 1] = r1[j - 1]; // save H_k-1,j-2
t = last_i2l1; // save H_i-2,l-1
} else { let k = last_row_id.get(ch2).val; let l = last_col_id;
if j as isize - l == 1 { let transpose = fr[j + 1] + (i as isize - k);
temp = min(temp, transpose);
} elseif i as isize - k == 1 { let transpose = t + (j as isize - l);
temp = min(temp, transpose);
}
}
last_i2l1 = r[j + 1];
r[j + 1] = temp;
}
last_row_id.get_mut(ch1).val = i as isize;
}
r[len2 + 1] as usize
}
/// Like optimal string alignment, but substrings can be edited an unlimited /// number of times, and the triangle inequality holds. /// /// ``` /// use strsim::damerau_levenshtein; /// /// assert_eq!(2, damerau_levenshtein("ab", "bca")); /// ``` pubfn damerau_levenshtein(a: &str, b: &str) -> usize {
damerau_levenshtein_impl(a.chars(), a.chars().count(), b.chars(), b.chars().count())
}
/// Calculates a normalized score of the Damerau–Levenshtein algorithm between /// 0.0 and 1.0 (inclusive), where 1.0 means the strings are the same. /// /// ``` /// use strsim::normalized_damerau_levenshtein; /// /// assert!((normalized_damerau_levenshtein("levenshtein", "löwenbräu") - 0.27272).abs() < 0.00001); /// assert!((normalized_damerau_levenshtein("", "") - 1.0).abs() < 0.00001); /// assert!(normalized_damerau_levenshtein("", "flower").abs() < 0.00001); /// assert!(normalized_damerau_levenshtein("tree", "").abs() < 0.00001); /// assert!((normalized_damerau_levenshtein("sunglasses", "sunglasses") - 1.0).abs() < 0.00001); /// ``` pubfn normalized_damerau_levenshtein(a: &str, b: &str) -> f64 { if a.is_empty() && b.is_empty() { return1.0;
}
let len1 = a.chars().count(); let len2 = b.chars().count(); let dist = damerau_levenshtein_impl(a.chars(), len1, b.chars(), len2); 1.0 - (dist as f64) / (max(len1, len2) as f64)
}
#[test] fn levenshtein_diff_longer() { let a = "The quick brown fox jumped over the angry dog."; let b = "Lorem ipsum dolor sit amet, dicta latine an eam.";
assert_eq!(37, levenshtein(a, b));
}
#[test] fn osa_distance_diff_longer() { let a = "The quick brown fox jumped over the angry dog."; let b = "Lehem ipsum dolor sit amet, dicta latine an eam.";
assert_eq!(36, osa_distance(a, b));
}
#[test] fn damerau_levenshtein_diff_longer() { let a = "The quick brown fox jumped over the angry dog."; let b = "Lehem ipsum dolor sit amet, dicta latine an eam.";
assert_eq!(36, damerau_levenshtein(a, b));
}
assert_delta!(1.0, sorensen_dice("a", "a"));
assert_delta!(0.0, sorensen_dice("a", "b"));
assert_delta!(1.0, sorensen_dice("", ""));
assert_delta!(0.0, sorensen_dice("a", ""));
assert_delta!(0.0, sorensen_dice("", "a"));
assert_delta!(1.0, sorensen_dice("apple event", "apple event"));
assert_delta!(0.90909, sorensen_dice("iphone", "iphone x"));
assert_delta!(0.0, sorensen_dice("french", "quebec"));
assert_delta!(1.0, sorensen_dice("france", "france"));
assert_delta!(0.2, sorensen_dice("fRaNce", "france"));
assert_delta!(0.8, sorensen_dice("healed", "sealed"));
assert_delta!( 0.78788,
sorensen_dice("web applications", "applications of the web")
);
assert_delta!( 0.92,
sorensen_dice( "this will have a typo somewhere", "this will huve a typo somewhere"
)
);
assert_delta!( 0.60606,
sorensen_dice( "Olive-green table for sale, in extremely good condition.", "For sale: table in very good condition, olive green in colour."
)
);
assert_delta!( 0.25581,
sorensen_dice( "Olive-green table for sale, in extremely good condition.", "For sale: green Subaru Impreza, 210,000 miles"
)
);
assert_delta!( 0.14118,
sorensen_dice( "Olive-green table for sale, in extremely good condition.", "Wanted: mountain bike with at least 21 gears."
)
);
assert_delta!( 0.77419,
sorensen_dice("this has one extra word", "this has one word")
);
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.47 Sekunden
(vorverarbeitet am 2026-06-18)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.