Skip to main content

clippy_utils/
str_utils.rs

1//! Utilities for analyzing and transforming strings.
2
3/// Dealing with string indices can be hard, this struct ensures that both the
4/// character and byte index are provided for correct indexing.
5///
6/// `char_index` and `byte_index` *are not guaranteed* to agree with each other.
7/// For ascii only text they are always equal, but multi byte characters
8/// make them diverge:
9///
10/// ```no_run
11/// # use clippy_utils::str_utils::{camel_case_until, StrIndex};
12/// // 'ö' is 1 character but 2 bytes in UTF-8, so the position after
13/// // "Abcöö" is character 5 but byte 7.
14/// assert_eq!(camel_case_until("Abc\u{f6}\u{f6}DD"), StrIndex::new(5, 7));
15/// ```
16#[derive(Debug, Default, PartialEq, Eq)]
17pub struct StrIndex {
18    /// The number of characters between the start of the `str` and this position
19    pub char_index: usize,
20    /// The number of bytes between the start of the `str` and this position
21    pub byte_index: usize,
22}
23
24impl StrIndex {
25    /// Creates a `StrIndex` from character and byte indices.
26    pub fn new(char_index: usize, byte_index: usize) -> Self {
27        Self { char_index, byte_index }
28    }
29}
30
31/// Returns the index of the character after the first camel-case component of `s`.
32///
33/// ```no_run
34/// # use clippy_utils::str_utils::{camel_case_until, StrIndex};
35/// assert_eq!(camel_case_until("AbcDef"), StrIndex::new(6, 6));
36/// assert_eq!(camel_case_until("ABCD"), StrIndex::new(0, 0));
37/// assert_eq!(camel_case_until("AbcDD"), StrIndex::new(3, 3));
38/// assert_eq!(camel_case_until("Abc\u{f6}\u{f6}DD"), StrIndex::new(5, 7));
39/// ```
40#[must_use]
41pub fn camel_case_until(s: &str) -> StrIndex {
42    let mut iter = s.char_indices().enumerate();
43    if let Some((_char_index, (_, first))) = iter.next() {
44        if !first.is_uppercase() {
45            return StrIndex::new(0, 0);
46        }
47    } else {
48        return StrIndex::new(0, 0);
49    }
50    let mut up = true;
51    let mut last_index = StrIndex::new(0, 0);
52    for (char_index, (byte_index, c)) in iter {
53        if up {
54            if c.is_lowercase() {
55                up = false;
56            } else {
57                return last_index;
58            }
59        } else if c.is_uppercase() {
60            up = true;
61            last_index.byte_index = byte_index;
62            last_index.char_index = char_index;
63        } else if !c.is_lowercase() {
64            return StrIndex::new(char_index, byte_index);
65        }
66    }
67
68    if up {
69        last_index
70    } else {
71        StrIndex::new(s.chars().count(), s.len())
72    }
73}
74
75/// Returns index of the first camel-case component of `s`.
76///
77/// ```no_run
78/// # use clippy_utils::str_utils::{camel_case_start, StrIndex};
79/// assert_eq!(camel_case_start("AbcDef"), StrIndex::new(0, 0));
80/// assert_eq!(camel_case_start("abcDef"), StrIndex::new(3, 3));
81/// assert_eq!(camel_case_start("ABCD"), StrIndex::new(4, 4));
82/// assert_eq!(camel_case_start("abcd"), StrIndex::new(4, 4));
83/// assert_eq!(camel_case_start("\u{f6}\u{f6}cd"), StrIndex::new(4, 6));
84/// ```
85#[must_use]
86pub fn camel_case_start(s: &str) -> StrIndex {
87    camel_case_start_from_idx(s, 0)
88}
89
90/// Returns `StrIndex` of the last camel-case component of `s[idx..]`.
91///
92/// ```no_run
93/// # use clippy_utils::str_utils::{camel_case_start_from_idx, StrIndex};
94/// assert_eq!(camel_case_start_from_idx("AbcDef", 0), StrIndex::new(0, 0));
95/// assert_eq!(camel_case_start_from_idx("AbcDef", 1), StrIndex::new(3, 3));
96/// assert_eq!(camel_case_start_from_idx("AbcDefGhi", 0), StrIndex::new(0, 0));
97/// assert_eq!(camel_case_start_from_idx("AbcDefGhi", 1), StrIndex::new(3, 3));
98/// assert_eq!(camel_case_start_from_idx("Abcdefg", 1), StrIndex::new(7, 7));
99/// ```
100pub fn camel_case_start_from_idx(s: &str, start_idx: usize) -> StrIndex {
101    let char_count = s.chars().count();
102    let range = 0..char_count;
103    let mut iter = range.rev().zip(s.char_indices().rev());
104    if let Some((_, (_, first))) = iter.next() {
105        if !first.is_lowercase() {
106            return StrIndex::new(char_count, s.len());
107        }
108    } else {
109        return StrIndex::new(char_count, s.len());
110    }
111
112    let mut down = true;
113    let mut last_index = StrIndex::new(char_count, s.len());
114    for (char_index, (byte_index, c)) in iter {
115        if byte_index < start_idx {
116            break;
117        }
118        if down {
119            if c.is_uppercase() {
120                down = false;
121                last_index.byte_index = byte_index;
122                last_index.char_index = char_index;
123            } else if !c.is_lowercase() {
124                return last_index;
125            }
126        } else if c.is_lowercase() {
127            down = true;
128        } else if c.is_uppercase() {
129            last_index.byte_index = byte_index;
130            last_index.char_index = char_index;
131        } else {
132            return last_index;
133        }
134    }
135
136    last_index
137}
138
139/// Get the indexes of camel case components of a string `s`
140///
141/// ```no_run
142/// # use clippy_utils::str_utils::{camel_case_indices, StrIndex};
143/// assert_eq!(
144///     camel_case_indices("AbcDef"),
145///     vec![StrIndex::new(0, 0), StrIndex::new(3, 3), StrIndex::new(6, 6)]
146/// );
147/// assert_eq!(
148///     camel_case_indices("abcDef"),
149///     vec![StrIndex::new(3, 3), StrIndex::new(6, 6)]
150/// );
151/// ```
152pub fn camel_case_indices(s: &str) -> Vec<StrIndex> {
153    let mut result = Vec::new();
154    let mut str_idx = camel_case_start(s);
155
156    while str_idx.byte_index < s.len() {
157        let next_idx = str_idx.byte_index + 1;
158        result.push(str_idx);
159        str_idx = camel_case_start_from_idx(s, next_idx);
160    }
161    result.push(str_idx);
162
163    result
164}
165
166/// Split camel case string into a vector of its components
167///
168/// ```no_run
169/// # use clippy_utils::str_utils::{camel_case_split, StrIndex};
170/// assert_eq!(camel_case_split("AbcDef"), vec!["Abc", "Def"]);
171/// ```
172pub fn camel_case_split(s: &str) -> Vec<&str> {
173    let mut offsets = camel_case_indices(s)
174        .iter()
175        .map(|e| e.byte_index)
176        .collect::<Vec<usize>>();
177    if offsets[0] != 0 {
178        offsets.insert(0, 0);
179    }
180
181    offsets.windows(2).map(|w| &s[w[0]..w[1]]).collect()
182}
183
184/// Dealing with string comparison can be complicated, this struct ensures that both the
185/// character and byte count are provided for correct indexing.
186///
187/// `char_count` and `byte_count` *are not guaranteed* to agree with each other.
188#[derive(Debug, Default, PartialEq, Eq)]
189pub struct StrCount {
190    /// The number of characters in a string.
191    pub char_count: usize,
192    /// The number of bytes in a string.
193    pub byte_count: usize,
194}
195
196impl StrCount {
197    /// Creates a `StrCount` from character and byte counts.
198    pub fn new(char_count: usize, byte_count: usize) -> Self {
199        Self { char_count, byte_count }
200    }
201}
202
203/// Returns the number of chars that match from the start
204///
205/// ```no_run
206/// # use clippy_utils::str_utils::{count_match_start, StrCount};
207/// assert_eq!(count_match_start("hello_mouse", "hello_penguin"), StrCount::new(6, 6));
208/// assert_eq!(count_match_start("hello_clippy", "bye_bugs"), StrCount::new(0, 0));
209/// assert_eq!(count_match_start("hello_world", "hello_world"), StrCount::new(11, 11));
210/// assert_eq!(count_match_start("T\u{f6}ffT\u{f6}ff", "T\u{f6}ff"), StrCount::new(4, 5));
211/// ```
212#[must_use]
213pub fn count_match_start(str1: &str, str2: &str) -> StrCount {
214    // (char_index, char1)
215    let char_count = str1.chars().count();
216    let iter1 = (0..=char_count).zip(str1.chars());
217    // (byte_index, char2)
218    let iter2 = str2.char_indices();
219
220    iter1
221        .zip(iter2)
222        .take_while(|((_, c1), (_, c2))| c1 == c2)
223        .last()
224        .map_or_else(StrCount::default, |((char_index, _), (byte_index, character))| {
225            StrCount::new(char_index + 1, byte_index + character.len_utf8())
226        })
227}
228
229/// Returns the number of chars and bytes that match from the end
230///
231/// ```no_run
232/// # use clippy_utils::str_utils::{count_match_end, StrCount};
233/// assert_eq!(count_match_end("hello_cat", "bye_cat"), StrCount::new(4, 4));
234/// assert_eq!(count_match_end("if_item_thing", "enum_value"), StrCount::new(0, 0));
235/// assert_eq!(count_match_end("Clippy", "Clippy"), StrCount::new(6, 6));
236/// assert_eq!(count_match_end("MyT\u{f6}ff", "YourT\u{f6}ff"), StrCount::new(4, 5));
237/// ```
238#[must_use]
239pub fn count_match_end(str1: &str, str2: &str) -> StrCount {
240    let char_count = str1.chars().count();
241    if char_count == 0 {
242        return StrCount::default();
243    }
244
245    // (char_index, char1)
246    let iter1 = (0..char_count).rev().zip(str1.chars().rev());
247    // (byte_index, char2)
248    let byte_count = str2.len();
249    let iter2 = str2.char_indices().rev();
250
251    iter1
252        .zip(iter2)
253        .take_while(|((_, c1), (_, c2))| c1 == c2)
254        .last()
255        .map_or_else(StrCount::default, |((char_index, _), (byte_index, _))| {
256            StrCount::new(char_count - char_index, byte_count - byte_index)
257        })
258}
259
260/// Returns a `snake_case` version of the input
261/// ```no_run
262/// use clippy_utils::str_utils::to_snake_case;
263/// assert_eq!(to_snake_case("AbcDef"), "abc_def");
264/// assert_eq!(to_snake_case("ABCD"), "a_b_c_d");
265/// assert_eq!(to_snake_case("AbcDD"), "abc_d_d");
266/// assert_eq!(to_snake_case("Abc1DD"), "abc1_d_d");
267/// ```
268pub fn to_snake_case(name: &str) -> String {
269    let mut s = String::new();
270    for (i, c) in name.chars().enumerate() {
271        if c.is_uppercase() {
272            // characters without capitalization are considered lowercase
273            if i != 0 {
274                s.push('_');
275            }
276            s.extend(c.to_lowercase());
277        } else {
278            s.push(c);
279        }
280    }
281    s
282}
283/// Returns a `CamelCase` version of the input
284/// ```no_run
285/// use clippy_utils::str_utils::to_camel_case;
286/// assert_eq!(to_camel_case("abc_def"), "AbcDef");
287/// assert_eq!(to_camel_case("a_b_c_d"), "ABCD");
288/// assert_eq!(to_camel_case("abc_d_d"), "AbcDD");
289/// assert_eq!(to_camel_case("abc1_d_d"), "Abc1DD");
290/// ```
291pub fn to_camel_case(item_name: &str) -> String {
292    let mut s = String::new();
293    let mut up = true;
294    for c in item_name.chars() {
295        if c.is_uppercase() {
296            // we only turn snake case text into CamelCase
297            return item_name.to_string();
298        }
299        if c == '_' {
300            up = true;
301            continue;
302        }
303        if up {
304            up = false;
305            s.extend(c.to_uppercase());
306        } else {
307            s.push(c);
308        }
309    }
310    s
311}
312
313#[cfg(test)]
314mod test {
315    use super::*;
316
317    #[test]
318    fn camel_case_start_full() {
319        assert_eq!(camel_case_start("AbcDef"), StrIndex::new(0, 0));
320        assert_eq!(camel_case_start("Abc"), StrIndex::new(0, 0));
321        assert_eq!(camel_case_start("ABcd"), StrIndex::new(0, 0));
322        assert_eq!(camel_case_start("ABcdEf"), StrIndex::new(0, 0));
323        assert_eq!(camel_case_start("AabABcd"), StrIndex::new(0, 0));
324    }
325
326    #[test]
327    fn camel_case_start_partial() {
328        assert_eq!(camel_case_start("abcDef"), StrIndex::new(3, 3));
329        assert_eq!(camel_case_start("aDbc"), StrIndex::new(1, 1));
330        assert_eq!(camel_case_start("aabABcd"), StrIndex::new(3, 3));
331        assert_eq!(camel_case_start("\u{f6}\u{f6}AabABcd"), StrIndex::new(2, 4));
332    }
333
334    #[test]
335    fn camel_case_start_not() {
336        assert_eq!(camel_case_start("AbcDef_"), StrIndex::new(7, 7));
337        assert_eq!(camel_case_start("AbcDD"), StrIndex::new(5, 5));
338        assert_eq!(camel_case_start("all_small"), StrIndex::new(9, 9));
339        assert_eq!(camel_case_start("\u{f6}_all_small"), StrIndex::new(11, 12));
340    }
341
342    #[test]
343    fn camel_case_start_caps() {
344        assert_eq!(camel_case_start("ABCD"), StrIndex::new(4, 4));
345    }
346
347    #[test]
348    fn camel_case_until_full() {
349        assert_eq!(camel_case_until("AbcDef"), StrIndex::new(6, 6));
350        assert_eq!(camel_case_until("Abc"), StrIndex::new(3, 3));
351        assert_eq!(camel_case_until("Abc\u{f6}\u{f6}\u{f6}"), StrIndex::new(6, 9));
352    }
353
354    #[test]
355    fn camel_case_until_not() {
356        assert_eq!(camel_case_until("abcDef"), StrIndex::new(0, 0));
357        assert_eq!(camel_case_until("aDbc"), StrIndex::new(0, 0));
358    }
359
360    #[test]
361    fn camel_case_until_partial() {
362        assert_eq!(camel_case_until("AbcDef_"), StrIndex::new(6, 6));
363        assert_eq!(camel_case_until("CallTypeC"), StrIndex::new(8, 8));
364        assert_eq!(camel_case_until("AbcDD"), StrIndex::new(3, 3));
365        assert_eq!(camel_case_until("Abc\u{f6}\u{f6}DD"), StrIndex::new(5, 7));
366    }
367
368    #[test]
369    fn until_caps() {
370        assert_eq!(camel_case_until("ABCD"), StrIndex::new(0, 0));
371    }
372
373    #[test]
374    fn camel_case_start_from_idx_full() {
375        assert_eq!(camel_case_start_from_idx("AbcDef", 0), StrIndex::new(0, 0));
376        assert_eq!(camel_case_start_from_idx("AbcDef", 1), StrIndex::new(3, 3));
377        assert_eq!(camel_case_start_from_idx("AbcDef", 4), StrIndex::new(6, 6));
378        assert_eq!(camel_case_start_from_idx("AbcDefGhi", 0), StrIndex::new(0, 0));
379        assert_eq!(camel_case_start_from_idx("AbcDefGhi", 1), StrIndex::new(3, 3));
380        assert_eq!(camel_case_start_from_idx("Abcdefg", 1), StrIndex::new(7, 7));
381    }
382
383    #[test]
384    fn camel_case_indices_full() {
385        assert_eq!(camel_case_indices("Abc\u{f6}\u{f6}DD"), vec![StrIndex::new(7, 9)]);
386    }
387
388    #[test]
389    fn camel_case_split_full() {
390        assert_eq!(camel_case_split("A"), vec!["A"]);
391        assert_eq!(camel_case_split("AbcDef"), vec!["Abc", "Def"]);
392        assert_eq!(camel_case_split("Abc"), vec!["Abc"]);
393        assert_eq!(camel_case_split("abcDef"), vec!["abc", "Def"]);
394        assert_eq!(
395            camel_case_split("\u{f6}\u{f6}AabABcd"),
396            vec!["\u{f6}\u{f6}", "Aab", "A", "Bcd"]
397        );
398    }
399}