convert_case_extras/lib.rs
1//! Extra utilities for [`convert_case`].
2//!
3//! ```
4//! use convert_case::Casing;
5//! use convert_case_extras::case;
6//!
7//! assert_eq!(
8//! "toggle_case_word".to_case(case::TOGGLE),
9//! "tOGGLE cASE wORD",
10//! )
11//! ```
12//!
13//! ## Random Feature
14//!
15//! The `random` feature contains `case::RANDOM` and `case::PSEUDO_RANDOM`.
16
17use convert_case::{Boundary, Case, Casing, Pattern};
18
19#[cfg(feature = "random")]
20use rand::prelude::*;
21
22/// Checks if a string matches the specified case.
23///
24/// A string matches a case if converting it to that case produces
25/// the same string (i.e., `s.to_case(case) == s`).
26///
27/// # Example
28/// ```
29/// use convert_case::Case;
30/// use convert_case_extras::is_case;
31///
32/// assert!(is_case("hello_world", Case::Snake));
33/// assert!(!is_case("hello_world", Case::Kebab));
34/// assert!(is_case("HelloWorld", Case::Pascal));
35/// ```
36pub fn is_case<T: AsRef<str>>(s: T, case: Case) -> bool {
37 s.as_ref() == s.as_ref().to_case(case)
38}
39
40/// A detector for determining which cases a string matches.
41///
42/// `CaseDetector` maintains a pool of cases and provides a method
43/// to detect which cases from the pool match a given string.
44///
45/// # Example
46/// ```
47/// use convert_case::Case;
48/// use convert_case_extras::CaseDetector;
49///
50/// // Default detector with all standard cases
51/// let detector = CaseDetector::default();
52/// let matches = detector.detect_cases("my_variable_name");
53/// assert!(matches.contains(&Case::Snake));
54///
55/// // Custom detector with specific cases
56/// let detector = CaseDetector::new()
57/// .add_case(Case::Snake)
58/// .add_case(Case::Kebab);
59/// let matches = detector.detect_cases("hello-world");
60/// assert_eq!(matches, vec![Case::Kebab]);
61/// ```
62#[derive(Debug, Clone)]
63pub struct CaseDetector {
64 cases: Vec<Case<'static>>,
65}
66
67impl CaseDetector {
68 /// Creates a new `CaseDetector` with an empty pool.
69 ///
70 /// Use builder methods like `add_case` to populate the pool.
71 ///
72 /// Use `default` instead to use all of the cases available in `convert-case`.
73 ///
74 /// # Example
75 /// ```
76 /// use convert_case::Case;
77 /// use convert_case_extras::CaseDetector;
78 ///
79 /// let detector = CaseDetector::new()
80 /// .add_case(Case::Snake)
81 /// .add_case(Case::Kebab);
82 /// ```
83 pub fn new() -> Self {
84 Self { cases: Vec::new() }
85 }
86
87 /// Adds a case to the pool. Returns self for method chaining.
88 ///
89 /// # Example
90 /// ```
91 /// use convert_case::Case;
92 /// use convert_case_extras::CaseDetector;
93 ///
94 /// let detector = CaseDetector::new()
95 /// .add_case(Case::Snake)
96 /// .add_case(Case::Kebab);
97 /// ```
98 pub fn add_case(mut self, case: Case<'static>) -> Self {
99 self.cases.push(case);
100 self
101 }
102
103 /// Adds multiple cases to the pool. Returns self for method chaining.
104 ///
105 /// # Example
106 /// ```
107 /// use convert_case::Case;
108 /// use convert_case_extras::CaseDetector;
109 ///
110 /// let detector = CaseDetector::new()
111 /// .add_cases(&[Case::Snake, Case::Kebab, Case::Camel]);
112 /// ```
113 pub fn add_cases(mut self, cases: &[Case<'static>]) -> Self {
114 self.cases.extend(cases.iter().copied());
115 self
116 }
117
118 /// Removes a case from the pool. Returns self for method chaining.
119 ///
120 /// # Example
121 /// ```
122 /// use convert_case::Case;
123 /// use convert_case_extras::CaseDetector;
124 ///
125 /// let detector = CaseDetector::default()
126 /// .remove_case(Case::Flat)
127 /// .remove_case(Case::UpperFlat);
128 /// ```
129 pub fn remove_case(mut self, case: Case<'static>) -> Self {
130 self.cases.retain(|&c| c != case);
131 self
132 }
133
134 /// Removes multiple cases from the pool. Returns self for method chaining.
135 ///
136 /// # Example
137 /// ```
138 /// use convert_case::Case;
139 /// use convert_case_extras::CaseDetector;
140 ///
141 /// let detector = CaseDetector::default()
142 /// .remove_cases(&[Case::Flat, Case::UpperFlat]);
143 /// ```
144 pub fn remove_cases(mut self, cases: &[Case<'static>]) -> Self {
145 for case in cases {
146 self.cases.retain(|&c| c != *case);
147 }
148 self
149 }
150
151 /// Detects all cases from the pool that the given string matches.
152 ///
153 /// A string "matches" a case if converting it to that case produces
154 /// the same string (i.e., `s.to_case(case) == s`).
155 ///
156 /// # Example
157 /// ```
158 /// use convert_case::Case;
159 /// use convert_case_extras::CaseDetector;
160 ///
161 /// let detector = CaseDetector::default();
162 ///
163 /// let matches = detector.detect_cases("hello_world");
164 /// assert!(matches.contains(&Case::Snake));
165 /// assert!(!matches.contains(&Case::Kebab));
166 ///
167 /// // Single lowercase word matches multiple cases
168 /// let matches = detector.detect_cases("word");
169 /// assert!(matches.contains(&Case::Snake));
170 /// assert!(matches.contains(&Case::Kebab));
171 /// assert!(matches.contains(&Case::Flat));
172 /// ```
173 pub fn detect_cases<T: AsRef<str>>(&self, s: T) -> Vec<Case<'static>> {
174 let s = s.as_ref();
175 self.cases
176 .iter()
177 .filter(|&&case| is_case(s, case))
178 .copied()
179 .collect()
180 }
181}
182
183impl Default for CaseDetector {
184 /// Creates a `CaseDetector` with all standard cases from `Case::all_cases()`.
185 fn default() -> Self {
186 Self {
187 cases: Case::all_cases().to_vec(),
188 }
189 }
190}
191
192pub mod pattern {
193 use super::*;
194
195 /// Makes the first letter of each word lowercase
196 /// and the remaining letters of each word uppercase.
197 /// ```
198 /// use convert_case_extras::pattern;
199 ///
200 /// assert_eq!(
201 /// pattern::TOGGLE.mutate(&["Case", "CONVERSION", "library"]),
202 /// vec!["cASE", "cONVERSION", "lIBRARY"],
203 /// );
204 /// ```
205 pub const TOGGLE: Pattern = Pattern::Custom(|words| {
206 words
207 .iter()
208 .map(|word| {
209 let mut chars = word.chars();
210
211 if let Some(c) = chars.next() {
212 [c.to_lowercase().collect(), chars.as_str().to_uppercase()].concat()
213 } else {
214 String::new()
215 }
216 })
217 .collect()
218 });
219
220 /// Makes each letter of each word alternate between lowercase and uppercase.
221 ///
222 /// It alternates across words,
223 /// which means the last letter of one word and the first letter of the
224 /// next will not be the same letter casing.
225 /// ```
226 /// use convert_case_extras::pattern;
227 ///
228 /// assert_eq!(
229 /// pattern::ALTERNATING.mutate(&["Case", "CONVERSION", "library"]),
230 /// vec!["cAsE", "cOnVeRsIoN", "lIbRaRy"],
231 /// );
232 /// assert_eq!(
233 /// pattern::ALTERNATING.mutate(&["Another", "Example"]),
234 /// vec!["aNoThEr", "ExAmPlE"],
235 /// );
236 /// ```
237 pub const ALTERNATING: Pattern = Pattern::Custom(|words| {
238 let mut upper = false;
239 words
240 .iter()
241 .map(|word| {
242 word.chars()
243 .map(|letter| {
244 if letter.is_uppercase() || letter.is_lowercase() {
245 if upper {
246 upper = false;
247 letter.to_uppercase().to_string()
248 } else {
249 upper = true;
250 letter.to_lowercase().to_string()
251 }
252 } else {
253 letter.to_string()
254 }
255 })
256 .collect()
257 })
258 .collect()
259 });
260
261 // #[doc(cfg(feature = "random"))]
262 /// Lowercases or uppercases each letter uniformly randomly.
263 ///
264 /// This uses the `rand` crate and is only available with the "random" feature.
265 /// ```
266 /// # #[cfg(any(doc, feature = "random"))]
267 /// use convert_case_extras::pattern;
268 ///
269 /// pattern::RANDOM.mutate(&["Case", "CONVERSION", "library"]);
270 /// // "casE", "coNVeRSiOn", "lIBraRY"
271 /// ```
272 #[cfg(feature = "random")]
273 pub const RANDOM: Pattern = Pattern::Custom(|words| {
274 let mut rng = rand::thread_rng();
275 words
276 .iter()
277 .map(|word| {
278 word.chars()
279 .map(|letter| {
280 if rng.gen::<f32>() > 0.5 {
281 letter.to_uppercase().to_string()
282 } else {
283 letter.to_lowercase().to_string()
284 }
285 })
286 .collect()
287 })
288 .collect()
289 });
290
291 /// Case each letter in random-like patterns.
292 ///
293 /// Instead of randomizing
294 /// each letter individually, it mutates each pair of characters
295 /// as either (Lowercase, Uppercase) or (Uppercase, Lowercase). This generates
296 /// more "random looking" words. A consequence of this algorithm for randomization
297 /// is that there will never be three consecutive letters that are all lowercase
298 /// or all uppercase. This uses the `rand` crate and is only available with the "random"
299 /// feature.
300 ///
301 /// This uses the `rand` crate and is only available with the "random" feature.
302 /// ```
303 /// # #[cfg(any(doc, feature = "random"))]
304 /// use convert_case_extras::pattern;
305 ///
306 /// pattern::PSEUDO_RANDOM.mutate(&["Case", "CONVERSION", "library"]);
307 /// // "cAsE", "cONveRSioN", "lIBrAry"
308 /// ```
309 #[cfg(feature = "random")]
310 pub const PSEUDO_RANDOM: Pattern = Pattern::Custom(|words| {
311 let mut rng = rand::thread_rng();
312
313 // Keeps track of when to alternate
314 let mut alt: Option<bool> = None;
315 words
316 .iter()
317 .map(|word| {
318 word.chars()
319 .map(|letter| {
320 match alt {
321 // No existing pattern, start one
322 None => {
323 if rng.gen::<f32>() > 0.5 {
324 alt = Some(false); // Make the next char lower
325 letter.to_uppercase().to_string()
326 } else {
327 alt = Some(true); // Make the next char upper
328 letter.to_lowercase().to_string()
329 }
330 }
331 // Existing pattern, do what it says
332 Some(upper) => {
333 alt = None;
334 if upper {
335 letter.to_uppercase().to_string()
336 } else {
337 letter.to_lowercase().to_string()
338 }
339 }
340 }
341 })
342 .collect()
343 })
344 .collect()
345 });
346}
347
348pub mod case {
349 use super::*;
350
351 /// Toggle case strings are delimited by spaces. All characters are uppercase except
352 /// for the leading character of each word, which is lowercase.
353 /// * Boundaries: [Space](`Boundary::Space`)
354 /// * Pattern: [Toggle](`pattern::TOGGLE`)
355 /// * Delimiter: Space `" "`
356 ///
357 /// ```
358 /// use convert_case::Casing;
359 /// use convert_case_extras::case;
360 /// assert_eq!("My variable NAME".to_case(case::TOGGLE), "mY vARIABLE nAME");
361 /// ```
362 pub const TOGGLE: Case = Case::Custom {
363 boundaries: &[Boundary::Space],
364 pattern: pattern::TOGGLE,
365 delimiter: " ",
366 };
367
368 /// Alternating case strings are delimited by spaces. Characters alternate between uppercase
369 /// and lowercase.
370 /// * Boundaries: [Space](Boundary::Space)
371 /// * Pattern: [Alternating](Pattern::Alternating)
372 /// * Delimiter: Space `" "`
373 ///
374 /// ```
375 /// use convert_case::Casing;
376 /// use convert_case_extras::case;
377 /// assert_eq!("My variable NAME".to_case(case::ALTERNATING), "mY vArIaBlE nAmE");
378 /// ```
379 pub const ALTERNATING: Case = Case::Custom {
380 boundaries: &[Boundary::Space],
381 pattern: pattern::ALTERNATING,
382 delimiter: " ",
383 };
384
385 /// Random case strings are delimited by spaces and characters are
386 /// randomly upper case or lower case.
387 ///
388 /// This uses the `rand` crate
389 /// and is only available with the "random" feature.
390 /// * Boundaries: [Space](Boundary::Space)
391 /// * Pattern: [Random](pattern::RANDOM)
392 /// * Delimiter: Space `" "`
393 ///
394 /// ```
395 /// use convert_case::Casing;
396 /// use convert_case_extras::case;
397 /// "My variable NAME".to_case(case::RANDOM);
398 /// // "My vaRIAbLE nAme"
399 /// ```
400 #[cfg(any(doc, feature = "random"))]
401 #[cfg(feature = "random")]
402 pub const RANDOM: Case = Case::Custom {
403 boundaries: &[Boundary::Space],
404 pattern: pattern::RANDOM,
405 delimiter: " ",
406 };
407
408 /// Pseudo-random case strings are delimited by spaces and characters are randomly
409 /// upper case or lower case, but there will never more than two consecutive lower
410 /// case or upper case letters in a row.
411 ///
412 /// This uses the `rand` crate and is
413 /// only available with the "random" feature.
414 /// * Boundaries: [Space](Boundary::Space)
415 /// * Pattern: [Pseudo random](pattern::PSEUDO_RANDOM)
416 /// * Delimiter: Space `" "`
417 ///
418 /// ```
419 /// use convert_case::Casing;
420 /// use convert_case_extras::case;
421 /// let new = "My variable NAME".to_case(case::PSEUDO_RANDOM);
422 /// ```
423 /// String `new` could be "mY vArIAblE NamE" for example.
424 #[cfg(any(doc, feature = "random"))]
425 #[cfg(feature = "random")]
426 pub const PSEUDO_RANDOM: Case = Case::Custom {
427 boundaries: &[Boundary::Space],
428 pattern: pattern::PSEUDO_RANDOM,
429 delimiter: " ",
430 };
431}
432
433#[cfg(test)]
434mod test {
435 use super::*;
436
437 use convert_case::Casing;
438
439 #[test]
440 fn toggle_case() {
441 assert_eq!("test_toggle".to_case(case::TOGGLE), "tEST tOGGLE");
442 }
443
444 #[cfg(feature = "random")]
445 #[test]
446 fn pseudo_no_triples() {
447 let words = vec!["abcdefg", "hijklmnop", "qrstuv", "wxyz"];
448 for _ in 0..5 {
449 let new = pattern::PSEUDO_RANDOM.mutate(&words).join("");
450 let mut iter = new
451 .chars()
452 .zip(new.chars().skip(1))
453 .zip(new.chars().skip(2));
454 assert!(!iter
455 .clone()
456 .any(|((a, b), c)| a.is_lowercase() && b.is_lowercase() && c.is_lowercase()));
457 assert!(
458 !iter.any(|((a, b), c)| a.is_uppercase() && b.is_uppercase() && c.is_uppercase())
459 );
460 }
461 }
462
463 #[cfg(feature = "random")]
464 #[test]
465 fn randoms_are_random() {
466 let words = vec!["abcdefg", "hijklmnop", "qrstuv", "wxyz"];
467
468 for _ in 0..5 {
469 let transformed = pattern::PSEUDO_RANDOM.mutate(&words);
470 assert_ne!(words, transformed);
471 let transformed = pattern::RANDOM.mutate(&words);
472 assert_ne!(words, transformed);
473 }
474 }
475}