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}