initiative_core/utils/
case_insensitive_str.rs1use 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 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}