#[inline]
pub fn distance(a: &str, b: &str) -> usize {
let a_len = a.chars().count();
let b_len = b.chars().count();
if a == b {
return 0;
}
if a_len > b_len {
return distance(b, a);
}
let mut prev_row: Vec<usize> = (0..=a_len).collect();
for (j, bc) in b.chars().enumerate() {
let mut prev = j + 1;
let mut current;
for (i, ac) in a.chars().enumerate() {
let cost = if ac == bc { 0 } else { 1 };
current = std::cmp::min(
std::cmp::min(prev + 1, prev_row[i + 1] + 1),
prev_row[i] + cost,
);
prev_row[i] = prev;
prev = current;
}
prev_row[a_len] = prev;
}
prev_row[a_len]
}
#[inline]
pub fn similarity(a: &str, b: &str) -> f64 {
let max_len = std::cmp::max(a.chars().count(), b.chars().count());
if max_len == 0 {
return 1.0; }
1.0 - (distance(a, b) as f64 / max_len as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_strings() {
assert_eq!(similarity("hello", "hello"), 1.0);
assert_eq!(distance("hello", "hello"), 0);
}
#[test]
fn completely_different() {
let s = similarity("abc", "xyz");
assert!(s < 0.01, "expected near 0, got {s}");
}
#[test]
fn one_edit_away() {
assert_eq!(distance("kitten", "sitten"), 1);
let s = similarity("kitten", "sitten");
assert!(s > 0.8 && s < 1.0);
}
#[test]
fn classic_example() {
assert_eq!(distance("kitten", "sitting"), 3);
let s = similarity("kitten", "sitting");
assert!((s - 0.571).abs() < 0.01, "got {s}");
}
#[test]
fn empty_strings() {
assert_eq!(similarity("", ""), 1.0);
assert_eq!(distance("", ""), 0);
assert_eq!(similarity("abc", ""), 0.0);
}
#[test]
fn symmetric() {
let a = similarity("saturday", "sunday");
let b = similarity("sunday", "saturday");
assert!((a - b).abs() < f64::EPSILON);
}
#[test]
fn unicode_characters() {
let s = similarity("café", "cafe");
assert!(s > 0.0 && s < 1.0);
}
}