Skip to main content

harper_core/
char_string.rs

1use crate::char_ext::CharExt;
2use std::borrow::Cow;
3use std::iter::Iterator;
4
5use smallvec::SmallVec;
6
7// TODO: remove this when `SmallVec` allows retrieving this value in a const context.
8pub(crate) const CHAR_STRING_INLINE_SIZE: usize = 16;
9
10/// A char sequence that improves cache locality.
11/// Most English words are fewer than 12 characters.
12pub type CharString = SmallVec<[char; CHAR_STRING_INLINE_SIZE]>;
13
14mod private {
15    pub trait Sealed {}
16
17    impl Sealed for [char] {}
18}
19
20/// Extensions to character sequences that make them easier to wrangle.
21pub trait CharStringExt: private::Sealed {
22    /// Convert all characters to lowercase, returning a new owned vector if any changes were made.
23    fn to_lower(&'_ self) -> Cow<'_, [char]>;
24
25    /// Normalize the character sequence according to the dictionary's standard character set.
26    fn normalized(&'_ self) -> Cow<'_, [char]>;
27
28    /// Convert the character sequence to a String.
29    fn to_string(&self) -> String;
30
31    /// Case-insensitive comparison with a character slice, assuming the right-hand side is lowercase ASCII.
32    /// Only normalizes the left side to lowercase and avoids allocations.
33    fn eq_ch(&self, other: &[char]) -> bool;
34
35    /// Case-insensitive comparison with a string slice, assuming the right-hand side is lowercase ASCII.
36    /// Only normalizes the left side to lowercase and avoids allocations.
37    fn eq_str(&self, other: &str) -> bool;
38
39    /// Case-insensitive comparison with any of a list of string slices, assuming the right-hand side is lowercase ASCII.
40    /// Only normalizes the left side to lowercase and avoids allocations.
41    fn eq_any_ignore_ascii_case_str(&self, others: &[&str]) -> bool;
42
43    /// Case-insensitive comparison with any of a list of character slices, assuming the right-hand side is lowercase ASCII.
44    /// Only normalizes the left side to lowercase and avoids allocations.
45    fn eq_any_ignore_ascii_case_chars(&self, others: &[&[char]]) -> bool;
46
47    /// Case-insensitive check if the string starts with the given ASCII prefix.
48    /// The prefix is assumed to be lowercase.
49    fn starts_with_ignore_ascii_case_str(&self, prefix: &str) -> bool;
50
51    /// Case-insensitive check if the string starts with any of the given ASCII prefixes.
52    /// The prefixes are assumed to be lowercase.
53    fn starts_with_any_ignore_ascii_case_str(&self, prefixes: &[&str]) -> bool;
54
55    /// Case-insensitive check if the string ends with the given ASCII suffix.
56    /// The suffix is assumed to be lowercase.
57    fn ends_with_ignore_ascii_case_chars(&self, suffix: &[char]) -> bool;
58
59    /// Case-insensitive check if the string ends with the given ASCII suffix.
60    /// The suffix is assumed to be lowercase.
61    fn ends_with_ignore_ascii_case_str(&self, suffix: &str) -> bool;
62
63    /// Case-insensitive check if the string ends with any of the given ASCII suffixes.
64    /// The suffixes are assumed to be lowercase.
65    fn ends_with_any_ignore_ascii_case_chars(&self, suffixes: &[&[char]]) -> bool;
66
67    /// Check if the string contains any vowels
68    fn contains_vowel(&self) -> bool;
69
70    /// Strip a prefix from the string, case-insensitively
71    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    /// Convert a given character sequence to the standard character set
92    /// the dictionary is in.
93    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        // Assert that the right-hand side is all-lowercase as required
109        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        // Assert that the right-hand side is all-lowercase as required
136        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}