1use crate::char_ext::CharExt;
2use std::borrow::Cow;
3use std::iter::Iterator;
4
5use smallvec::SmallVec;
6
7pub(crate) const CHAR_STRING_INLINE_SIZE: usize = 16;
9
10pub type CharString = SmallVec<[char; CHAR_STRING_INLINE_SIZE]>;
13
14mod private {
15 pub trait Sealed {}
16
17 impl Sealed for [char] {}
18}
19
20pub trait CharStringExt: private::Sealed {
22 fn to_lower(&'_ self) -> Cow<'_, [char]>;
24
25 fn normalized(&'_ self) -> Cow<'_, [char]>;
27
28 fn to_string(&self) -> String;
30
31 fn eq_ch(&self, other: &[char]) -> bool;
34
35 fn eq_str(&self, other: &str) -> bool;
38
39 fn eq_any_ignore_ascii_case_str(&self, others: &[&str]) -> bool;
42
43 fn eq_any_ignore_ascii_case_chars(&self, others: &[&[char]]) -> bool;
46
47 fn starts_with_ignore_ascii_case_str(&self, prefix: &str) -> bool;
50
51 fn starts_with_any_ignore_ascii_case_str(&self, prefixes: &[&str]) -> bool;
54
55 fn ends_with_ignore_ascii_case_chars(&self, suffix: &[char]) -> bool;
58
59 fn ends_with_ignore_ascii_case_str(&self, suffix: &str) -> bool;
62
63 fn ends_with_any_ignore_ascii_case_chars(&self, suffixes: &[&[char]]) -> bool;
66
67 fn contains_vowel(&self) -> bool;
69
70 fn strip_prefix_ignore_ascii_case_chars(&self, prefix: &[char]) -> Option<&[char]>;
72}
73
74impl CharStringExt for [char] {
75 fn to_lower(&'_ self) -> Cow<'_, [char]> {
76 if self.iter().all(|c| c.is_lowercase()) {
77 return Cow::Borrowed(self);
78 }
79
80 let mut out = CharString::with_capacity(self.len());
81
82 out.extend(self.iter().flat_map(|v| v.to_lowercase()));
83
84 Cow::Owned(out.to_vec())
85 }
86
87 fn to_string(&self) -> String {
88 self.iter().collect()
89 }
90
91 fn normalized(&'_ self) -> Cow<'_, [char]> {
94 if self.as_ref().iter().any(|c| c.normalized() != *c) {
95 Cow::Owned(
96 self.as_ref()
97 .iter()
98 .copied()
99 .map(|c| c.normalized())
100 .collect(),
101 )
102 } else {
103 Cow::Borrowed(self)
104 }
105 }
106
107 fn eq_str(&self, other: &str) -> bool {
108 debug_assert!(
110 other
111 .chars()
112 .all(|c| c.is_ascii_lowercase() || !c.is_ascii_alphabetic()),
113 "eq_str requires right-hand side to be lowercase ASCII, but got: {:?}",
114 other
115 );
116
117 let mut chit = self.iter();
118 let mut strit = other.chars();
119
120 loop {
121 let (c, s) = (chit.next(), strit.next());
122 match (c, s) {
123 (Some(c), Some(s)) => {
124 if c.to_ascii_lowercase() != s {
125 return false;
126 }
127 }
128 (None, None) => return true,
129 _ => return false,
130 }
131 }
132 }
133
134 fn eq_ch(&self, other: &[char]) -> bool {
135 debug_assert!(
137 other
138 .iter()
139 .all(|c| c.is_ascii_lowercase() || !c.is_ascii_alphabetic()),
140 "eq_ch requires right-hand side to be lowercase ASCII, but got: {:?}",
141 other
142 );
143
144 self.len() == other.len()
145 && self
146 .iter()
147 .zip(other.iter())
148 .all(|(a, b)| a.to_ascii_lowercase() == *b)
149 }
150
151 fn eq_any_ignore_ascii_case_str(&self, others: &[&str]) -> bool {
152 others.iter().any(|str| self.eq_str(str))
153 }
154
155 fn eq_any_ignore_ascii_case_chars(&self, others: &[&[char]]) -> bool {
156 others.iter().any(|chars| self.eq_ch(chars))
157 }
158
159 fn starts_with_ignore_ascii_case_str(&self, prefix: &str) -> bool {
160 let prefix_len = prefix.chars().count();
161 if self.len() < prefix_len {
162 return false;
163 }
164 self.iter()
165 .take(prefix_len)
166 .zip(prefix.chars())
167 .all(|(a, b)| a.to_ascii_lowercase() == b)
168 }
169
170 fn starts_with_any_ignore_ascii_case_str(&self, prefixes: &[&str]) -> bool {
171 prefixes
172 .iter()
173 .any(|prefix| self.starts_with_ignore_ascii_case_str(prefix))
174 }
175
176 fn ends_with_ignore_ascii_case_str(&self, suffix: &str) -> bool {
177 let suffix_len = suffix.chars().count();
178 if self.len() < suffix_len {
179 return false;
180 }
181 self.iter()
182 .rev()
183 .take(suffix_len)
184 .rev()
185 .zip(suffix.chars())
186 .all(|(a, b)| a.to_ascii_lowercase() == b)
187 }
188
189 fn ends_with_ignore_ascii_case_chars(&self, suffix: &[char]) -> bool {
190 let suffix_len = suffix.len();
191 if self.len() < suffix_len {
192 return false;
193 }
194 self.iter()
195 .rev()
196 .take(suffix_len)
197 .rev()
198 .zip(suffix.iter())
199 .all(|(a, b)| a.to_ascii_lowercase() == *b)
200 }
201
202 fn ends_with_any_ignore_ascii_case_chars(&self, suffixes: &[&[char]]) -> bool {
203 suffixes
204 .iter()
205 .any(|suffix| self.ends_with_ignore_ascii_case_chars(suffix))
206 }
207
208 fn contains_vowel(&self) -> bool {
209 self.iter().any(|c| c.is_vowel())
210 }
211
212 fn strip_prefix_ignore_ascii_case_chars(&self, prefix: &[char]) -> Option<&[char]> {
213 (self.len() >= prefix.len()
214 && self
215 .iter()
216 .zip(prefix)
217 .all(|(a, b)| a.eq_ignore_ascii_case(b)))
218 .then_some(&self[prefix.len()..])
219 }
220}
221
222macro_rules! char_string {
223 ($string:literal) => {{
224 use crate::char_string::CharString;
225
226 $string.chars().collect::<CharString>()
227 }};
228}
229
230pub(crate) use char_string;
231
232#[cfg(test)]
233mod tests {
234 use super::CharStringExt;
235
236 #[test]
237 fn eq_ignore_ascii_case_chars_matches_lowercase() {
238 assert!(['H', 'e', 'l', 'l', 'o'].eq_ch(&['h', 'e', 'l', 'l', 'o']));
239 }
240
241 #[test]
242 fn eq_ignore_ascii_case_chars_does_not_match_different_word() {
243 assert!(!['H', 'e', 'l', 'l', 'o'].eq_ch(&['w', 'o', 'r', 'l', 'd']));
244 }
245
246 #[test]
247 fn eq_ignore_ascii_case_str_matches_lowercase() {
248 assert!(['H', 'e', 'l', 'l', 'o'].eq_str("hello"));
249 }
250
251 #[test]
252 fn eq_ignore_ascii_case_str_does_not_match_different_word() {
253 assert!(!['H', 'e', 'l', 'l', 'o'].eq_str("world"));
254 }
255
256 #[test]
257 fn ends_with_ignore_ascii_case_chars_matches_suffix() {
258 assert!(['H', 'e', 'l', 'l', 'o'].ends_with_ignore_ascii_case_chars(&['l', 'o']));
259 }
260
261 #[test]
262 fn ends_with_ignore_ascii_case_chars_does_not_match_different_suffix() {
263 assert!(
264 !['H', 'e', 'l', 'l', 'o']
265 .ends_with_ignore_ascii_case_chars(&['w', 'o', 'r', 'l', 'd'])
266 );
267 }
268
269 #[test]
270 fn ends_with_ignore_ascii_case_str_matches_suffix() {
271 assert!(['H', 'e', 'l', 'l', 'o'].ends_with_ignore_ascii_case_str("lo"));
272 }
273
274 #[test]
275 fn ends_with_ignore_ascii_case_str_does_not_match_different_suffix() {
276 assert!(!['H', 'e', 'l', 'l', 'o'].ends_with_ignore_ascii_case_str("world"));
277 }
278
279 #[test]
280 fn differs_only_by_length_1() {
281 assert!(!['b', 'b'].eq_str("b"));
282 }
283
284 #[test]
285 fn differs_only_by_length_2() {
286 assert!(!['c'].eq_str("cc"));
287 }
288
289 #[test]
290 #[should_panic]
291 fn right_side_must_be_all_lowercase_str() {
292 assert!(['c'].eq_str("C"))
293 }
294
295 #[test]
296 #[should_panic]
297 fn right_side_must_be_all_lowercase_ch() {
298 assert!(['c'].eq_ch(&['C']))
299 }
300}