Skip to main content

matchr/
normalize.rs

1use unicode_normalization::char::is_combining_mark;
2use unicode_normalization::UnicodeNormalization;
3
4/// Trim, lowercase, and strip diacritics.
5///
6/// Steps:
7/// 1. Trim surrounding whitespace
8/// 2. Lowercase (Unicode-aware)
9/// 3. Decompose to NFD
10/// 4. Drop combining marks (Unicode category Mn)
11/// 5. Recompose to NFC
12///
13/// # Examples
14///
15/// ```
16/// use matchr::normalize;
17///
18/// assert_eq!(normalize("Café"), "cafe");
19/// assert_eq!(normalize(" Hello "), "hello");
20/// ```
21pub fn normalize(s: &str) -> String {
22    s.trim()
23        .to_lowercase()
24        .nfd()
25        .filter(|c| !is_combining_mark(*c))
26        .nfc()
27        .collect()
28}
29
30#[cfg(test)]
31mod test {
32    use super::*;
33
34    #[test]
35    fn test_lowercase() {
36        assert_eq!(normalize("Hello"), "hello");
37    }
38
39    #[test]
40    fn test_trim() {
41        assert_eq!(normalize(" hello "), "hello");
42    }
43
44    #[test]
45    fn test_both() {
46        assert_eq!(normalize(" Hello World "), "hello world")
47    }
48
49    #[test]
50    fn test_strips_acute_accent() {
51        assert_eq!(normalize("café"), "cafe");
52    }
53
54    #[test]
55    fn test_strips_tilde() {
56        assert_eq!(normalize("Ñoño"), "nono");
57    }
58
59    #[test]
60    fn test_strips_umlaut() {
61        assert_eq!(normalize("Über"), "uber");
62        assert_eq!(normalize("naïve"), "naive");
63    }
64
65    #[test]
66    fn test_strips_multiple_diacritics() {
67        assert_eq!(normalize("Crème Brûlée"), "creme brulee");
68    }
69
70    #[test]
71    fn test_no_diacritics_unchanged() {
72        assert_eq!(normalize("hello world"), "hello world");
73    }
74
75    #[test]
76    fn test_precomposed_and_decomposed_match() {
77        // "é" can be encoded as either U+00E9 (precomposed) or
78        // "e" + U+0301 (decomposed). Both should normalise the same.
79        let precomposed = "caf\u{00E9}";
80        let decomposed = "cafe\u{0301}";
81        assert_eq!(normalize(precomposed), normalize(decomposed));
82        assert_eq!(normalize(precomposed), "cafe");
83    }
84}