convert_case/
words.rs

1use crate::Case;
2
3#[cfg(feature = "random")]
4use rand::prelude::*;
5
6pub(super) struct Words {
7    words: Vec<String>,
8}
9
10impl Words {
11    pub fn new(name: &str) -> Self {
12        let words = name
13            .split(|c| "-_ ".contains(c))
14            .flat_map(Self::split_camel)
15            .filter(|s| !s.is_empty())
16            .collect();
17        Words { words }
18    }
19
20    pub fn from_casing(name: &str, case: Case) -> Self {
21        use Case::*;
22        let words = match case {
23            Title | Upper | Lower | Toggle | Alternating => name
24                .split_ascii_whitespace()
25                .map(ToString::to_string)
26                .collect(),
27            Kebab | Cobol | Train => name
28                .split('-')
29                .map(ToString::to_string)
30                .filter(|s| !s.is_empty())
31                .collect(),
32            Snake | UpperSnake | ScreamingSnake => name
33                .split('_')
34                .map(ToString::to_string)
35                .filter(|s| !s.is_empty())
36                .collect(),
37            Pascal | Camel | UpperCamel => Self::split_camel(name),
38            Flat | UpperFlat => vec![name.to_string()],
39
40            // Same behavior as title, upper, etc.
41            #[cfg(feature = "random")]
42            Random | PseudoRandom => name
43                .split_ascii_whitespace()
44                .map(ToString::to_string)
45                .collect(),
46        };
47        Self { words }
48    }
49
50    fn split_camel(name: &str) -> Vec<String> {
51        let left_iter = name.chars();
52        let mid_iter = name.chars().skip(1);
53        let right_iter = name.chars().skip(2);
54
55        let mut split_indices = left_iter
56            .zip(mid_iter)
57            .zip(right_iter)
58            .enumerate()
59            .filter(|(_, ((f, s), t))| Self::three_char_is_boundary(*f, *s, *t))
60            .map(|(i, _)| i + 1)
61            .collect::<Vec<usize>>();
62
63        // Check for boundary in the last two characters
64        // Can be rewritten nicer (use fold)
65        let mut backwards_seek = name.chars().rev();
66        let last = backwards_seek.next();
67        let second_last = backwards_seek.next();
68        if let (Some(a), Some(b)) = (second_last, last) {
69            if Self::two_char_is_boundary(a, b) {
70                split_indices.push(name.len() - 1);
71            }
72        }
73
74        Self::split_at_indices(name, split_indices)
75    }
76
77    /// Allowed boundaries are (lower upper) (digit (!digit and !punc)) ((!digit and !punc) digit).
78    fn two_char_is_boundary(f: char, s: char) -> bool {
79        (f.is_lowercase() && s.is_uppercase())
80            || (f.is_ascii_digit() && !(s.is_ascii_digit() || s.is_ascii_punctuation()))
81            || (!(f.is_ascii_digit() || f.is_ascii_punctuation()) && s.is_ascii_digit())
82    }
83
84    /// Checks if three characters are the end of an acronym, otherwise
85    /// calls `two_char_is_boundary`.
86    fn three_char_is_boundary(f: char, s: char, t: char) -> bool {
87        (f.is_uppercase() && s.is_uppercase() && t.is_lowercase())
88            || Self::two_char_is_boundary(f, s)
89    }
90
91    fn split_at_indices(name: &str, indices: Vec<usize>) -> Vec<String> {
92        let mut words = Vec::new();
93
94        let mut first = name;
95        let mut second;
96        for &x in indices.iter().rev() {
97            let pair = first.split_at(x);
98            first = pair.0;
99            second = pair.1;
100            words.push(second);
101        }
102        words.push(first);
103
104        words.iter().rev().map(ToString::to_string).collect()
105    }
106
107    pub fn into_case(mut self, case: Case) -> String {
108        use Case::*;
109        match case {
110            Camel => {
111                self.make_camel_case();
112                self.join("")
113            }
114            Title => {
115                self.capitalize_first_letter();
116                self.join(" ")
117            }
118            Pascal | UpperCamel => {
119                self.capitalize_first_letter();
120                self.join("")
121            }
122            Toggle => {
123                self.lower_first_letter();
124                self.join(" ")
125            }
126            Snake => {
127                self.make_lowercase();
128                self.join("_")
129            }
130            Cobol => {
131                self.make_uppercase();
132                self.join("-")
133            }
134            Kebab => {
135                self.make_lowercase();
136                self.join("-")
137            }
138            UpperSnake | ScreamingSnake => {
139                self.make_uppercase();
140                self.join("_")
141            }
142            Lower => {
143                self.make_lowercase();
144                self.join(" ")
145            }
146            Upper => {
147                self.make_uppercase();
148                self.join(" ")
149            }
150            Flat => {
151                self.make_lowercase();
152                self.join("")
153            }
154            Train => {
155                self.capitalize_first_letter();
156                self.join("-")
157            }
158            UpperFlat => {
159                self.make_uppercase();
160                self.join("")
161            }
162            Alternating => {
163                self.make_alternating();
164                self.join(" ")
165            }
166            #[cfg(feature = "random")]
167            Random => {
168                self.randomize();
169                self.join(" ")
170            }
171            #[cfg(feature = "random")]
172            PseudoRandom => {
173                self.pseudo_randomize();
174                self.join(" ")
175            }
176        }
177    }
178
179    // Randomly picks whether to be upper case or lower case
180    #[cfg(feature = "random")]
181    fn randomize(&mut self) {
182        let mut rng = rand::thread_rng();
183        self.words = self
184            .words
185            .iter()
186            .map(|word| {
187                word.chars()
188                    .map(|letter| {
189                        if rng.gen::<f32>() > 0.5 {
190                            letter.to_uppercase().to_string()
191                        } else {
192                            letter.to_lowercase().to_string()
193                        }
194                    })
195                    .collect()
196            })
197            .collect();
198    }
199
200    // Randomly selects patterns: [upper, lower] or [lower, upper]
201    // for a more random feeling pattern.
202    #[cfg(feature = "random")]
203    fn pseudo_randomize(&mut self) {
204        let mut rng = rand::thread_rng();
205
206        // Keeps track of when to alternate
207        let mut alt: Option<bool> = None;
208        self.words = self
209            .words
210            .iter()
211            .map(|word| {
212                word.chars()
213                    .map(|letter| {
214                        match alt {
215                            // No existing pattern, start one
216                            None => {
217                                if rng.gen::<f32>() > 0.5 {
218                                    alt = Some(false); // Make the next char lower
219                                    letter.to_uppercase().to_string()
220                                } else {
221                                    alt = Some(true); // Make the next char upper
222                                    letter.to_lowercase().to_string()
223                                }
224                            }
225                            // Existing pattern, do what it says
226                            Some(upper) => {
227                                alt = None;
228                                if upper {
229                                    letter.to_uppercase().to_string()
230                                } else {
231                                    letter.to_lowercase().to_string()
232                                }
233                            }
234                        }
235                    })
236                    .collect()
237            })
238            .collect();
239    }
240
241    fn make_camel_case(&mut self) {
242        self.words = self
243            .words
244            .iter()
245            .enumerate()
246            .map(|(i, word)| {
247                if i != 0 {
248                    let mut chars = word.chars();
249                    if let Some(a) = chars.next() {
250                        a.to_uppercase()
251                            .chain(chars.as_str().to_lowercase().chars())
252                            .collect()
253                    } else {
254                        String::new()
255                    }
256                } else {
257                    word.to_lowercase()
258                }
259            })
260            .collect();
261    }
262
263    fn make_alternating(&mut self) {
264        let mut upper = false;
265        self.words = self
266            .words
267            .iter()
268            .map(|word| {
269                word.chars()
270                    .map(|letter| {
271                        if letter.is_uppercase() || letter.is_lowercase() {
272                            if upper {
273                                upper = false;
274                                letter.to_uppercase().to_string()
275                            } else {
276                                upper = true;
277                                letter.to_lowercase().to_string()
278                            }
279                        } else {
280                            letter.to_string()
281                        }
282                    })
283                    .collect()
284            })
285            .collect();
286    }
287
288    fn make_uppercase(&mut self) {
289        self.words = self.words.iter().map(|word| word.to_uppercase()).collect();
290    }
291
292    fn make_lowercase(&mut self) {
293        self.words = self.words.iter().map(|word| word.to_lowercase()).collect();
294    }
295
296    fn capitalize_first_letter(&mut self) {
297        self.words = self
298            .words
299            .iter()
300            .map(|word| {
301                let mut chars = word.chars();
302                if let Some(a) = chars.next() {
303                    a.to_uppercase()
304                        .chain(chars.as_str().to_lowercase().chars())
305                        .collect()
306                } else {
307                    String::new()
308                }
309            })
310            .collect();
311    }
312
313    fn lower_first_letter(&mut self) {
314        self.words = self
315            .words
316            .iter()
317            .map(|word| {
318                let mut chars = word.chars();
319                if let Some(a) = chars.next() {
320                    a.to_lowercase()
321                        .chain(chars.as_str().to_uppercase().chars())
322                        .collect()
323                } else {
324                    String::new()
325                }
326            })
327            .collect();
328    }
329
330    // Alternative: construct [my, -, variable, -, name] then collect
331    fn join(self, delim: &str) -> String {
332        self.words
333            .iter()
334            .enumerate()
335            .map(|(i, val)| {
336                if i == 0 {
337                    val.to_owned()
338                } else {
339                    delim.to_owned() + val
340                }
341            })
342            .collect()
343    }
344}
345
346#[cfg(test)]
347mod test {
348    use super::*;
349
350    #[test]
351    fn correct_two_char_boundaries() {
352        assert!(!Words::two_char_is_boundary('a', 'a'));
353        assert!(Words::two_char_is_boundary('a', 'A'));
354        assert!(Words::two_char_is_boundary('a', '5'));
355        assert!(!Words::two_char_is_boundary('a', ','));
356        assert!(!Words::two_char_is_boundary('A', 'A'));
357        assert!(!Words::two_char_is_boundary('A', 'a'));
358        assert!(Words::two_char_is_boundary('A', '5'));
359        assert!(!Words::two_char_is_boundary('A', ','));
360        assert!(Words::two_char_is_boundary('5', 'a'));
361        assert!(Words::two_char_is_boundary('5', 'A'));
362        assert!(!Words::two_char_is_boundary('5', '5'));
363        assert!(!Words::two_char_is_boundary('5', ','));
364        assert!(!Words::two_char_is_boundary(',', 'a'));
365        assert!(!Words::two_char_is_boundary(',', 'A'));
366        assert!(!Words::two_char_is_boundary(',', '5'));
367        assert!(!Words::two_char_is_boundary(',', ','));
368    }
369
370    #[test]
371    fn correct_three_char_boundaries() {
372        assert!(Words::three_char_is_boundary('A', 'A', 'a'));
373        assert!(!Words::three_char_is_boundary('A', 'a', 'a'));
374        assert!(!Words::three_char_is_boundary('A', 'a', 'A'));
375        assert!(!Words::three_char_is_boundary('A', 'A', '3'));
376    }
377}