Skip to main content

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}