convert_case/
lib.rs

1//! Convert to and from different string cases.
2//!
3//! # Basic Usage
4//!
5//! The most common use of this crate is to just convert a string into a
6//! particular case, like snake, camel, or kebab.  You can use the [`ccase`]
7//! macro to convert most string types into the new case.
8//! ```
9//! use convert_case::ccase;
10//!
11//! let s = "myVarName";
12//! assert_eq!(ccase!(snake, s),  "my_var_name");
13//! assert_eq!(ccase!(kebab, s),  "my-var-name");
14//! assert_eq!(ccase!(pascal, s), "MyVarName");
15//! assert_eq!(ccase!(title, s),  "My Var Name");
16//! ```
17//!
18//! For more explicit conversion, import the [`Casing`] trait which adds methods
19//! to string types that perform the conversion based on a variant of the [`Case`] enum.
20//! ```
21//! use convert_case::{Case, Casing};
22//!
23//! let s = "myVarName";
24//! assert_eq!(s.to_case(Case::Snake),  "my_var_name");
25//! assert_eq!(s.to_case(Case::Kebab),  "my-var-name");
26//! assert_eq!(s.to_case(Case::Pascal), "MyVarName");
27//! assert_eq!(s.to_case(Case::Title),  "My Var Name");
28//! ```
29//!
30//! For a full list of cases, see [`Case`].
31//!
32//! # Splitting Conditions
33//!
34//! Case conversion starts by splitting a single identifier into a list of words.  The
35//! condition for when to split and how to perform the split is defined by a [`Boundary`].
36//!
37//! By default, [`ccase`] and [`Casing::to_case`] will split identifiers at all locations
38//! based on a list of [default boundaries](Boundary::defaults).
39//!
40//! ```
41//! use convert_case::ccase;
42//!
43//! assert_eq!(ccase!(pascal, "hyphens-and_underscores"), "HyphensAndUnderscores");
44//! assert_eq!(ccase!(pascal, "lowerUpper space"), "LowerUpperSpace");
45//! assert_eq!(ccase!(snake, "HTTPRequest"), "http_request");
46//! assert_eq!(ccase!(snake, "vector4d"), "vector_4_d")
47//! ```
48//!
49//! Associated with each case is a [list of boundaries](Case::boundaries) that can be
50//! used to split identifiers instead of the defaults.  We can use the following notation
51//! with the [`ccase`] macro.
52//! ```
53//! use convert_case::ccase;
54//!
55//! assert_eq!(
56//!     ccase!(title, "1999-25-01_family_photo.png"),
57//!     "1999 25 01 Family Photo.png",
58//! );
59//! assert_eq!(
60//!     ccase!(snake -> title, "1999-25-01_family_photo.png"),
61//!     "1999-25-01 Family Photo.png",
62//! );
63//! ```
64//! Or we can use the [`from_case`](Casing::from_case) method on `Casing` before calling
65//! `to_case`.
66//! ```
67//! use convert_case::{Case, Casing};
68//!
69//! assert_eq!(
70//!     "John McCarthy".to_case(Case::Snake),
71//!     "john_mc_carthy",
72//! );
73//! assert_eq!(
74//!     "John McCarthy".from_case(Case::Title).to_case(Case::Snake),
75//!     "john_mccarthy",
76//! );
77//! ```
78//! You can remove boundaries from the list of defaults with [`Casing::without_boundaries`].  See
79//! the list of constants on [`Boundary`] for splitting conditions.
80//! ```
81//! use convert_case::{Boundary, Case, Casing};
82//!
83//! assert_eq!(
84//!     "Vector4D".without_boundaries(&[Boundary::DigitUpper]).to_case(Case::Snake),
85//!     "vector_4d",
86//! );
87//! ```
88//!
89//! # Other Behavior
90//!
91//! ### Acronyms
92//! Part of the default list of boundaries is [`acronym`](Boundary::Acronym) which
93//! will detect two capital letters followed by a lowercase letter.  But there is no memory
94//! that the word itself was parsed considered an acronym.
95//! ```
96//! # use convert_case::ccase;
97//! assert_eq!(ccase!(snake, "HTTPRequest"), "http_request");
98//! assert_eq!(ccase!(pascal, "HTTPRequest"), "HttpRequest");
99//! ```
100//!
101//! ### Digits
102//! The default list of boundaries includes splitting before and after digits.
103//! ```
104//! # use convert_case::ccase;
105//! assert_eq!(ccase!(title, "word2vec"), "Word 2 Vec");
106//! ```
107//!
108//! ### Unicode
109//! Conversion works on _graphemes_ as defined by the
110//! [`unicode_segmentation`](unicode_segmentation::UnicodeSegmentation::graphemes) library.
111//! This means that transforming letters to lowercase or uppercase works on all unicode
112//! characters, which also means that the number of characters isn't necessarily the
113//! same after conversion.
114//! ```
115//! # use convert_case::ccase;
116//! assert_eq!(ccase!(kebab, "GranatÄpfel"), "granat-äpfel");
117//! assert_eq!(ccase!(title, "ПЕРСПЕКТИВА24"), "Перспектива 24");
118//! assert_eq!(ccase!(lower, "ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς");
119//! ```
120//!
121//! ### Symbols
122//! All symbols that are not part of the default boundary conditions are ignored.  This
123//! is any symbol that isn't an underscore, hyphen, or space.
124//! ```
125//! # use convert_case::ccase;
126//! assert_eq!(ccase!(snake, "dots.arent.default"), "dots.arent.default");
127//! assert_eq!(ccase!(pascal, "path/to/file_name"), "Path/to/fileName");
128//! assert_eq!(ccase!(pascal, "list\nof\nwords"),   "List\nof\nwords");
129//! ```
130//!
131//! ### Delimiters
132//! Leading, trailing, and duplicate delimiters create empty words.
133//! This propogates and the converted string will share the behavior.  This can cause
134//! unintuitive behavior for patterns that transform words based on index.
135//! ```
136//! # use convert_case::ccase;
137//! assert_eq!(ccase!(constant, "_leading_score"), "_LEADING_SCORE");
138//! assert_eq!(ccase!(ada, "trailing-dash-"), "Trailing_Dash_");
139//! assert_eq!(ccase!(train, "duplicate----hyphens"), "Duplicate----Hyphens");
140//! assert_eq!(ccase!(camel, "_empty__first_word"), "EmptyFirstWord");
141//! ```
142//!
143//! # Customizing Behavior
144//!
145//! Case conversion takes place in three steps:
146//! 1. Splitting the identifier into a list of words
147//! 2. Mutating the letter case of graphemes within each word
148//! 3. Joining the words back into an identifier using a delimiter
149//!
150//! Those are defined by boundaries, patterns, and delimiters respectively.  Graphically:
151//!
152//! ```md
153//! Identifier        Identifier'
154//!     |                 ^
155//!     | boundaries      | delimiter
156//!     V                 |
157//!   Words ----------> Words'
158//!           pattern
159//! ```
160//!
161//! ## Patterns
162//!
163//! How to change the case of letters across a list of words is called a _pattern_.
164//! A pattern is a function that when passed a `&[&str]`, produces a
165//! `Vec<String>`.  The [`Pattern`] enum encapsulates the common transformations
166//! used across all cases.  Although custom functions can be supplied with the
167//! [`Custom`](Pattern::Custom) variant.
168//!
169//! ## Boundaries
170//!
171//! The condition for splitting at part of an identifier, where to perform
172//! the split, and if any characters are removed are defined by [boundaries](Boundary).
173//! By default, identifiers are split based on [`Boundary::defaults`].  This list
174//! contains word boundaries that you would likely see after creating a multi-word
175//! identifier of any case.
176//!
177//! Custom boundary conditions can be created.  Commonly, you might split based on some
178//! character or list of characters.  The [`Boundary::from_delim`] method builds
179//! a boundary that splits on the presence of a string, and removes the string
180//! from the final list of words.
181//!
182//! You can also use of [`Boundary::Custom`] to explicitly define boundary
183//! conditions.  If you actually need to create a
184//! boundary condition from scratch, you should file an issue to let the author know
185//! how you used it.
186//!
187//! ## Cases
188//!
189//! A case is defined by a list of boundaries, a pattern, and a delimiter: the string to
190//! intersperse between words before concatenation. [`Case::Custom`] is a struct enum variant with
191//! exactly those three fields.  You could create your own case like so.
192//! ```
193//! use convert_case::{Case, Casing, Boundary, Pattern};
194//!
195//! let dot_case = Case::Custom {
196//!     boundaries: &[Boundary::from_delim(".")],
197//!     pattern: Pattern::Lowercase,
198//!     delim: ".",
199//! };
200//!
201//! assert_eq!("AnimalFactoryFactory".to_case(dot_case), "animal.factory.factory");
202//!
203//! assert_eq!(
204//!     "pd.options.mode.copy_on_write"
205//!         .from_case(dot_case)
206//!         .to_case(Case::Title),
207//!     "Pd Options Mode Copy_on_write",
208//! )
209//! ```
210//!
211//! ## Converter
212//!
213//! Case conversion with `convert_case` allows using attributes from two cases.  From
214//! the first case is how you split the identifier (the _from_ case), and
215//! from the second is how to mutate and join the words (the _to_ case.)  The
216//! [`Converter`] is used to define the _conversion_ process, not a case directly.
217//!
218//! It has the same fields as case, but is exposed via a builder interface
219//! and can be used to apply a conversion on a string directly, without
220//! specifying all the parameters at the time of conversion.
221//!
222//! In the below example, we build a converter that maps the double colon
223//! delimited module path in rust into a series of file directories.
224//!
225//! ```
226//! use convert_case::{Case, Converter, Boundary};
227//!
228//! let modules_into_path = Converter::new()
229//!     .set_boundaries(&[Boundary::from_delim("::")])
230//!     .set_delim("/");
231//!
232//! assert_eq!(
233//!     modules_into_path.convert("std::os::unix"),
234//!     "std/os/unix",
235//! );
236//! ```
237//!
238//! # Random Feature
239//!
240//! This feature adds two additional cases with non-deterministic behavior.
241//! The `random` feature depends on the [`rand`](https://docs.rs/rand) crate.
242//!
243//! The [`Case::Random`] variant will flip a coin at every letter to make it uppercase
244//! or lowercase.  Here are some examples:
245//!
246//! ```
247//! # use convert_case::ccase;
248//! # #[cfg(any(doc, feature = "random"))]
249//! ccase!(random, "What's the deal with airline food?");
250//! // WhAT's tHe Deal WitH aIRline fOOD?
251//! ```
252//!
253//! For a more even distribution of uppercase and lowercase letters, which might _look_ more
254//! random, use the [`Case::PseudoRandom`] variant.  This variant randomly decides if every pair of letters
255//! is upper then lower or lower then upper.  This guarantees that no three consecutive characters
256//! is all uppercase or all lowercase.
257//!
258//! ```
259//! # use convert_case::ccase;
260//! # #[cfg(any(doc, feature = "random"))]
261//! ccase!(random, "What's the deal with airline food?");
262//! // wHAt'S The DeAl WIth AiRlInE fOoD?
263//! ```
264//!
265//! # Associated Projects
266//!
267//! ## stringcase.org
268//!
269//! While developing `convert_case`, the author became fascinated in the naming conventions
270//! used for cases as well as different implementations for conversion.  On [stringcase.org](https://stringcase.org)
271//! is documentation of the history of naming conventions, a catalogue of case conversion tools,
272//! and a more mathematical definition of what it means to "convert the string case of an identifier."
273//!
274//! ## Command Line Utility `ccase`
275//!
276//! `convert_case` was originally developed for the purposes of a command line utility
277//! for converting the case of strings and filenames.  You can check out
278//! [`ccase` on Github](https://github.com/rutrum/ccase).
279#![cfg_attr(not(test), no_std)]
280extern crate alloc;
281
282use alloc::string::String;
283
284mod boundary;
285mod case;
286mod converter;
287mod pattern;
288
289pub use boundary::{split, Boundary};
290pub use case::Case;
291pub use converter::Converter;
292pub use pattern::Pattern;
293
294/// Describes items that can be converted into a case.  This trait is used
295/// in conjunction with the [`StateConverter`] struct which is returned from a couple
296/// methods on `Casing`.
297pub trait Casing<T: AsRef<str>> {
298    /// Convert the string into the given case.  It will reference `self` and create a new
299    /// `String` with the same pattern and delimeter as `case`.  It will split on boundaries
300    /// defined at [`Boundary::defaults()`].
301    /// ```
302    /// use convert_case::{Case, Casing};
303    ///
304    /// assert_eq!(
305    ///     "tetronimo-piece-border",
306    ///     "Tetronimo piece border".to_case(Case::Kebab)
307    /// );
308    /// ```
309    fn to_case(&self, case: Case) -> String;
310
311    /// Start the case conversion by storing the boundaries associated with the given case.
312    /// ```
313    /// use convert_case::{Case, Casing};
314    ///
315    /// assert_eq!(
316    ///     "2020-08-10_dannie_birthday",
317    ///     "2020-08-10 Dannie Birthday"
318    ///         .from_case(Case::Title)
319    ///         .to_case(Case::Snake)
320    /// );
321    /// ```
322    #[allow(clippy::wrong_self_convention)]
323    fn from_case(&self, case: Case) -> StateConverter<T>;
324
325    /// Creates a `StateConverter` struct initialized with the boundaries
326    /// provided.
327    /// ```
328    /// use convert_case::{Boundary, Case, Casing};
329    ///
330    /// assert_eq!(
331    ///     "e1_m1_hangar",
332    ///     "E1M1 Hangar"
333    ///         .with_boundaries(&[Boundary::DigitUpper, Boundary::Space])
334    ///         .to_case(Case::Snake)
335    /// );
336    /// ```
337    fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
338
339    /// Creates a `StateConverter` struct initialized without the boundaries
340    /// provided.
341    /// ```
342    /// use convert_case::{Boundary, Case, Casing};
343    ///
344    /// assert_eq!(
345    ///     "2d_transformation",
346    ///     "2dTransformation"
347    ///         .without_boundaries(&Boundary::digits())
348    ///         .to_case(Case::Snake)
349    /// );
350    /// ```
351    fn without_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
352
353    /// Determines if `self` is of the given case.  This is done simply by applying
354    /// the conversion and seeing if the result is the same.
355    /// ```
356    /// use convert_case::{Case, Casing};
357    ///
358    /// assert!( "kebab-case-string".is_case(Case::Kebab));
359    /// assert!( "Train-Case-String".is_case(Case::Train));
360    ///
361    /// assert!(!"kebab-case-string".is_case(Case::Snake));
362    /// assert!(!"kebab-case-string".is_case(Case::Train));
363    /// ```
364    fn is_case(&self, case: Case) -> bool;
365}
366
367impl<T: AsRef<str>> Casing<T> for T {
368    fn to_case(&self, case: Case) -> String {
369        StateConverter::new(self).to_case(case)
370    }
371
372    fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
373        StateConverter::new(self).set_boundaries(bs)
374    }
375
376    fn without_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
377        StateConverter::new(self).without_boundaries(bs)
378    }
379
380    fn from_case(&self, case: Case) -> StateConverter<T> {
381        StateConverter::new(self).from_case(case)
382    }
383
384    fn is_case(&self, case: Case) -> bool {
385        self.as_ref() == self.to_case(case).as_str()
386        /*
387        let digitless = self
388            .as_ref()
389            .chars()
390            .filter(|x| !x.is_ascii_digit())
391            .collect::<String>();
392
393        digitless == digitless.to_case(case)
394        */
395    }
396}
397
398/// Holds information about parsing before converting into a case.
399///
400/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
401/// `Casing`.  For a more fine grained approach to case conversion, consider using the [`Converter`]
402/// struct.
403/// ```
404/// use convert_case::{Case, Casing};
405///
406/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
407/// assert_eq!("Ninety-nine Problems", title);
408/// ```
409pub struct StateConverter<'a, T: AsRef<str>> {
410    s: &'a T,
411    conv: Converter,
412}
413
414impl<'a, T: AsRef<str>> StateConverter<'a, T> {
415    /// Only called by Casing function to_case()
416    fn new(s: &'a T) -> Self {
417        Self {
418            s,
419            conv: Converter::new(),
420        }
421    }
422
423    /// Uses the boundaries associated with `case` for word segmentation.  This
424    /// will overwrite any boundary information initialized before.  This method is
425    /// likely not useful, but provided anyway.
426    /// ```
427    /// use convert_case::{Case, Casing};
428    ///
429    /// let name = "Chuck Schuldiner"
430    ///     .from_case(Case::Snake) // from Casing trait
431    ///     .from_case(Case::Title) // from StateConverter, overwrites previous
432    ///     .to_case(Case::Kebab);
433    /// assert_eq!("chuck-schuldiner", name);
434    /// ```
435    pub fn from_case(self, case: Case) -> Self {
436        Self {
437            conv: self.conv.from_case(case),
438            ..self
439        }
440    }
441
442    /// Overwrites boundaries for word segmentation with those provided.  This will overwrite
443    /// any boundary information initialized before.  This method is likely not useful, but
444    /// provided anyway.
445    /// ```
446    /// use convert_case::{Boundary, Case, Casing};
447    ///
448    /// let song = "theHumbling river-puscifer"
449    ///     .from_case(Case::Kebab) // from Casing trait
450    ///     .set_boundaries(&[Boundary::Space, Boundary::LowerUpper]) // overwrites `from_case`
451    ///     .to_case(Case::Pascal);
452    /// assert_eq!("TheHumblingRiver-puscifer", song);  // doesn't split on hyphen `-`
453    /// ```
454    pub fn set_boundaries(self, bs: &[Boundary]) -> Self {
455        Self {
456            s: self.s,
457            conv: self.conv.set_boundaries(bs),
458        }
459    }
460
461    /// Removes any boundaries that were already initialized.  This is particularly useful when a
462    /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
463    /// some.
464    /// ```
465    /// use convert_case::{Boundary, Case, Casing};
466    ///
467    /// assert_eq!(
468    ///     "2d_transformation",
469    ///     "2dTransformation"
470    ///         .from_case(Case::Camel)
471    ///         .without_boundaries(&Boundary::digits())
472    ///         .to_case(Case::Snake)
473    /// );
474    /// ```
475    pub fn without_boundaries(self, bs: &[Boundary]) -> Self {
476        Self {
477            s: self.s,
478            conv: self.conv.remove_boundaries(bs),
479        }
480    }
481
482    /// Consumes the `StateConverter` and returns the converted string.
483    /// ```
484    /// use convert_case::{Boundary, Case, Casing};
485    ///
486    /// assert_eq!(
487    ///     "ice-cream social",
488    ///     "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower)
489    /// );
490    /// ```
491    pub fn to_case(self, case: Case) -> String {
492        self.conv.to_case(case).convert(self.s)
493    }
494}
495
496/// The variant of `case` from a token.
497///
498/// The token associated with each variant is the variant written in snake case.
499#[cfg(not(feature = "random"))]
500#[macro_export]
501macro_rules! case {
502    (snake) => {
503        convert_case::Case::Snake
504    };
505    (constant) => {
506        convert_case::Case::Constant
507    };
508    (upper_snake) => {
509        convert_case::Case::UpperSnake
510    };
511    (ada) => {
512        convert_case::Case::Ada;
513    };
514    (kebab) => {
515        convert_case::Case::Kebab
516    };
517    (cobol) => {
518        convert_case::Case::Cobol
519    };
520    (upper_kebab) => {
521        convert_case::Case::UpperKebab
522    };
523    (train) => {
524        convert_case::Case::Train
525    };
526    (flat) => {
527        convert_case::Case::Flat
528    };
529    (upper_flat) => {
530        convert_case::Case::UpperFlat
531    };
532    (pascal) => {
533        convert_case::Case::Pascal
534    };
535    (upper_camel) => {
536        convert_case::Case::UpperCamel
537    };
538    (camel) => {
539        convert_case::Case::Camel
540    };
541    (lower) => {
542        convert_case::Case::Lower
543    };
544    (upper) => {
545        convert_case::Case::Upper
546    };
547    (title) => {
548        convert_case::Case::Title
549    };
550    (sentence) => {
551        convert_case::Case::Sentence
552    };
553    (alternating) => {
554        convert_case::Case::Alternating
555    };
556    (toggle) => {
557        convert_case::Case::Toggle
558    };
559}
560
561/// The variant of `case` from a token.
562///
563/// The token associated with each variant is the variant written in snake case.
564#[cfg(feature = "random")]
565#[macro_export]
566macro_rules! case {
567    (snake) => {
568        convert_case::Case::Snake
569    };
570    (constant) => {
571        convert_case::Case::Constant
572    };
573    (upper_snake) => {
574        convert_case::Case::UpperSnake
575    };
576    (ada) => {
577        convert_case::Case::Ada;
578    };
579    (kebab) => {
580        convert_case::Case::Kebab
581    };
582    (cobol) => {
583        convert_case::Case::Cobol
584    };
585    (upper_kebab) => {
586        convert_case::Case::UpperKebab
587    };
588    (train) => {
589        convert_case::Case::Train
590    };
591    (flat) => {
592        convert_case::Case::Flat
593    };
594    (upper_flat) => {
595        convert_case::Case::UpperFlat
596    };
597    (pascal) => {
598        convert_case::Case::Pascal
599    };
600    (upper_camel) => {
601        convert_case::Case::UpperCamel
602    };
603    (camel) => {
604        convert_case::Case::Camel
605    };
606    (lower) => {
607        convert_case::Case::Lower
608    };
609    (upper) => {
610        convert_case::Case::Upper
611    };
612    (title) => {
613        convert_case::Case::Title
614    };
615    (sentence) => {
616        convert_case::Case::Sentence
617    };
618    (alternating) => {
619        convert_case::Case::Alternating
620    };
621    (toggle) => {
622        convert_case::Case::Toggle
623    };
624    (random) => {
625        convert_case::Case::Random
626    };
627    (psuedo_random) => {
628        convert_case::Case::PsuedoRandom
629    };
630}
631
632/// Convert an identifier into a case.
633///
634/// The macro can be used as follows.
635/// ```
636/// use convert_case::ccase;
637///
638/// assert_eq!(ccase!(snake, "myVarName"), "my_var_name");
639/// // equivalent to
640/// // "myVarName".to_case(Case::Snake)
641/// ```
642/// You can also specify a _from_ case, or the case that determines how the input
643/// string is split into words.
644/// ```
645/// use convert_case::ccase;
646///
647/// assert_eq!(ccase!(sentence -> snake, "Ice-cream sales"), "ice-cream_sales");
648/// // equivalent to
649/// // "Ice-cream sales".from_case(Case::Sentence).to_case(Case::Snake)
650/// ```
651#[macro_export]
652macro_rules! ccase {
653    ($case:ident, $e:expr) => {
654        convert_case::Converter::new()
655            .to_case(convert_case::case!($case))
656            .convert($e)
657    };
658    ($from:ident -> $to:ident, $e:expr) => {
659        convert_case::Converter::new()
660            .from_case(convert_case::case!($from))
661            .to_case(convert_case::case!($to))
662            .convert($e)
663    };
664}
665
666#[cfg(test)]
667mod test {
668    use super::*;
669
670    use alloc::vec;
671    use alloc::vec::Vec;
672
673    fn possible_cases(s: &str) -> Vec<Case> {
674        Case::deterministic_cases()
675            .iter()
676            .filter(|&case| s.from_case(*case).to_case(*case) == s)
677            .map(|c| *c)
678            .collect()
679    }
680
681    #[test]
682    fn lossless_against_lossless() {
683        let examples = vec![
684            (Case::Snake, "my_variable_22_name"),
685            (Case::Constant, "MY_VARIABLE_22_NAME"),
686            (Case::Ada, "My_Variable_22_Name"),
687            (Case::Kebab, "my-variable-22-name"),
688            (Case::Cobol, "MY-VARIABLE-22-NAME"),
689            (Case::Train, "My-Variable-22-Name"),
690            (Case::Pascal, "MyVariable22Name"),
691            (Case::Camel, "myVariable22Name"),
692            (Case::Lower, "my variable 22 name"),
693            (Case::Upper, "MY VARIABLE 22 NAME"),
694            (Case::Title, "My Variable 22 Name"),
695            (Case::Sentence, "My variable 22 name"),
696            (Case::Toggle, "mY vARIABLE 22 nAME"),
697            (Case::Alternating, "mY vArIaBlE 22 nAmE"),
698        ];
699
700        for (case_a, str_a) in &examples {
701            for (case_b, str_b) in &examples {
702                assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
703            }
704        }
705    }
706
707    #[test]
708    fn obvious_default_parsing() {
709        let examples = vec![
710            "SuperMario64Game",
711            "super-mario64-game",
712            "superMario64 game",
713            "Super Mario 64_game",
714            "SUPERMario 64-game",
715            "super_mario-64 game",
716        ];
717
718        for example in examples {
719            assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
720        }
721    }
722
723    #[test]
724    fn multiline_strings() {
725        assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
726    }
727
728    #[test]
729    fn camel_case_acroynms() {
730        assert_eq!(
731            "xml_http_request",
732            "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
733        );
734        assert_eq!(
735            "xml_http_request",
736            "XMLHttpRequest"
737                .from_case(Case::UpperCamel)
738                .to_case(Case::Snake)
739        );
740        assert_eq!(
741            "xml_http_request",
742            "XMLHttpRequest"
743                .from_case(Case::Pascal)
744                .to_case(Case::Snake)
745        );
746    }
747
748    #[test]
749    fn leading_tailing_delimeters() {
750        assert_eq!(
751            "_leading_underscore",
752            "_leading_underscore"
753                .from_case(Case::Snake)
754                .to_case(Case::Snake)
755        );
756        assert_eq!(
757            "tailing_underscore_",
758            "tailing_underscore_"
759                .from_case(Case::Snake)
760                .to_case(Case::Snake)
761        );
762        assert_eq!(
763            "_leading_hyphen",
764            "-leading-hyphen"
765                .from_case(Case::Kebab)
766                .to_case(Case::Snake)
767        );
768        assert_eq!(
769            "tailing_hyphen_",
770            "tailing-hyphen-"
771                .from_case(Case::Kebab)
772                .to_case(Case::Snake)
773        );
774        assert_eq!(
775            "tailing_hyphens_____",
776            "tailing-hyphens-----"
777                .from_case(Case::Kebab)
778                .to_case(Case::Snake)
779        );
780        assert_eq!(
781            "tailingHyphens",
782            "tailing-hyphens-----"
783                .from_case(Case::Kebab)
784                .to_case(Case::Camel)
785        );
786    }
787
788    #[test]
789    fn double_delimeters() {
790        assert_eq!(
791            "many___underscores",
792            "many___underscores"
793                .from_case(Case::Snake)
794                .to_case(Case::Snake)
795        );
796        assert_eq!(
797            "many---underscores",
798            "many---underscores"
799                .from_case(Case::Kebab)
800                .to_case(Case::Kebab)
801        );
802    }
803
804    #[test]
805    fn early_word_boundaries() {
806        assert_eq!(
807            "a_bagel",
808            "aBagel".from_case(Case::Camel).to_case(Case::Snake)
809        );
810    }
811
812    #[test]
813    fn late_word_boundaries() {
814        assert_eq!(
815            "team_a",
816            "teamA".from_case(Case::Camel).to_case(Case::Snake)
817        );
818    }
819
820    #[test]
821    fn empty_string() {
822        for (case_a, case_b) in Case::all_cases()
823            .into_iter()
824            .zip(Case::all_cases().into_iter())
825        {
826            assert_eq!("", "".from_case(*case_a).to_case(*case_b));
827        }
828    }
829
830    #[test]
831    fn default_all_boundaries() {
832        assert_eq!(
833            "abc_abc_abc_abc_abc_abc",
834            "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
835        );
836        assert_eq!("8_a_8_a_8", "8a8A8".to_case(Case::Snake));
837    }
838
839    #[test]
840    fn alternating_ignore_symbols() {
841        assert_eq!("tHaT's", "that's".to_case(Case::Alternating));
842    }
843
844    mod is_case {
845        use super::*;
846
847        #[test]
848        fn snake() {
849            assert!("im_snake_case".is_case(Case::Snake));
850            assert!(!"im_NOTsnake_case".is_case(Case::Snake));
851        }
852
853        #[test]
854        fn kebab() {
855            assert!("im-kebab-case".is_case(Case::Kebab));
856            assert!(!"im_not_kebab".is_case(Case::Kebab));
857        }
858
859        #[test]
860        fn lowercase_word() {
861            for lower_case in [
862                Case::Snake,
863                Case::Kebab,
864                Case::Flat,
865                Case::Lower,
866                Case::Camel,
867            ] {
868                assert!("lowercase".is_case(lower_case));
869            }
870        }
871
872        #[test]
873        fn uppercase_word() {
874            for upper_case in [Case::Constant, Case::Cobol, Case::UpperFlat, Case::Upper] {
875                assert!("UPPERCASE".is_case(upper_case));
876            }
877        }
878
879        #[test]
880        fn capital_word() {
881            for capital_case in [
882                Case::Ada,
883                Case::Train,
884                Case::Pascal,
885                Case::Title,
886                Case::Sentence,
887            ] {
888                assert!("Capitalcase".is_case(capital_case));
889            }
890        }
891
892        #[test]
893        fn underscores_not_kebab() {
894            assert!(!"kebab-case".is_case(Case::Snake));
895        }
896
897        #[test]
898        fn multiple_delimiters() {
899            assert!(!"kebab-snake_case".is_case(Case::Snake));
900            assert!(!"kebab-snake_case".is_case(Case::Kebab));
901            assert!(!"kebab-snake_case".is_case(Case::Lower));
902        }
903
904        /*
905        #[test]
906        fn digits_ignored() {
907            assert!("UPPER_CASE_WITH_DIGIT1".is_case(Case::Constant));
908
909            assert!("transformation_2d".is_case(Case::Snake));
910
911            assert!("Transformation2d".is_case(Case::Pascal));
912            assert!("Transformation2D".is_case(Case::Pascal));
913
914            assert!("transformation2D".is_case(Case::Camel));
915
916            assert!(!"5isntPascal".is_case(Case::Pascal))
917        }
918        */
919
920        #[test]
921        fn not_a_case() {
922            for c in Case::all_cases() {
923                assert!(!"hyphen-and_underscore".is_case(*c));
924                assert!(!"Sentence-with-hyphens".is_case(*c));
925                assert!(!"Sentence_with_underscores".is_case(*c));
926            }
927        }
928    }
929
930    #[test]
931    fn remove_boundaries() {
932        assert_eq!(
933            "m02_s05_binary_trees.pdf",
934            "M02S05BinaryTrees.pdf"
935                .from_case(Case::Pascal)
936                .without_boundaries(&[Boundary::UpperDigit])
937                .to_case(Case::Snake)
938        );
939    }
940
941    #[test]
942    fn with_boundaries() {
943        assert_eq!(
944            "my-dumb-file-name",
945            "my_dumbFileName"
946                .with_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
947                .to_case(Case::Kebab)
948        );
949    }
950
951    #[cfg(feature = "random")]
952    #[test]
953    fn random_case_boundaries() {
954        for &random_case in Case::random_cases() {
955            assert_eq!(
956                "split_by_spaces",
957                "Split By Spaces"
958                    .from_case(random_case)
959                    .to_case(Case::Snake)
960            );
961        }
962    }
963
964    #[test]
965    fn multiple_from_case() {
966        assert_eq!(
967            "longtime_nosee",
968            "LongTime NoSee"
969                .from_case(Case::Camel)
970                .from_case(Case::Title)
971                .to_case(Case::Snake),
972        )
973    }
974
975    use std::collections::HashSet;
976    use std::iter::FromIterator;
977
978    #[test]
979    fn detect_many_cases() {
980        let lower_cases_vec = possible_cases(&"asef");
981        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
982        let mut actual = HashSet::new();
983        actual.insert(Case::Lower);
984        actual.insert(Case::Camel);
985        actual.insert(Case::Snake);
986        actual.insert(Case::Kebab);
987        actual.insert(Case::Flat);
988        assert_eq!(lower_cases_set, actual);
989
990        let lower_cases_vec = possible_cases(&"asefCase");
991        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
992        let mut actual = HashSet::new();
993        actual.insert(Case::Camel);
994        assert_eq!(lower_cases_set, actual);
995    }
996
997    #[test]
998    fn detect_each_case() {
999        let s = "My String Identifier".to_string();
1000        for &case in Case::deterministic_cases() {
1001            let new_s = s.from_case(case).to_case(case);
1002            let possible = possible_cases(&new_s);
1003            assert!(possible.iter().any(|c| c == &case));
1004        }
1005    }
1006
1007    // From issue https://github.com/rutrum/convert-case/issues/8
1008    #[test]
1009    fn accent_mark() {
1010        let s = "música moderna".to_string();
1011        assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
1012    }
1013
1014    // From issue https://github.com/rutrum/convert-case/issues/4
1015    #[test]
1016    fn russian() {
1017        let s = "ПЕРСПЕКТИВА24".to_string();
1018        let _n = s.to_case(Case::Title);
1019        let _n = s.to_case(Case::Alternating);
1020    }
1021
1022    // From issue https://github.com/rutrum/convert-case/issues/4
1023    #[test]
1024    #[cfg(feature = "random")]
1025    fn russian_random() {
1026        let s = "ПЕРСПЕКТИВА24".to_string();
1027        let _n = s.to_case(Case::Random);
1028        let _n = s.to_case(Case::PseudoRandom);
1029    }
1030}