Skip to main content

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 propagates 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//! To remove empty words before joining, you can call `remove_empty` from the
145//! `Casing` trait before finishing the conversion.
146//! ```
147//! # use convert_case::{Casing, Case};
148//! assert_eq!(
149//!     "_empty__first_word".remove_empty().to_case(Case::Camel),
150//!     "emptyFirstWord"
151//! )
152//! ```
153//!
154//! # Customizing Behavior
155//!
156//! Case conversion takes place in three steps:
157//! 1. Splitting the identifier into a list of words
158//! 2. Mutating the letter case of graphemes within each word
159//! 3. Joining the words back into an identifier using a delimiter
160//!
161//! Those are defined by boundaries, patterns, and delimiters respectively.  Graphically:
162//!
163//! ```md
164//! Identifier        Identifier'
165//!     |                 ^
166//!     | boundaries      | delimiter
167//!     V                 |
168//!   Words ----------> Words'
169//!           patterns
170//! ```
171//!
172//! ## Patterns
173//!
174//! How to change the case of letters across a list of words is called a _pattern_.
175//! A pattern is a function that when passed a `&[&str]`, produces a
176//! `Vec<String>`.  The [`Pattern`] enum encapsulates the common transformations
177//! used across all cases.  Although custom functions can be supplied with the
178//! [`Custom`](Pattern::Custom) variant.
179//!
180//! ## Boundaries
181//!
182//! The condition for splitting at part of an identifier, where to perform
183//! the split, and if any characters are removed are defined by [boundaries](Boundary).
184//! By default, identifiers are split based on [`Boundary::defaults`].  This list
185//! contains word boundaries that you would likely see after creating a multi-word
186//! identifier of typical cases.
187//!
188//! Custom boundary conditions can also be created.  Commonly, you might split based on some
189//! character or list of characters.  The [`separator`] macro builds
190//! a boundary that splits on the presence of a string, and then removes the string
191//! while producing the list of words.
192//!
193//! You can also use [`Boundary::Custom`] to explicitly define boundary
194//! conditions.  If you actually need to create a
195//! boundary condition from scratch, you should file an issue to let the author know
196//! how you used it.  I'm not certain what other boundary condition would be helpful.
197//!
198//! ## Cases
199//!
200//! A case is defined by a list of boundaries, a pattern, and a _delimiter_: the string to
201//! intersperse between words before concatenation. [`Case::Custom`] is a struct enum variant with
202//! exactly those three fields.  You could create your own case like so.
203//! ```
204//! use convert_case::{Case, Casing, separator, Pattern};
205//!
206//! let dot_case = Case::Custom {
207//!     boundaries: &[separator!(".")],
208//!     pattern: Pattern::Lowercase,
209//!     delimiter: ".",
210//! };
211//!
212//! assert_eq!("AnimalFactoryFactory".to_case(dot_case), "animal.factory.factory");
213//!
214//! assert_eq!(
215//!     "pd.options.mode.copy_on_write"
216//!         .from_case(dot_case)
217//!         .to_case(Case::Title),
218//!     "Pd Options Mode Copy_on_write",
219//! )
220//! ```
221//!
222//! ## Converter
223//!
224//! Case conversion with `convert_case` allows using attributes from two cases.  From
225//! the first case is how you split the identifier (the _from_ case), and
226//! from the second is how to mutate and join the words (the _to_ case.)  The
227//! [`Converter`] is used to define the _conversion_ process, not a case directly.
228//!
229//! It has the same fields as case, but is exposed via a builder interface
230//! and can be used to apply a conversion on a string directly, without
231//! specifying all the parameters at the time of conversion.
232//!
233//! In the below example, we build a converter that maps the double colon
234//! delimited module path in rust into a series of file directories.
235//!
236//! ```
237//! use convert_case::{Case, Converter, separator};
238//!
239//! let modules_into_path = Converter::new()
240//!     .set_boundaries(&[separator!("::")])
241//!     .set_delimiter("/");
242//!
243//! assert_eq!(
244//!     modules_into_path.convert("std::os::unix"),
245//!     "std/os/unix",
246//! );
247//! ```
248//!
249//! # Associated Projects
250//!
251//! ## Rust library `convert_case_extras`
252//!
253//! Some extra utilities for convert_case that don't need to be in the main library.
254//! You can read more here: [`convert_case_extras`](https://docs.rs/convert_case_extras).
255//!
256//! ## stringcase.org
257//!
258//! While developing `convert_case`, the author became fascinated in the naming conventions
259//! used for cases as well as different implementations for conversion.  On [stringcase.org](https://stringcase.org)
260//! is documentation of the history of naming conventions, a catalogue of case conversion tools,
261//! and a more rigorous definition of what it means to "convert the case of an identifier."
262//!
263//! ## Command Line Utility `ccase`
264//!
265//! `convert_case` was originally developed for the purposes of a command line utility
266//! for converting the case of strings and filenames.  You can check out
267//! [`ccase` on Github](https://github.com/rutrum/ccase).
268#![cfg_attr(not(test), no_std)]
269extern crate alloc;
270
271use alloc::string::String;
272
273mod boundary;
274mod case;
275mod converter;
276mod pattern;
277
278pub use boundary::{split, Boundary};
279pub use case::Case;
280pub use converter::Converter;
281pub use pattern::Pattern;
282
283/// Describes items that can be converted into a case.  This trait is used
284/// in conjunction with the [`StateConverter`] struct which is returned from a couple
285/// methods on `Casing`.
286pub trait Casing<T: AsRef<str>> {
287    /// Convert the string into the given case.  It will reference `self` and create a new
288    /// `String` with the same pattern and delimiter as `case`.  It will split on boundaries
289    /// defined at [`Boundary::defaults()`].
290    /// ```
291    /// use convert_case::{Case, Casing};
292    ///
293    /// assert_eq!(
294    ///     "Tetronimo piece border".to_case(Case::Kebab),
295    ///     "tetronimo-piece-border",
296    /// );
297    /// ```
298    fn to_case(&self, case: Case) -> String;
299
300    /// Start the case conversion by storing the boundaries associated with the given case.
301    /// ```
302    /// use convert_case::{Case, Casing};
303    ///
304    /// assert_eq!(
305    ///     "2020-08-10 Dannie Birthday"
306    ///         .from_case(Case::Title)
307    ///         .to_case(Case::Snake),
308    ///     "2020-08-10_dannie_birthday",
309    /// );
310    /// ```
311    #[allow(clippy::wrong_self_convention)]
312    fn from_case(&self, case: Case) -> StateConverter<'_, T>;
313
314    /// Creates a `StateConverter` struct initialized with the boundaries provided.
315    /// ```
316    /// use convert_case::{Boundary, Case, Casing};
317    ///
318    /// assert_eq!(
319    ///     "E1M1 Hangar"
320    ///         .set_boundaries(&[Boundary::DigitUpper, Boundary::Space])
321    ///         .to_case(Case::Snake),
322    ///     "e1_m1_hangar",
323    /// );
324    /// ```
325    fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T>;
326
327    /// Creates a `StateConverter` struct initialized without the boundaries
328    /// provided.
329    /// ```
330    /// use convert_case::{Boundary, Case, Casing};
331    ///
332    /// assert_eq!(
333    ///     "2d_transformation",
334    ///     "2dTransformation"
335    ///         .remove_boundaries(&Boundary::digits())
336    ///         .to_case(Case::Snake)
337    /// );
338    /// ```
339    fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T>;
340
341    /// Creates a `StateConverter` with the `RemoveEmpty` pattern prepended.
342    /// This filters out empty words before conversion, useful when splitting
343    /// produces empty words from leading, trailing, and duplicate delimiters.
344    /// ```
345    /// use convert_case::{Case, Casing};
346    ///
347    /// assert_eq!(
348    ///     "--leading-delims"
349    ///         .from_case(Case::Kebab)
350    ///         .remove_empty()
351    ///         .to_case(Case::Camel),
352    ///     "leadingDelims",
353    /// );
354    /// ```
355    fn remove_empty(&self) -> StateConverter<'_, T>;
356}
357
358impl<T: AsRef<str>> Casing<T> for T {
359    fn to_case(&self, case: Case) -> String {
360        StateConverter::new(self).to_case(case)
361    }
362
363    fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T> {
364        StateConverter::new(self).set_boundaries(bs)
365    }
366
367    fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T> {
368        StateConverter::new(self).remove_boundaries(bs)
369    }
370
371    fn from_case(&self, case: Case) -> StateConverter<'_, T> {
372        StateConverter::new(self).from_case(case)
373    }
374
375    fn remove_empty(&self) -> StateConverter<'_, T> {
376        StateConverter::new(self).remove_empty()
377    }
378}
379
380/// Holds information about parsing before converting into a case.
381///
382/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
383/// `Casing`.  For a more fine grained approach to case conversion, consider using the [`Converter`]
384/// struct.
385/// ```
386/// # use convert_case::{Case, Casing};
387/// assert_eq!(
388///     "By-Tor And The Snow Dog".from_case(Case::Title).to_case(Case::Snake),
389///     "by-tor_and_the_snow_dog",
390/// );
391/// ```
392pub struct StateConverter<'a, T: AsRef<str>> {
393    s: &'a T,
394    conv: Converter,
395}
396
397impl<'a, T: AsRef<str>> StateConverter<'a, T> {
398    /// Only called by Casing function to_case()
399    fn new(s: &'a T) -> Self {
400        Self {
401            s,
402            conv: Converter::new(),
403        }
404    }
405
406    /// Uses the boundaries associated with `case` for word segmentation.  This
407    /// will overwrite any boundary information initialized before.  This method is
408    /// likely not useful, but provided anyway.
409    /// ```
410    /// use convert_case::{Case, Casing};
411    /// assert_eq!(
412    ///     "Alan Turing"
413    ///         .from_case(Case::Snake) // from Casing trait
414    ///         .from_case(Case::Title) // from StateConverter, overwrites previous
415    ///         .to_case(Case::Kebab),
416    ///     "alan-turing"
417    /// );
418    /// ```
419    pub fn from_case(self, case: Case) -> Self {
420        Self {
421            conv: self.conv.from_case(case),
422            ..self
423        }
424    }
425
426    /// Overwrites boundaries for word segmentation with those provided.  This will overwrite
427    /// any boundary information initialized before.  This method is likely not useful, but
428    /// provided anyway.
429    /// ```
430    /// use convert_case::{Boundary, Case, Casing};
431    /// assert_eq!(
432    ///     "Vector5d Transformation"
433    ///         .from_case(Case::Title) // from Casing trait
434    ///         .set_boundaries(&[Boundary::Space, Boundary::LowerDigit]) // overwrites `from_case`
435    ///         .to_case(Case::Kebab),
436    ///     "vector-5d-transformation"
437    /// );
438    /// ```
439    pub fn set_boundaries(self, bs: &[Boundary]) -> Self {
440        Self {
441            s: self.s,
442            conv: self.conv.set_boundaries(bs),
443        }
444    }
445
446    /// Removes any boundaries that were already initialized.  This is particularly useful when a
447    /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
448    /// some.
449    /// ```
450    /// use convert_case::{Boundary, Case, Casing};
451    /// assert_eq!(
452    ///     "2dTransformation"
453    ///         .from_case(Case::Camel)
454    ///         .remove_boundaries(&Boundary::digits())
455    ///         .to_case(Case::Snake),
456    ///     "2d_transformation"
457    /// );
458    /// ```
459    pub fn remove_boundaries(self, bs: &[Boundary]) -> Self {
460        Self {
461            s: self.s,
462            conv: self.conv.remove_boundaries(bs),
463        }
464    }
465
466    /// Prepends the `RemoveEmpty` pattern to filter out empty words before conversion.
467    /// This is useful when splitting produces empty words from leading, trailing, and
468    /// duplicate delimiters.
469    /// ```
470    /// use convert_case::{Case, Casing};
471    /// assert_eq!(
472    ///     "_leading_underscore"
473    ///         .from_case(Case::Snake)
474    ///         .remove_empty()
475    ///         .to_case(Case::Camel),
476    ///     "leadingUnderscore"
477    /// );
478    /// ```
479    pub fn remove_empty(self) -> Self {
480        Self {
481            s: self.s,
482            conv: self.conv.add_pattern(pattern::Pattern::RemoveEmpty),
483        }
484    }
485
486    /// Consumes the `StateConverter` and returns the converted string.
487    /// ```
488    /// use convert_case::{Boundary, Case, Casing};
489    /// assert_eq!(
490    ///     "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower),
491    ///     "ice-cream social",
492    /// );
493    /// ```
494    pub fn to_case(self, case: Case) -> String {
495        self.conv.to_case(case).convert(self.s)
496    }
497}
498
499/// The variant of `case` from a token.
500///
501/// The token associated with each variant is the variant written in snake case.
502/// To do conversion with a macro, see [`ccase`].
503#[macro_export]
504macro_rules! case {
505    (snake) => {
506        convert_case::Case::Snake
507    };
508    (constant) => {
509        convert_case::Case::Constant
510    };
511    (upper_snake) => {
512        convert_case::Case::UpperSnake
513    };
514    (ada) => {
515        convert_case::Case::Ada;
516    };
517    (kebab) => {
518        convert_case::Case::Kebab
519    };
520    (cobol) => {
521        convert_case::Case::Cobol
522    };
523    (upper_kebab) => {
524        convert_case::Case::UpperKebab
525    };
526    (train) => {
527        convert_case::Case::Train
528    };
529    (flat) => {
530        convert_case::Case::Flat
531    };
532    (upper_flat) => {
533        convert_case::Case::UpperFlat
534    };
535    (pascal) => {
536        convert_case::Case::Pascal
537    };
538    (upper_camel) => {
539        convert_case::Case::UpperCamel
540    };
541    (camel) => {
542        convert_case::Case::Camel
543    };
544    (lower) => {
545        convert_case::Case::Lower
546    };
547    (upper) => {
548        convert_case::Case::Upper
549    };
550    (title) => {
551        convert_case::Case::Title
552    };
553    (sentence) => {
554        convert_case::Case::Sentence
555    };
556}
557
558/// Convert an identifier into a case.
559///
560/// You can convert a string by writing the case name as a token.
561/// ```
562/// use convert_case::ccase;
563///
564/// assert_eq!(ccase!(snake, "myVarName"), "my_var_name");
565/// // equivalent to
566/// // "myVarName".to_case(Case::Snake)
567/// ```
568/// You can also specify a _from_ case, or the case that determines how the input
569/// string is split into words.
570/// ```
571/// use convert_case::ccase;
572///
573/// assert_eq!(ccase!(sentence -> snake, "Ice-cream sales"), "ice-cream_sales");
574/// // equivalent to
575/// // "Ice-cream sales".from_case(Case::Sentence).to_case(Case::Snake)
576/// ```
577#[macro_export]
578macro_rules! ccase {
579    ($case:ident, $e:expr) => {
580        convert_case::Converter::new()
581            .to_case(convert_case::case!($case))
582            .convert($e)
583    };
584    ($from:ident -> $to:ident, $e:expr) => {
585        convert_case::Converter::new()
586            .from_case(convert_case::case!($from))
587            .to_case(convert_case::case!($to))
588            .convert($e)
589    };
590}
591
592#[cfg(test)]
593mod test {
594    use super::*;
595
596    use alloc::vec;
597
598    #[test]
599    fn lossless_against_lossless() {
600        let examples = vec![
601            (Case::Snake, "my_variable_22_name"),
602            (Case::Constant, "MY_VARIABLE_22_NAME"),
603            (Case::Ada, "My_Variable_22_Name"),
604            (Case::Kebab, "my-variable-22-name"),
605            (Case::Cobol, "MY-VARIABLE-22-NAME"),
606            (Case::Train, "My-Variable-22-Name"),
607            (Case::Pascal, "MyVariable22Name"),
608            (Case::Camel, "myVariable22Name"),
609            (Case::Lower, "my variable 22 name"),
610            (Case::Upper, "MY VARIABLE 22 NAME"),
611            (Case::Title, "My Variable 22 Name"),
612            (Case::Sentence, "My variable 22 name"),
613        ];
614
615        for (case_a, str_a) in &examples {
616            for (case_b, str_b) in &examples {
617                assert_eq!(str_b.to_case(*case_a), *str_a);
618                assert_eq!(str_b.from_case(*case_b).to_case(*case_a), *str_a);
619            }
620        }
621    }
622
623    #[test]
624    fn obvious_default_parsing() {
625        let examples = vec![
626            "SuperMario64Game",
627            "super-mario64-game",
628            "superMario64 game",
629            "Super Mario 64_game",
630            "SUPERMario 64-game",
631            "super_mario-64 game",
632        ];
633
634        for example in examples {
635            assert_eq!(example.to_case(Case::Snake), "super_mario_64_game");
636        }
637    }
638
639    #[test]
640    fn multiline_strings() {
641        assert_eq!("one\ntwo\nthree".to_case(Case::Title), "One\ntwo\nthree");
642    }
643
644    #[test]
645    fn camel_case_acronyms() {
646        assert_eq!(
647            "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake),
648            "xml_http_request"
649        );
650        assert_eq!(
651            "XMLHttpRequest"
652                .from_case(Case::UpperCamel)
653                .to_case(Case::Snake),
654            "xml_http_request"
655        );
656        assert_eq!(
657            "XMLHttpRequest"
658                .from_case(Case::Pascal)
659                .to_case(Case::Snake),
660            "xml_http_request"
661        );
662    }
663
664    #[test]
665    fn leading_tailing_double_delimiters() {
666        let words = ["first", "second"];
667        let delimited_cases = &[
668            Case::Snake,
669            Case::Kebab,
670            Case::Lower,
671            Case::Custom {
672                boundaries: &[Boundary::Custom {
673                    condition: |s| *s.get(0).unwrap() == ".",
674                    start: 0,
675                    len: 1,
676                }],
677                pattern: Pattern::Lowercase,
678                delimiter: ".",
679            },
680        ];
681
682        for &case in delimited_cases {
683            let delim = case.delimiter();
684            let double = format!("{delim}{delim}");
685
686            let identifiers = [
687                format!("{delim}{}", words.join(delim)),
688                format!("{}{delim}", words.join(delim)),
689                format!("{delim}{}{delim}", words.join(delim)),
690                format!("{}", words.join(&double)),
691                format!("{delim}{}", words.join(&double)),
692                format!("{}{delim}", words.join(&double)),
693                format!("{delim}{}{delim}", words.join(&double)),
694            ];
695
696            for identifier in identifiers {
697                assert_eq!(identifier.to_case(case), identifier);
698                assert_eq!(identifier.from_case(case).to_case(case), identifier);
699            }
700        }
701    }
702
703    #[test]
704    fn early_word_boundaries() {
705        assert_eq!(
706            "aBagel".from_case(Case::Camel).to_case(Case::Snake),
707            "a_bagel"
708        );
709    }
710
711    #[test]
712    fn late_word_boundaries() {
713        assert_eq!(
714            "teamA".from_case(Case::Camel).to_case(Case::Snake),
715            "team_a"
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_abcAbc ABCAbc".to_case(Case::Snake),
733            "abc_abc_abc_abc_abc_abc"
734        );
735        assert_eq!("8a8A8".to_case(Case::Snake), "8_a_8_a_8");
736    }
737
738    #[test]
739    fn remove_boundaries() {
740        assert_eq!(
741            "M02S05BinaryTrees.pdf"
742                .from_case(Case::Pascal)
743                .remove_boundaries(&[Boundary::UpperDigit])
744                .to_case(Case::Snake),
745            "m02_s05_binary_trees.pdf"
746        );
747    }
748
749    #[test]
750    fn with_boundaries() {
751        assert_eq!(
752            "my_dumbFileName"
753                .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
754                .to_case(Case::Kebab),
755            "my-dumb-file-name"
756        );
757    }
758
759    // From issue https://github.com/rutrum/convert-case/issues/4
760    // From issue https://github.com/rutrum/convert-case/issues/8
761    #[test]
762    fn unicode_words() {
763        let strings = &["ПЕРСПЕКТИВА24", "música moderna"];
764        for s in strings {
765            for &case in Case::all_cases() {
766                assert!(!s.to_case(case).is_empty());
767            }
768            for &from in Case::all_cases() {
769                for &to in Case::all_cases() {
770                    assert!(!s.from_case(from).to_case(to).is_empty());
771                }
772            }
773        }
774    }
775
776    // idea for asserting the associated boundaries are correct
777    #[test]
778    fn appropriate_associated_boundaries() {
779        let word_groups = &[
780            vec!["my", "var", "name"],
781            vec!["MY", "var", "Name"],
782            vec!["another", "vAR"],
783            vec!["XML", "HTTP", "Request"],
784        ];
785
786        for words in word_groups {
787            for case in Case::all_cases() {
788                if case == &Case::Flat || case == &Case::UpperFlat {
789                    continue;
790                }
791                assert_eq!(
792                    case.pattern().mutate(&split(
793                        &case.pattern().mutate(words).join(case.delimiter()),
794                        case.boundaries()
795                    )),
796                    case.pattern().mutate(words),
797                    "Test boundaries on Case::{:?} with {:?}",
798                    case,
799                    words,
800                );
801            }
802        }
803    }
804}