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::remove_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".remove_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//!
141//! // not what you might expect!
142//! assert_eq!(ccase!(camel, "_empty__first_word"), "EmptyFirstWord");
143//! ```
144//!
145//! # Customizing Behavior
146//!
147//! Case conversion takes place in three steps:
148//! 1. Splitting the identifier into a list of words
149//! 2. Mutating the letter case of graphemes within each word
150//! 3. Joining the words back into an identifier using a delimiter
151//!
152//! Those are defined by boundaries, patterns, and delimiters respectively.  Graphically:
153//!
154//! ```md
155//! Identifier        Identifier'
156//!     |                 ^
157//!     | boundaries      | delimiter
158//!     V                 |
159//!   Words ----------> Words'
160//!           pattern
161//! ```
162//!
163//! ## Patterns
164//!
165//! How to change the case of letters across a list of words is called a _pattern_.
166//! A pattern is a function that when passed a `&[&str]`, produces a
167//! `Vec<String>`.  The [`Pattern`] enum encapsulates the common transformations
168//! used across all cases.  Although custom functions can be supplied with the
169//! [`Custom`](Pattern::Custom) variant.
170//!
171//! ## Boundaries
172//!
173//! The condition for splitting at part of an identifier, where to perform
174//! the split, and if any characters are removed are defined by [boundaries](Boundary).
175//! By default, identifiers are split based on [`Boundary::defaults`].  This list
176//! contains word boundaries that you would likely see after creating a multi-word
177//! identifier of typical cases.
178//!
179//! Custom boundary conditions can also be created.  Commonly, you might split based on some
180//! character or list of characters.  The [`delim_boundary`] macro builds
181//! a boundary that splits on the presence of a string, and then removes the string
182//! while producing the list of words.
183//!
184//! You can also use [`Boundary::Custom`] to explicitly define boundary
185//! conditions.  If you actually need to create a
186//! boundary condition from scratch, you should file an issue to let the author know
187//! how you used it.  I'm not certain what other boundary condition would be helpful.
188//!
189//! ## Cases
190//!
191//! A case is defined by a list of boundaries, a pattern, and a _delimiter_: the string to
192//! intersperse between words before concatenation. [`Case::Custom`] is a struct enum variant with
193//! exactly those three fields.  You could create your own case like so.
194//! ```
195//! use convert_case::{Case, Casing, delim_boundary, Pattern};
196//!
197//! let dot_case = Case::Custom {
198//!     boundaries: &[delim_boundary!(".")],
199//!     pattern: Pattern::Lowercase,
200//!     delim: ".",
201//! };
202//!
203//! assert_eq!("AnimalFactoryFactory".to_case(dot_case), "animal.factory.factory");
204//!
205//! assert_eq!(
206//!     "pd.options.mode.copy_on_write"
207//!         .from_case(dot_case)
208//!         .to_case(Case::Title),
209//!     "Pd Options Mode Copy_on_write",
210//! )
211//! ```
212//!
213//! ## Converter
214//!
215//! Case conversion with `convert_case` allows using attributes from two cases.  From
216//! the first case is how you split the identifier (the _from_ case), and
217//! from the second is how to mutate and join the words (the _to_ case.)  The
218//! [`Converter`] is used to define the _conversion_ process, not a case directly.
219//!
220//! It has the same fields as case, but is exposed via a builder interface
221//! and can be used to apply a conversion on a string directly, without
222//! specifying all the parameters at the time of conversion.
223//!
224//! In the below example, we build a converter that maps the double colon
225//! delimited module path in rust into a series of file directories.
226//!
227//! ```
228//! use convert_case::{Case, Converter, delim_boundary};
229//!
230//! let modules_into_path = Converter::new()
231//!     .set_boundaries(&[delim_boundary!("::")])
232//!     .set_delim("/");
233//!
234//! assert_eq!(
235//!     modules_into_path.convert("std::os::unix"),
236//!     "std/os/unix",
237//! );
238//! ```
239//!
240//! # Associated Projects
241//!
242//! ## Rust library `convert_case_extras`
243//!
244//! Some extra utilties for convert_case that don't need to be in the main library.
245//! You can read more here: [`convert_case_extras`](https://docs.rs/convert_case_extras).
246//!
247//! ## stringcase.org
248//!
249//! While developing `convert_case`, the author became fascinated in the naming conventions
250//! used for cases as well as different implementations for conversion.  On [stringcase.org](https://stringcase.org)
251//! is documentation of the history of naming conventions, a catalogue of case conversion tools,
252//! and a more rigorous definition of what it means to "convert the case of an identifier."
253//!
254//! ## Command Line Utility `ccase`
255//!
256//! `convert_case` was originally developed for the purposes of a command line utility
257//! for converting the case of strings and filenames.  You can check out
258//! [`ccase` on Github](https://github.com/rutrum/ccase).
259#![cfg_attr(not(test), no_std)]
260extern crate alloc;
261
262use alloc::string::String;
263
264mod boundary;
265mod case;
266mod converter;
267mod pattern;
268
269pub use boundary::{split, Boundary};
270pub use case::Case;
271pub use converter::Converter;
272pub use pattern::Pattern;
273
274/// Describes items that can be converted into a case.  This trait is used
275/// in conjunction with the [`StateConverter`] struct which is returned from a couple
276/// methods on `Casing`.
277pub trait Casing<T: AsRef<str>> {
278    /// Convert the string into the given case.  It will reference `self` and create a new
279    /// `String` with the same pattern and delimeter as `case`.  It will split on boundaries
280    /// defined at [`Boundary::defaults()`].
281    /// ```
282    /// use convert_case::{Case, Casing};
283    ///
284    /// assert_eq!(
285    ///     "tetronimo-piece-border",
286    ///     "Tetronimo piece border".to_case(Case::Kebab)
287    /// );
288    /// ```
289    fn to_case(&self, case: Case) -> String;
290
291    /// Start the case conversion by storing the boundaries associated with the given case.
292    /// ```
293    /// use convert_case::{Case, Casing};
294    ///
295    /// assert_eq!(
296    ///     "2020-08-10_dannie_birthday",
297    ///     "2020-08-10 Dannie Birthday"
298    ///         .from_case(Case::Title)
299    ///         .to_case(Case::Snake)
300    /// );
301    /// ```
302    #[allow(clippy::wrong_self_convention)]
303    fn from_case(&self, case: Case) -> StateConverter<T>;
304
305    /// Creates a `StateConverter` struct initialized with the boundaries provided.
306    /// ```
307    /// use convert_case::{Boundary, Case, Casing};
308    ///
309    /// assert_eq!(
310    ///     "e1_m1_hangar",
311    ///     "E1M1 Hangar"
312    ///         .set_boundaries(&[Boundary::DigitUpper, Boundary::Space])
313    ///         .to_case(Case::Snake)
314    /// );
315    /// ```
316    fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
317
318    /// Creates a `StateConverter` struct initialized without the boundaries
319    /// provided.
320    /// ```
321    /// use convert_case::{Boundary, Case, Casing};
322    ///
323    /// assert_eq!(
324    ///     "2d_transformation",
325    ///     "2dTransformation"
326    ///         .remove_boundaries(&Boundary::digits())
327    ///         .to_case(Case::Snake)
328    /// );
329    /// ```
330    fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
331
332    /// Determines if `self` is of the given case.  This is done simply by applying
333    /// the conversion and seeing if the result is the same.
334    /// ```
335    /// use convert_case::{Case, Casing};
336    ///
337    /// assert!( "kebab-case-string".is_case(Case::Kebab));
338    /// assert!( "Train-Case-String".is_case(Case::Train));
339    ///
340    /// assert!(!"kebab-case-string".is_case(Case::Snake));
341    /// assert!(!"kebab-case-string".is_case(Case::Train));
342    /// ```
343    fn is_case(&self, case: Case) -> bool;
344}
345
346impl<T: AsRef<str>> Casing<T> for T {
347    fn to_case(&self, case: Case) -> String {
348        StateConverter::new(self).to_case(case)
349    }
350
351    fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
352        StateConverter::new(self).set_boundaries(bs)
353    }
354
355    fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
356        StateConverter::new(self).remove_boundaries(bs)
357    }
358
359    fn from_case(&self, case: Case) -> StateConverter<T> {
360        StateConverter::new(self).from_case(case)
361    }
362
363    fn is_case(&self, case: Case) -> bool {
364        self.as_ref() == self.to_case(case).as_str()
365        /*
366        let digitless = self
367            .as_ref()
368            .chars()
369            .filter(|x| !x.is_ascii_digit())
370            .collect::<String>();
371
372        digitless == digitless.to_case(case)
373        */
374    }
375}
376
377/// Holds information about parsing before converting into a case.
378///
379/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
380/// `Casing`.  For a more fine grained approach to case conversion, consider using the [`Converter`]
381/// struct.
382/// ```
383/// use convert_case::{Case, Casing};
384///
385/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
386/// assert_eq!("Ninety-nine Problems", title);
387/// ```
388pub struct StateConverter<'a, T: AsRef<str>> {
389    s: &'a T,
390    conv: Converter,
391}
392
393impl<'a, T: AsRef<str>> StateConverter<'a, T> {
394    /// Only called by Casing function to_case()
395    fn new(s: &'a T) -> Self {
396        Self {
397            s,
398            conv: Converter::new(),
399        }
400    }
401
402    /// Uses the boundaries associated with `case` for word segmentation.  This
403    /// will overwrite any boundary information initialized before.  This method is
404    /// likely not useful, but provided anyway.
405    /// ```
406    /// use convert_case::{Case, Casing};
407    ///
408    /// let name = "Chuck Schuldiner"
409    ///     .from_case(Case::Snake) // from Casing trait
410    ///     .from_case(Case::Title) // from StateConverter, overwrites previous
411    ///     .to_case(Case::Kebab);
412    /// assert_eq!("chuck-schuldiner", name);
413    /// ```
414    pub fn from_case(self, case: Case) -> Self {
415        Self {
416            conv: self.conv.from_case(case),
417            ..self
418        }
419    }
420
421    /// Overwrites boundaries for word segmentation with those provided.  This will overwrite
422    /// any boundary information initialized before.  This method is likely not useful, but
423    /// provided anyway.
424    /// ```
425    /// use convert_case::{Boundary, Case, Casing};
426    ///
427    /// let song = "theHumbling river-puscifer"
428    ///     .from_case(Case::Kebab) // from Casing trait
429    ///     .set_boundaries(&[Boundary::Space, Boundary::LowerUpper]) // overwrites `from_case`
430    ///     .to_case(Case::Pascal);
431    /// assert_eq!("TheHumblingRiver-puscifer", song);  // doesn't split on hyphen `-`
432    /// ```
433    pub fn set_boundaries(self, bs: &[Boundary]) -> Self {
434        Self {
435            s: self.s,
436            conv: self.conv.set_boundaries(bs),
437        }
438    }
439
440    /// Removes any boundaries that were already initialized.  This is particularly useful when a
441    /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
442    /// some.
443    /// ```
444    /// use convert_case::{Boundary, Case, Casing};
445    ///
446    /// assert_eq!(
447    ///     "2d_transformation",
448    ///     "2dTransformation"
449    ///         .from_case(Case::Camel)
450    ///         .remove_boundaries(&Boundary::digits())
451    ///         .to_case(Case::Snake)
452    /// );
453    /// ```
454    pub fn remove_boundaries(self, bs: &[Boundary]) -> Self {
455        Self {
456            s: self.s,
457            conv: self.conv.remove_boundaries(bs),
458        }
459    }
460
461    /// Consumes the `StateConverter` and returns the converted string.
462    /// ```
463    /// use convert_case::{Boundary, Case, Casing};
464    ///
465    /// assert_eq!(
466    ///     "ice-cream social",
467    ///     "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower)
468    /// );
469    /// ```
470    pub fn to_case(self, case: Case) -> String {
471        self.conv.to_case(case).convert(self.s)
472    }
473}
474
475/// The variant of `case` from a token.
476///
477/// The token associated with each variant is the variant written in snake case.
478#[macro_export]
479macro_rules! case {
480    (snake) => {
481        convert_case::Case::Snake
482    };
483    (constant) => {
484        convert_case::Case::Constant
485    };
486    (upper_snake) => {
487        convert_case::Case::UpperSnake
488    };
489    (ada) => {
490        convert_case::Case::Ada;
491    };
492    (kebab) => {
493        convert_case::Case::Kebab
494    };
495    (cobol) => {
496        convert_case::Case::Cobol
497    };
498    (upper_kebab) => {
499        convert_case::Case::UpperKebab
500    };
501    (train) => {
502        convert_case::Case::Train
503    };
504    (flat) => {
505        convert_case::Case::Flat
506    };
507    (upper_flat) => {
508        convert_case::Case::UpperFlat
509    };
510    (pascal) => {
511        convert_case::Case::Pascal
512    };
513    (upper_camel) => {
514        convert_case::Case::UpperCamel
515    };
516    (camel) => {
517        convert_case::Case::Camel
518    };
519    (lower) => {
520        convert_case::Case::Lower
521    };
522    (upper) => {
523        convert_case::Case::Upper
524    };
525    (title) => {
526        convert_case::Case::Title
527    };
528    (sentence) => {
529        convert_case::Case::Sentence
530    };
531}
532
533/// Convert an identifier into a case.
534///
535/// The macro can be used as follows.
536/// ```
537/// use convert_case::ccase;
538///
539/// assert_eq!(ccase!(snake, "myVarName"), "my_var_name");
540/// // equivalent to
541/// // "myVarName".to_case(Case::Snake)
542/// ```
543/// You can also specify a _from_ case, or the case that determines how the input
544/// string is split into words.
545/// ```
546/// use convert_case::ccase;
547///
548/// assert_eq!(ccase!(sentence -> snake, "Ice-cream sales"), "ice-cream_sales");
549/// // equivalent to
550/// // "Ice-cream sales".from_case(Case::Sentence).to_case(Case::Snake)
551/// ```
552#[macro_export]
553macro_rules! ccase {
554    ($case:ident, $e:expr) => {
555        convert_case::Converter::new()
556            .to_case(convert_case::case!($case))
557            .convert($e)
558    };
559    ($from:ident -> $to:ident, $e:expr) => {
560        convert_case::Converter::new()
561            .from_case(convert_case::case!($from))
562            .to_case(convert_case::case!($to))
563            .convert($e)
564    };
565}
566
567#[cfg(test)]
568mod test {
569    use super::*;
570
571    use alloc::vec;
572    use alloc::vec::Vec;
573
574    fn possible_cases(s: &str) -> Vec<Case> {
575        Case::all_cases()
576            .iter()
577            .filter(|&case| s.from_case(*case).to_case(*case) == s)
578            .map(|c| *c)
579            .collect()
580    }
581
582    #[test]
583    fn lossless_against_lossless() {
584        let examples = vec![
585            (Case::Snake, "my_variable_22_name"),
586            (Case::Constant, "MY_VARIABLE_22_NAME"),
587            (Case::Ada, "My_Variable_22_Name"),
588            (Case::Kebab, "my-variable-22-name"),
589            (Case::Cobol, "MY-VARIABLE-22-NAME"),
590            (Case::Train, "My-Variable-22-Name"),
591            (Case::Pascal, "MyVariable22Name"),
592            (Case::Camel, "myVariable22Name"),
593            (Case::Lower, "my variable 22 name"),
594            (Case::Upper, "MY VARIABLE 22 NAME"),
595            (Case::Title, "My Variable 22 Name"),
596            (Case::Sentence, "My variable 22 name"),
597        ];
598
599        for (case_a, str_a) in &examples {
600            for (case_b, str_b) in &examples {
601                assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
602            }
603        }
604    }
605
606    #[test]
607    fn obvious_default_parsing() {
608        let examples = vec![
609            "SuperMario64Game",
610            "super-mario64-game",
611            "superMario64 game",
612            "Super Mario 64_game",
613            "SUPERMario 64-game",
614            "super_mario-64 game",
615        ];
616
617        for example in examples {
618            assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
619        }
620    }
621
622    #[test]
623    fn multiline_strings() {
624        assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
625    }
626
627    #[test]
628    fn camel_case_acroynms() {
629        assert_eq!(
630            "xml_http_request",
631            "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
632        );
633        assert_eq!(
634            "xml_http_request",
635            "XMLHttpRequest"
636                .from_case(Case::UpperCamel)
637                .to_case(Case::Snake)
638        );
639        assert_eq!(
640            "xml_http_request",
641            "XMLHttpRequest"
642                .from_case(Case::Pascal)
643                .to_case(Case::Snake)
644        );
645    }
646
647    #[test]
648    fn leading_tailing_delimeters() {
649        assert_eq!(
650            "_leading_underscore",
651            "_leading_underscore"
652                .from_case(Case::Snake)
653                .to_case(Case::Snake)
654        );
655        assert_eq!(
656            "tailing_underscore_",
657            "tailing_underscore_"
658                .from_case(Case::Snake)
659                .to_case(Case::Snake)
660        );
661        assert_eq!(
662            "_leading_hyphen",
663            "-leading-hyphen"
664                .from_case(Case::Kebab)
665                .to_case(Case::Snake)
666        );
667        assert_eq!(
668            "tailing_hyphen_",
669            "tailing-hyphen-"
670                .from_case(Case::Kebab)
671                .to_case(Case::Snake)
672        );
673        assert_eq!(
674            "tailing_hyphens_____",
675            "tailing-hyphens-----"
676                .from_case(Case::Kebab)
677                .to_case(Case::Snake)
678        );
679        assert_eq!(
680            "tailingHyphens",
681            "tailing-hyphens-----"
682                .from_case(Case::Kebab)
683                .to_case(Case::Camel)
684        );
685    }
686
687    #[test]
688    fn double_delimeters() {
689        assert_eq!(
690            "many___underscores",
691            "many___underscores"
692                .from_case(Case::Snake)
693                .to_case(Case::Snake)
694        );
695        assert_eq!(
696            "many---underscores",
697            "many---underscores"
698                .from_case(Case::Kebab)
699                .to_case(Case::Kebab)
700        );
701    }
702
703    #[test]
704    fn early_word_boundaries() {
705        assert_eq!(
706            "a_bagel",
707            "aBagel".from_case(Case::Camel).to_case(Case::Snake)
708        );
709    }
710
711    #[test]
712    fn late_word_boundaries() {
713        assert_eq!(
714            "team_a",
715            "teamA".from_case(Case::Camel).to_case(Case::Snake)
716        );
717    }
718
719    #[test]
720    fn empty_string() {
721        for (case_a, case_b) in Case::all_cases()
722            .into_iter()
723            .zip(Case::all_cases().into_iter())
724        {
725            assert_eq!("", "".from_case(*case_a).to_case(*case_b));
726        }
727    }
728
729    #[test]
730    fn default_all_boundaries() {
731        assert_eq!(
732            "abc_abc_abc_abc_abc_abc",
733            "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
734        );
735        assert_eq!("8_a_8_a_8", "8a8A8".to_case(Case::Snake));
736    }
737
738    mod is_case {
739        use super::*;
740
741        #[test]
742        fn snake() {
743            assert!("im_snake_case".is_case(Case::Snake));
744            assert!(!"im_NOTsnake_case".is_case(Case::Snake));
745        }
746
747        #[test]
748        fn kebab() {
749            assert!("im-kebab-case".is_case(Case::Kebab));
750            assert!(!"im_not_kebab".is_case(Case::Kebab));
751        }
752
753        #[test]
754        fn lowercase_word() {
755            for lower_case in [
756                Case::Snake,
757                Case::Kebab,
758                Case::Flat,
759                Case::Lower,
760                Case::Camel,
761            ] {
762                assert!("lowercase".is_case(lower_case));
763            }
764        }
765
766        #[test]
767        fn uppercase_word() {
768            for upper_case in [Case::Constant, Case::Cobol, Case::UpperFlat, Case::Upper] {
769                assert!("UPPERCASE".is_case(upper_case));
770            }
771        }
772
773        #[test]
774        fn capital_word() {
775            for capital_case in [
776                Case::Ada,
777                Case::Train,
778                Case::Pascal,
779                Case::Title,
780                Case::Sentence,
781            ] {
782                assert!("Capitalcase".is_case(capital_case));
783            }
784        }
785
786        #[test]
787        fn underscores_not_kebab() {
788            assert!(!"kebab-case".is_case(Case::Snake));
789        }
790
791        #[test]
792        fn multiple_delimiters() {
793            assert!(!"kebab-snake_case".is_case(Case::Snake));
794            assert!(!"kebab-snake_case".is_case(Case::Kebab));
795            assert!(!"kebab-snake_case".is_case(Case::Lower));
796        }
797
798        /*
799        #[test]
800        fn digits_ignored() {
801            assert!("UPPER_CASE_WITH_DIGIT1".is_case(Case::Constant));
802
803            assert!("transformation_2d".is_case(Case::Snake));
804
805            assert!("Transformation2d".is_case(Case::Pascal));
806            assert!("Transformation2D".is_case(Case::Pascal));
807
808            assert!("transformation2D".is_case(Case::Camel));
809
810            assert!(!"5isntPascal".is_case(Case::Pascal))
811        }
812        */
813
814        #[test]
815        fn not_a_case() {
816            for c in Case::all_cases() {
817                assert!(!"hyphen-and_underscore".is_case(*c));
818                assert!(!"Sentence-with-hyphens".is_case(*c));
819                assert!(!"Sentence_with_underscores".is_case(*c));
820            }
821        }
822    }
823
824    #[test]
825    fn remove_boundaries() {
826        assert_eq!(
827            "m02_s05_binary_trees.pdf",
828            "M02S05BinaryTrees.pdf"
829                .from_case(Case::Pascal)
830                .remove_boundaries(&[Boundary::UpperDigit])
831                .to_case(Case::Snake)
832        );
833    }
834
835    #[test]
836    fn with_boundaries() {
837        assert_eq!(
838            "my-dumb-file-name",
839            "my_dumbFileName"
840                .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
841                .to_case(Case::Kebab)
842        );
843    }
844
845    #[test]
846    fn multiple_from_case() {
847        assert_eq!(
848            "longtime_nosee",
849            "LongTime NoSee"
850                .from_case(Case::Camel)
851                .from_case(Case::Title)
852                .to_case(Case::Snake),
853        )
854    }
855
856    use std::collections::HashSet;
857    use std::iter::FromIterator;
858
859    #[test]
860    fn detect_many_cases() {
861        let lower_cases_vec = possible_cases(&"asef");
862        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
863        let mut actual = HashSet::new();
864        actual.insert(Case::Lower);
865        actual.insert(Case::Camel);
866        actual.insert(Case::Snake);
867        actual.insert(Case::Kebab);
868        actual.insert(Case::Flat);
869        assert_eq!(lower_cases_set, actual);
870
871        let lower_cases_vec = possible_cases(&"asefCase");
872        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
873        let mut actual = HashSet::new();
874        actual.insert(Case::Camel);
875        assert_eq!(lower_cases_set, actual);
876    }
877
878    #[test]
879    fn detect_each_case() {
880        let s = "My String Identifier".to_string();
881        for &case in Case::all_cases() {
882            let new_s = s.from_case(case).to_case(case);
883            let possible = possible_cases(&new_s);
884            assert!(possible.iter().any(|c| c == &case));
885        }
886    }
887
888    // From issue https://github.com/rutrum/convert-case/issues/8
889    #[test]
890    fn accent_mark() {
891        let s = "música moderna".to_string();
892        assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
893    }
894
895    // From issue https://github.com/rutrum/convert-case/issues/4
896    #[test]
897    fn russian() {
898        let s = "ПЕРСПЕКТИВА24".to_string();
899        let _n = s.to_case(Case::Title);
900    }
901
902    // idea for asserting the associated boundaries are correct
903    #[test]
904    fn appropriate_associated_boundaries() {
905        let word_groups = &[
906            vec!["my", "var", "name"],
907            vec!["MY", "var", "Name"],
908            vec!["another", "vAR"],
909            vec!["XML", "HTTP", "Request"],
910        ];
911
912        for words in word_groups {
913            for case in Case::all_cases() {
914                if case == &Case::Flat || case == &Case::UpperFlat {
915                    continue;
916                }
917                assert_eq!(
918                    case.pattern().mutate(&split(
919                        &case.pattern().mutate(words).join(case.delim()),
920                        case.boundaries()
921                    )),
922                    case.pattern().mutate(words),
923                    "Test boundaries on Case::{:?} with {:?}",
924                    case,
925                    words,
926                );
927            }
928        }
929    }
930}