convert_case/
pattern.rs

1//! Functions for transforming a list of words.
2//!
3//! A pattern is a function that maps a list of words into another list
4//! after changing the casing of each letter.  How a patterns mutates
5//! each letter can be dependent on the word the letters are present in.
6
7#[cfg(feature = "random")]
8use rand::prelude::*;
9
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13use unicode_segmentation::UnicodeSegmentation;
14
15mod word_pattern {
16    use super::*;
17
18    pub fn lowercase(word: &str) -> String {
19        word.to_lowercase()
20    }
21
22    pub fn uppercase(word: &str) -> String {
23        word.to_uppercase()
24    }
25
26    pub fn capital(word: &str) -> String {
27        let mut graphemes = word.graphemes(true);
28
29        if let Some(c) = graphemes.next() {
30            [c.to_uppercase(), graphemes.as_str().to_lowercase()].concat()
31        } else {
32            String::new()
33        }
34    }
35
36    pub fn toggle(word: &str) -> String {
37        let mut graphemes = word.graphemes(true);
38
39        if let Some(c) = graphemes.next() {
40            [c.to_lowercase(), graphemes.as_str().to_uppercase()].concat()
41        } else {
42            String::new()
43        }
44    }
45}
46
47pub type Pattern = fn(&[&str]) -> Vec<String>;
48
49/// The no-op pattern performs no mutations.
50/// ```
51/// # use convert_case::pattern;
52/// assert_eq!(
53///     vec!["Case", "CONVERSION", "library"],
54///     pattern::noop(&["Case", "CONVERSION", "library"])
55/// );
56/// ```
57pub fn noop(words: &[&str]) -> Vec<String> {
58    words.iter().map(|word| word.to_string()).collect()
59}
60
61/// Makes all words lowercase.
62/// ```
63/// # use convert_case::pattern;
64/// assert_eq!(
65///     vec!["case", "conversion", "library"],
66///     pattern::lowercase(&["Case", "CONVERSION", "library"])
67/// );
68/// ```
69pub fn lowercase(words: &[&str]) -> Vec<String> {
70    words
71        .iter()
72        .map(|word| word_pattern::lowercase(word))
73        .collect()
74}
75
76/// Makes all words uppercase.
77/// ```
78/// # use convert_case::pattern;
79/// assert_eq!(
80///     vec!["CASE", "CONVERSION", "LIBRARY"],
81///     pattern::uppercase(&["Case", "CONVERSION", "library"])
82/// );
83/// ```
84pub fn uppercase(words: &[&str]) -> Vec<String> {
85    words
86        .iter()
87        .map(|word| word_pattern::uppercase(word))
88        .collect()
89}
90
91/// Makes the first letter of each word uppercase
92/// and the remaining letters of each word lowercase.
93/// ```
94/// # use convert_case::pattern;
95/// assert_eq!(
96///     vec!["Case", "Conversion", "Library"],
97///     pattern::capital(&["Case", "CONVERSION", "library"])
98/// );
99/// ```
100pub fn capital(words: &[&str]) -> Vec<String> {
101    words
102        .iter()
103        .map(|word| word_pattern::capital(word))
104        .collect()
105}
106
107/// Makes the first word lowercase and the
108/// remaining capitalized.
109/// ```
110/// # use convert_case::pattern;
111/// assert_eq!(
112///     vec!["case", "Conversion", "Library"],
113///     pattern::camel(&["Case", "CONVERSION", "library"])
114/// );
115/// ```
116pub fn camel(words: &[&str]) -> Vec<String> {
117    words
118        .iter()
119        .enumerate()
120        .map(|(i, &word)| {
121            if i == 0 {
122                word_pattern::lowercase(&word)
123            } else {
124                word_pattern::capital(&word)
125            }
126        })
127        .collect()
128}
129
130/// Makes the first word capitalized and the
131/// remaining lowercase.
132/// ```
133/// # use convert_case::pattern;
134/// assert_eq!(
135///     vec!["Case", "conversion", "library"],
136///     pattern::sentence(&["Case", "CONVERSION", "library"])
137/// );
138/// ```
139pub fn sentence(words: &[&str]) -> Vec<String> {
140    words
141        .iter()
142        .enumerate()
143        .map(|(i, &word)| {
144            if i == 0 {
145                word_pattern::capital(&word)
146            } else {
147                word_pattern::lowercase(&word)
148            }
149        })
150        .collect()
151}
152
153/// Makes the first letter of each word lowercase
154/// and the remaining letters of each word uppercase.
155/// ```
156/// # use convert_case::pattern;
157/// assert_eq!(
158///     vec!["cASE", "cONVERSION", "lIBRARY"],
159///     pattern::toggle(&["Case", "CONVERSION", "library"])
160/// );
161/// ```
162pub fn toggle(words: &[&str]) -> Vec<String> {
163    words
164        .iter()
165        .map(|word| word_pattern::toggle(word))
166        .collect()
167}
168
169/// Makes each letter of each word alternate
170/// between lowercase and uppercase.  
171///
172/// It alternates across words,
173/// which means the last letter of one word and the first letter of the
174/// next will not be the same letter casing.
175/// ```
176/// # use convert_case::pattern;
177/// assert_eq!(
178///     vec!["cAsE", "cOnVeRsIoN", "lIbRaRy"],
179///     pattern::alternating(&["Case", "CONVERSION", "library"])
180/// );
181/// assert_eq!(
182///     vec!["aNoThEr", "ExAmPlE"],
183///     pattern::alternating(&["Another", "Example"]),
184/// );
185/// ```
186pub fn alternating(words: &[&str]) -> Vec<String> {
187    let mut upper = false;
188    words
189        .iter()
190        .map(|word| {
191            word.chars()
192                .map(|letter| {
193                    if letter.is_uppercase() || letter.is_lowercase() {
194                        if upper {
195                            upper = false;
196                            letter.to_uppercase().to_string()
197                        } else {
198                            upper = true;
199                            letter.to_lowercase().to_string()
200                        }
201                    } else {
202                        letter.to_string()
203                    }
204                })
205                .collect()
206        })
207        .collect()
208}
209
210/// Lowercases or uppercases each letter
211/// uniformly randomly.  
212///
213/// This uses the `rand` crate and is only available with the "random"
214/// feature.  This example will not pass the assertion due to randomness, but it used as an
215/// example of what output is possible.
216/// ```should_panic
217/// # use convert_case::pattern;
218/// # #[cfg(any(doc, feature = "random"))]
219/// assert_eq!(
220///     vec!["Case", "coNVeRSiOn", "lIBraRY"],
221///     pattern::random(&["Case", "CONVERSION", "library"])
222/// );
223/// ```
224#[cfg(feature = "random")]
225pub fn random(words: &[&str]) -> Vec<String> {
226    let mut rng = rand::thread_rng();
227    words
228        .iter()
229        .map(|word| {
230            word.chars()
231                .map(|letter| {
232                    if rng.gen::<f32>() > 0.5 {
233                        letter.to_uppercase().to_string()
234                    } else {
235                        letter.to_lowercase().to_string()
236                    }
237                })
238                .collect()
239        })
240        .collect()
241}
242
243/// Case each letter in random-like patterns.  
244///
245/// Instead of randomizing
246/// each letter individually, it mutates each pair of characters
247/// as either (Lowercase, Uppercase) or (Uppercase, Lowercase).  This generates
248/// more "random looking" words.  A consequence of this algorithm for randomization
249/// is that there will never be three consecutive letters that are all lowercase
250/// or all uppercase.  This uses the `rand` crate and is only available with the "random"
251/// feature.  This example will not pass the assertion due to randomness, but it used as an
252/// example of what output is possible.
253/// ```should_panic
254/// # use convert_case::pattern;
255/// # #[cfg(any(doc, feature = "random"))]
256/// assert_eq!(
257///     vec!["cAsE", "cONveRSioN", "lIBrAry"],
258///     pattern::pseudo_random(&["Case", "CONVERSION", "library"]),
259/// );
260/// ```
261#[cfg(feature = "random")]
262pub fn pseudo_random(words: &[&str]) -> Vec<String> {
263    let mut rng = rand::thread_rng();
264
265    // Keeps track of when to alternate
266    let mut alt: Option<bool> = None;
267    words
268        .iter()
269        .map(|word| {
270            word.chars()
271                .map(|letter| {
272                    match alt {
273                        // No existing pattern, start one
274                        None => {
275                            if rng.gen::<f32>() > 0.5 {
276                                alt = Some(false); // Make the next char lower
277                                letter.to_uppercase().to_string()
278                            } else {
279                                alt = Some(true); // Make the next char upper
280                                letter.to_lowercase().to_string()
281                            }
282                        }
283                        // Existing pattern, do what it says
284                        Some(upper) => {
285                            alt = None;
286                            if upper {
287                                letter.to_uppercase().to_string()
288                            } else {
289                                letter.to_lowercase().to_string()
290                            }
291                        }
292                    }
293                })
294                .collect()
295        })
296        .collect()
297}
298
299#[cfg(test)]
300mod test {
301    use super::*;
302
303    #[cfg(feature = "random")]
304    #[test]
305    fn pseudo_no_triples() {
306        let words = vec!["abcdefg", "hijklmnop", "qrstuv", "wxyz"];
307        for _ in 0..5 {
308            let new = pseudo_random(&words).join("");
309            let mut iter = new
310                .chars()
311                .zip(new.chars().skip(1))
312                .zip(new.chars().skip(2));
313            assert!(!iter
314                .clone()
315                .any(|((a, b), c)| a.is_lowercase() && b.is_lowercase() && c.is_lowercase()));
316            assert!(
317                !iter.any(|((a, b), c)| a.is_uppercase() && b.is_uppercase() && c.is_uppercase())
318            );
319        }
320    }
321
322    #[cfg(feature = "random")]
323    #[test]
324    fn randoms_are_random() {
325        let words = vec!["abcdefg", "hijklmnop", "qrstuv", "wxyz"];
326
327        for _ in 0..5 {
328            let transformed = pseudo_random(&words);
329            assert_ne!(words, transformed);
330            let transformed = random(&words);
331            assert_ne!(words, transformed);
332        }
333    }
334
335    #[test]
336    fn mutate_empty_strings() {
337        for word_pattern in [
338            word_pattern::lowercase,
339            word_pattern::uppercase,
340            word_pattern::capital,
341            word_pattern::toggle,
342        ] {
343            assert_eq!(String::new(), word_pattern(&String::new()))
344        }
345    }
346}