initiative_core/utils/
case_insensitive_str.rs

1use std::cmp::Ordering;
2
3pub trait CaseInsensitiveStr<'a> {
4    fn eq_ci<S: AsRef<str>>(&self, other: S) -> bool;
5
6    fn cmp_ci<S: AsRef<str>>(&self, other: S) -> Ordering;
7
8    fn in_ci<S: AsRef<str>>(&self, haystack: &[S]) -> bool;
9
10    fn starts_with_ci<S: AsRef<str>>(&self, prefix: S) -> bool;
11
12    fn ends_with_ci<S: AsRef<str>>(&self, suffix: S) -> bool;
13
14    fn strip_prefix_ci<S: AsRef<str>>(&'a self, prefix: S) -> Option<&'a str>;
15
16    fn strip_suffix_ci<S: AsRef<str>>(&'a self, prefix: S) -> Option<&'a str>;
17}
18
19impl<'a, T: AsRef<str>> CaseInsensitiveStr<'a> for T {
20    fn eq_ci<S: AsRef<str>>(&self, other: S) -> bool {
21        let (a, b) = (self.as_ref(), other.as_ref());
22
23        a == b
24            || (a.len() == b.len())
25                && a.chars().zip(b.chars()).all(|(a, b)| {
26                    a == b
27                        || !(!a.is_alphabetic()
28                            || !b.is_alphabetic()
29                            || a.is_lowercase() == b.is_lowercase()
30                            || !a.to_lowercase().eq(b.to_lowercase()))
31                })
32    }
33
34    fn cmp_ci<S: AsRef<str>>(&self, other: S) -> Ordering {
35        let (a, b) = (self.as_ref(), other.as_ref());
36
37        if a == b {
38            Ordering::Equal
39        } else {
40            a.chars()
41                .zip(b.chars())
42                .find_map(|(a, b)| {
43                    match if a == b {
44                        Ordering::Equal
45                    } else if a.is_uppercase() || b.is_uppercase() {
46                        a.to_lowercase().cmp(b.to_lowercase())
47                    } else {
48                        a.cmp(&b)
49                    } {
50                        Ordering::Equal => None,
51                        o => Some(o),
52                    }
53                })
54                .unwrap_or_else(|| a.len().cmp(&b.len()))
55        }
56    }
57
58    fn in_ci<S: AsRef<str>>(&self, haystack: &[S]) -> bool {
59        let needle = self.as_ref();
60        haystack.iter().any(|s| s.eq_ci(needle))
61    }
62
63    fn starts_with_ci<S: AsRef<str>>(&self, prefix: S) -> bool {
64        let (subject, prefix) = (self.as_ref(), prefix.as_ref());
65
66        if let Some(start) = subject.get(..prefix.len()) {
67            start.eq_ci(prefix)
68        } else {
69            false
70        }
71    }
72
73    fn ends_with_ci<S: AsRef<str>>(&self, suffix: S) -> bool {
74        let (subject, suffix) = (self.as_ref(), suffix.as_ref());
75
76        if let Some(end) = subject
77            .len()
78            .checked_sub(suffix.len())
79            .and_then(|i| subject.get(i..))
80        {
81            end.eq_ci(suffix)
82        } else {
83            false
84        }
85    }
86
87    fn strip_prefix_ci<S: AsRef<str>>(&'a self, prefix: S) -> Option<&'a str> {
88        let prefix = prefix.as_ref();
89
90        if self.starts_with_ci(prefix) {
91            self.as_ref().get(prefix.len()..)
92        } else {
93            None
94        }
95    }
96
97    fn strip_suffix_ci<S: AsRef<str>>(&'a self, suffix: S) -> Option<&'a str> {
98        let suffix = suffix.as_ref();
99
100        if self.ends_with_ci(suffix) {
101            let subject = self.as_ref();
102
103            subject
104                .len()
105                .checked_sub(suffix.len())
106                .and_then(|i| subject.get(..i))
107        } else {
108            None
109        }
110    }
111}
112
113#[cfg(test)]
114mod test {
115    use super::*;
116
117    #[test]
118    fn eq_ci_test() {
119        assert!("".eq_ci(""));
120        assert!("abc".eq_ci("abc"));
121        assert!("abc".eq_ci("abC"));
122        assert!("!@#".eq_ci("!@#"));
123        assert!("p🥔tat🥔".eq_ci("P🥔TAT🥔"));
124
125        assert!(!"abcd".eq_ci("abc"));
126        assert!(!"abc".eq_ci("abcd"));
127        assert!(!"".eq_ci("🥔"));
128        assert!(!"🥔".eq_ci(""));
129        assert!(!"🥔".eq_ci("potato"));
130        assert!(!"potato".eq_ci("🥔"));
131        assert!(!"SS".eq_ci("ß"));
132        assert!(!"ß".eq_ci("S"));
133        assert!(!"S".eq_ci("ß"));
134    }
135
136    #[test]
137    #[ignore]
138    fn eq_ci_test_failing() {
139        // This is a known limitation. Documenting here for posteritity.
140        assert!("ß".eq_ci("SS"));
141    }
142
143    #[test]
144    fn starts_ends_with_ci_test() {
145        assert!("AbC".starts_with_ci("aB"));
146        assert!("AbC".ends_with_ci("Bc"));
147        assert!("AbC".starts_with_ci(""));
148        assert!("AbC".ends_with_ci(""));
149
150        assert!(!"🥔".starts_with_ci("a"));
151        assert!(!"🥔".ends_with_ci("a"));
152        assert!(!"abc".starts_with_ci("abcd"));
153        assert!(!"abc".ends_with_ci("abcd"));
154    }
155
156    #[test]
157    fn strip_prefix_suffix_ci_test() {
158        assert_eq!(Some("aBC"), "aBCXYz".strip_suffix_ci("xYz"));
159        assert_eq!(Some("XYz"), "aBCXYz".strip_prefix_ci("aBc"));
160        assert_eq!(Some("p🥔tat"), "p🥔tat🥔".strip_suffix_ci("🥔"));
161
162        assert_eq!(Some(""), "".strip_prefix_ci(""));
163    }
164
165    #[test]
166    fn cmp_ci_test() {
167        let mut data = vec![
168            "ddd", "aaa", "!", "aaaa", "aAA", "", "aaa", "CCC", "🥔", "Bbb",
169        ];
170
171        data.sort_by(|a, b| a.cmp_ci(b));
172
173        assert_eq!(
174            vec!["", "!", "aaa", "aAA", "aaa", "aaaa", "Bbb", "CCC", "ddd", "🥔"],
175            data,
176        );
177    }
178
179    #[test]
180    fn in_ci_test() {
181        assert!("B".in_ci(&["a", "b", "c"]));
182        assert!(!"d".in_ci(&["a", "b", "c"]));
183    }
184}