soundex_rs/
lib.rs

1//! soundex_rs is a library that calculates the words' soundex.
2//!
3//! # References
4//! <https://support.esri.com/en/technical-article/000003773>
5//!
6//!  # Features
7//! | feature | description  |
8//! | --------| -------------|
9//! | default | The result retains the first four characters of the soundex value|
10//! | full    | The result retains the complete value of soundex |
11//!
12//! # Examples
13//! ```
14//! use soundex_rs::Soundex;
15//! println!("{}", "hello world".soundex());
16//! ```
17
18use std::ops::Deref;
19
20pub trait Soundex {
21    /// soundex get the string's soundex value.
22    /// # Examples
23    /// ```
24    /// use soundex_rs::Soundex;
25    /// if cfg!(feature="full") {
26    ///     assert_eq!("hello world".soundex(), "H4643".to_string());
27    /// } else {
28    ///     assert_eq!("hello world".soundex(), "H464".to_string());
29    /// }
30    /// ```
31    fn soundex(&self) -> String;
32}
33
34/// Default implementation for strings.
35impl<T: Deref<Target = str>> Soundex for T {
36    fn soundex(&self) -> String {
37        if self.is_empty() {
38            return Default::default();
39        }
40
41        let mut reslut = String::with_capacity(4);
42        let mut last = None;
43        let mut count = 0;
44
45        for next in self.chars() {
46            let score = number_map(next);
47
48            if last.is_none() {
49                if !next.is_alphanumeric() {
50                    continue;
51                }
52
53                last = score;
54                reslut.push(next.to_ascii_uppercase());
55            } else {
56                if !next.is_ascii_alphabetic() || is_drop(next) || score == last {
57                    continue;
58                }
59
60                last = score;
61                reslut.push(score.unwrap());
62            }
63
64            count += 1;
65
66            #[cfg(not(feature = "full"))]
67            {
68                if count == 4 {
69                    break;
70                }
71            }
72        }
73
74        if count < 4 {
75            reslut.push_str("0".repeat(4 - count).as_str());
76        }
77
78        reslut
79    }
80}
81
82#[inline(always)]
83fn number_map(i: char) -> Option<char> {
84    match i.to_ascii_lowercase() {
85        'b' | 'f' | 'p' | 'v' => Some('1'),
86        'c' | 'g' | 'j' | 'k' | 'q' | 's' | 'x' | 'z' => Some('2'),
87        'd' | 't' => Some('3'),
88        'l' => Some('4'),
89        'm' | 'n' => Some('5'),
90        'r' => Some('6'),
91        _ => Some('0'),
92    }
93}
94
95#[inline(always)]
96fn is_drop(c: char) -> bool {
97    matches!(
98        c.to_ascii_lowercase(),
99        'a' | 'e' | 'i' | 'o' | 'u' | 'y' | 'h' | 'w'
100    )
101}
102
103/// equal compares two strings' soundex value, if the result is equal, returns true.
104/// # Examples
105/// ```
106///  use soundex_rs::equal;
107///  assert!(equal("Y.LEE", "Y.LIE"));
108/// ```
109pub fn equal<LEFT, RIGHT>(left: LEFT, right: RIGHT) -> bool
110where
111    LEFT: Soundex,
112    RIGHT: Soundex,
113{
114    left.soundex() == right.soundex()
115}
116
117#[cfg(test)]
118mod tests {
119    use super::Soundex;
120    use crate::equal;
121
122    #[test]
123    fn test_soundex() {
124        let m = vec![
125            ("", "".to_string()),
126            ("c你rfpv", "C610".to_string()),
127            ("你rfpv", "你610".to_string()),
128            ("x", "X000".to_string()),
129            ("xxxxx", "X000".to_string()),
130            ("difficult", "D1243".to_string()),
131            ("Knuth", "K530".to_string()),
132            ("Kant", "K530".to_string()),
133            ("Jarovski", "J612".to_string()),
134            ("Resnik", "R252".to_string()),
135            ("Reznick", "R252".to_string()),
136            ("Euler", "E460".to_string()),
137            ("Peterson", "P3625".to_string()),
138            ("Jefferson", "J1625".to_string()),
139            ("bb你iiiffpvsgsslkfldsjfasdas", "B24214321232".to_string()),
140        ];
141
142        for (i, v) in m.into_iter() {
143            if cfg!(feature = "full") {
144                assert_eq!(i.soundex(), v, "{}", i);
145                assert_eq!(i.to_string().soundex(), v, "{}", i);
146                assert_eq!(i.to_string().as_mut().soundex(), v, "{}", i);
147            } else {
148                assert_eq!(i.soundex(), String::from_iter(v.chars().take(4)), "{}", i);
149                assert_eq!(
150                    i.to_string().soundex(),
151                    String::from_iter(v.chars().take(4)),
152                    "{}",
153                    i
154                );
155                assert_eq!(
156                    i.to_string().as_mut().soundex(),
157                    String::from_iter(v.chars().take(4)),
158                    "{}",
159                    i
160                );
161            }
162        }
163    }
164
165    #[test]
166    fn test_equal() {
167        assert!(equal("hello", "hello".to_string()));
168        assert!(equal("hello", "hello"));
169        assert!(!equal("hello world", "hello".to_string()));
170    }
171}