convert_case/
pattern.rs

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