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}