pub fn soundex(s: &str) -> String {
let s = s.trim().to_uppercase();
let chars: Vec<char> = s.chars().filter(|c| c.is_ascii_alphabetic()).collect();
if chars.is_empty() {
return "0000".to_string();
}
let first = chars[0];
let mut code = String::from(first);
let to_digit = |c: char| -> char {
match c {
'B' | 'F' | 'P' | 'V' => '1',
'C' | 'G' | 'J' | 'K' | 'Q' | 'S' | 'X' | 'Z' => '2',
'D' | 'T' => '3',
'L' => '4',
'M' | 'N' => '5',
'R' => '6',
_ => '0',
}
};
let mut last_digit = to_digit(first);
for &c in &chars[1..] {
if code.len() >= 4 {
break;
}
let digit = to_digit(c);
if digit != '0' && digit != last_digit {
code.push(digit);
}
last_digit = digit;
}
while code.len() < 4 {
code.push('0');
}
code
}
pub fn soundex_match(a: &str, b: &str) -> bool {
soundex(a) == soundex(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soundex_robert() {
assert_eq!(soundex("Robert"), "R163");
}
#[test]
fn test_soundex_rupert() {
assert_eq!(soundex("Rupert"), "R163");
}
#[test]
fn test_soundex_match_similar_names() {
assert!(soundex_match("Robert", "Rupert"));
}
#[test]
fn test_soundex_no_match() {
assert!(!soundex_match("Robert", "Smith"));
}
#[test]
fn test_soundex_ashcraft() {
assert_eq!(soundex("Ashcraft"), "A226");
}
#[test]
fn test_soundex_empty() {
assert_eq!(soundex(""), "0000");
}
#[test]
fn test_soundex_single_char() {
assert_eq!(soundex("A"), "A000");
}
#[test]
fn test_soundex_case_insensitive() {
assert_eq!(soundex("smith"), soundex("SMITH"));
}
#[test]
fn test_soundex_washington() {
assert_eq!(soundex("Washington"), "W252");
}
#[test]
fn test_soundex_place_names() {
assert!(soundex_match("Springfield", "Springfeild"));
}
}