convert_case/
lib.rs

1//! Converts to and from various cases.
2//!
3//! # Command Line Utility `ccase`
4//!
5//! This library was developed for the purposes of a command line utility for converting
6//! the case of strings and filenames.  You can check out
7//! [`ccase` on Github](https://github.com/rutrum/ccase).
8//!
9//! # Rust Library
10//!
11//! Provides a [`Case`] enum which defines a variety of cases to convert into.
12//! Strings have implemented the [`Casing`] trait, which adds methods for
13//! case conversion.
14//!
15//! You can convert strings into a case using the [`to_case`](Casing::to_case) method.
16//! ```
17//! use convert_case::{Case, Casing};
18//!
19//! assert_eq!("Ronnie James Dio", "ronnie james dio".to_case(Case::Title));
20//! assert_eq!("ronnieJamesDio", "Ronnie_James_dio".to_case(Case::Camel));
21//! assert_eq!("Ronnie-James-Dio", "RONNIE_JAMES_DIO".to_case(Case::Train));
22//! ```
23//!
24//! By default, `to_case` will split along a set of default word boundaries, that is
25//! * underscores `_`,
26//! * hyphens `-`,
27//! * spaces ` `,
28//! * changes in capitalization from lowercase to uppercase `aA`,
29//! * adjacent digits and letters `a1`, `1a`, `A1`, `1A`,
30//! * and acroynms `AAa` (as in `HTTPRequest`).
31//!
32//! For more precision, the `from_case` method splits based on the word boundaries
33//! of a particular case.  For example, splitting from snake case will only use
34//! underscores as word boundaries.
35//! ```
36//! # use convert_case::{Case, Casing};
37//! assert_eq!(
38//!     "2020 04 16 My Cat Cali",
39//!     "2020-04-16_my_cat_cali".to_case(Case::Title)
40//! );
41//! assert_eq!(
42//!     "2020-04-16 My Cat Cali",
43//!     "2020-04-16_my_cat_cali".from_case(Case::Snake).to_case(Case::Title)
44//! );
45//! ```
46//!
47//! This library can detect acronyms in camel-like strings.  It also ignores any leading,
48//! trailing, or duplicate delimiters.
49//! ```
50//! # use convert_case::{Case, Casing};
51//! assert_eq!("io_stream", "IOStream".to_case(Case::Snake));
52//! assert_eq!("my_json_parser", "myJSONParser".to_case(Case::Snake));
53//!
54//! assert_eq!("weird_var_name", "__weird--var _name-".to_case(Case::Snake));
55//! ```
56//!
57//! It also works non-ascii characters.  However, no inferences on the language itself is made.
58//! For instance, the digraph `ij` in Dutch will not be capitalized, because it is represented
59//! as two distinct Unicode characters.  However, `æ` would be capitalized.  Accuracy with unicode
60//! characters is done using the `unicode-segmentation` crate, the sole dependency of this crate.
61//! ```
62//! # use convert_case::{Case, Casing};
63//! assert_eq!("granat-äpfel", "GranatÄpfel".to_case(Case::Kebab));
64//! assert_eq!("Перспектива 24", "ПЕРСПЕКТИВА24".to_case(Case::Title));
65//!
66//! // The example from str::to_lowercase documentation
67//! let odysseus = "ὈΔΥΣΣΕΎΣ";
68//! assert_eq!("ὀδυσσεύς", odysseus.to_case(Case::Lower));
69//! ```
70//!
71//! By default, characters followed by digits and vice-versa are
72//! considered word boundaries.  In addition, any special ASCII characters (besides `_` and `-`)
73//! are ignored.
74//! ```
75//! # use convert_case::{Case, Casing};
76//! assert_eq!("e_5150", "E5150".to_case(Case::Snake));
77//! assert_eq!("10,000_days", "10,000Days".to_case(Case::Snake));
78//! assert_eq!("HELLO, WORLD!", "Hello, world!".to_case(Case::Upper));
79//! assert_eq!("One\ntwo\nthree", "ONE\nTWO\nTHREE".to_case(Case::Title));
80//! ```
81//!
82//! You can also test what case a string is in.
83//! ```
84//! # use convert_case::{Case, Casing};
85//! assert!( "css-class-name".is_case(Case::Kebab));
86//! assert!(!"css-class-name".is_case(Case::Snake));
87//! assert!(!"UPPER_CASE_VAR".is_case(Case::Snake));
88//! ```
89//!
90//! # Note on Accuracy
91//!
92//! The `Casing` methods `from_case` and `to_case` do not fail.  Conversion to a case will always
93//! succeed.  However, the results can still be unexpected.  Failure to detect any word boundaries
94//! for a particular case means the entire string will be considered a single word.
95//! ```
96//! use convert_case::{Case, Casing};
97//!
98//! // Mistakenly parsing using Case::Snake
99//! assert_eq!("My-kebab-var", "my-kebab-var".from_case(Case::Snake).to_case(Case::Title));
100//!
101//! // Converts using an unexpected method
102//! assert_eq!("my_kebab_like_variable", "myKebab-like-variable".to_case(Case::Snake));
103//! ```
104//!
105//! # Boundary Specificity
106//!
107//! It can be difficult to determine how to split a string into words.  That is why this case
108//! provides the [`from_case`](Casing::from_case) functionality, but sometimes that isn't enough
109//! to meet a specific use case.
110//!
111//! Say an identifier has the word `2D`, such as `scale2D`.  No exclusive usage of `from_case` will
112//! be enough to solve the problem.  In this case we can further specify which boundaries to split
113//! the string on.  `convert_case` provides some patterns for achieving this specificity.
114//! We can specify what boundaries we want to split on using instances of the [`Boundary`] struct.
115//! ```
116//! use convert_case::{Boundary, Case, Casing};
117//!
118//! // Not quite what we want
119//! assert_eq!(
120//!     "scale_2_d",
121//!     "scale2D"
122//!         .from_case(Case::Camel)
123//!         .to_case(Case::Snake)
124//! );
125//!
126//! // Remove boundary from Case::Camel
127//! assert_eq!(
128//!     "scale_2d",
129//!     "scale2D"
130//!         .from_case(Case::Camel)
131//!         .without_boundaries(&[Boundary::DIGIT_UPPER, Boundary::DIGIT_LOWER])
132//!         .to_case(Case::Snake)
133//! );
134//!
135//! // Write boundaries explicitly
136//! assert_eq!(
137//!     "scale_2d",
138//!     "scale2D"
139//!         .with_boundaries(&[Boundary::LOWER_DIGIT])
140//!         .to_case(Case::Snake)
141//! );
142//! ```
143//!
144//! The `Casing` trait provides initial methods, but any subsequent methods that do not resolve
145//! the conversion return a [`StateConverter`] struct.  It contains similar methods as `Casing`.
146//!
147//! ## Custom Boundaries
148//!
149//! `convert_case` provides a number of constants for boundaries associated with common cases.
150//! But you can create your own boundary to split on other criteria.  For simple, delimiter
151//! based splits, use [`Boundary::from_delim`].
152//!
153//! ```
154//! # use convert_case::{Boundary, Case, Casing};
155//! assert_eq!(
156//!     "Coolers Revenge",
157//!     "coolers.revenge"
158//!         .with_boundaries(&[Boundary::from_delim(".")])
159//!         .to_case(Case::Title)
160//! )
161//! ```
162//!
163//! For more complex boundaries, such as splitting based on the first character being a certain
164//! symbol and the second is lowercase, you can instantiate a boundary directly.
165//!
166//! ```
167//! # use convert_case::{Boundary, Case, Casing};
168//! let at_then_letter = Boundary {
169//!     name: "AtLetter",
170//!     condition: |s, _| {
171//!         s.get(0).map(|c| *c == "@") == Some(true)
172//!             && s.get(1).map(|c| *c == c.to_lowercase()) == Some(true)
173//!     },
174//!     arg: None,
175//!     start: 1,
176//!     len: 0,
177//! };
178//! assert_eq!(
179//!     "Name@ Domain",
180//!     "name@domain"
181//!         .with_boundaries(&[at_then_letter])
182//!         .to_case(Case::Title)
183//! )
184//! ```
185//!
186//! To learn more about building a boundary from scratch, read the [`Boundary`] struct.
187//!
188//! # Custom Case
189//!
190//! Case has a special variant [`Case::Custom`] that exposes the three components necessary
191//! for case conversion.  This allows you to define a custom case that behaves appropriately
192//! in the `.to_case` and `.from_case` methods.
193//!
194//! A common example might be a "dot case" that has lowercase letters and is delimited by
195//! periods.  We could define this as follows.
196//! ```
197//! use convert_case::{Case, Casing, pattern, Boundary};
198//!
199//! let dot_case = Case::Custom {
200//!     boundaries: &[Boundary::from_delim(".")],
201//!     pattern: pattern::lowercase,
202//!     delim: ".",
203//! };
204//!
205//! assert_eq!(
206//!     "dot.case.var",
207//!     "Dot case var".to_case(dot_case)
208//! )
209//! ```
210//! And because we defined boundary conditions, this means `.from_case` should also behave as expected.
211//! ```
212//! # use convert_case::{Case, Casing, pattern, Boundary};
213//! # let dot_case = Case::Custom {
214//! #     boundaries: &[Boundary::from_delim(".")],
215//! #     pattern: pattern::lowercase,
216//! #     delim: ".",
217//! # };
218//! assert_eq!(
219//!     "dotCaseVar",
220//!     "dot.case.var".from_case(dot_case).to_case(Case::Camel)
221//! )
222//! ```
223//!
224//! # Converter Struct
225//!
226//! Case conversion takes place in two parts.  The first splits an identifier into a series of words,
227//! and the second joins the words back together.  Each of these are steps are defined using the
228//! `.from_case` and `.to_case` methods respectively.
229//!
230//! [`Converter`] is a struct that encapsulates the boundaries used for splitting and the pattern
231//! and delimiter for mutating and joining.  The [`convert`](Converter::convert) method will
232//! apply the boundaries, pattern, and delimiter appropriately.  This lets you define the
233//! parameters for case conversion upfront.
234//! ```
235//! use convert_case::{Converter, pattern};
236//!
237//! let conv = Converter::new()
238//!     .set_pattern(pattern::camel)
239//!     .set_delim("_");
240//!
241//! assert_eq!(
242//!     "my_Special_Case",
243//!     conv.convert("My Special Case")
244//! )
245//! ```
246//! For more details on how strings are converted, see the docs for [`Converter`].
247//!
248//! # Random Feature
249//!
250//! This feature adds two additional cases: [`Case::Random`] and [`Case::PseudoRandom`].
251//! The `random` feature depends on the [`rand`](https://docs.rs/rand) crate.
252//!
253//! You can enable this feature by including the following in your `Cargo.toml`.
254//! ```{toml}
255//! [dependencies]
256//! convert_case = { version = "^0.8.0", features = ["random"] }
257//! ```
258
259#![cfg_attr(not(test), no_std)]
260extern crate alloc;
261
262use alloc::string::{String, ToString};
263
264mod boundary;
265mod case;
266mod converter;
267
268pub mod pattern;
269pub use boundary::{split, Boundary};
270pub use case::Case;
271pub use converter::Converter;
272
273/// Describes items that can be converted into a case.  This trait is used
274/// in conjunction with the [`StateConverter`] struct which is returned from a couple
275/// methods on `Casing`.
276pub trait Casing<T: AsRef<str>> {
277    /// Convert the string into the given case.  It will reference `self` and create a new
278    /// `String` with the same pattern and delimeter as `case`.  It will split on boundaries
279    /// defined at [`Boundary::defaults()`].
280    /// ```
281    /// use convert_case::{Case, Casing};
282    ///
283    /// assert_eq!(
284    ///     "tetronimo-piece-border",
285    ///     "Tetronimo piece border".to_case(Case::Kebab)
286    /// );
287    /// ```
288    fn to_case(&self, case: Case) -> String;
289
290    /// Start the case conversion by storing the boundaries associated with the given case.
291    /// ```
292    /// use convert_case::{Case, Casing};
293    ///
294    /// assert_eq!(
295    ///     "2020-08-10_dannie_birthday",
296    ///     "2020-08-10 Dannie Birthday"
297    ///         .from_case(Case::Title)
298    ///         .to_case(Case::Snake)
299    /// );
300    /// ```
301    #[allow(clippy::wrong_self_convention)]
302    fn from_case(&self, case: Case) -> StateConverter<T>;
303
304    /// Creates a `StateConverter` struct initialized with the boundaries
305    /// provided.
306    /// ```
307    /// use convert_case::{Boundary, Case, Casing};
308    ///
309    /// assert_eq!(
310    ///     "e1_m1_hangar",
311    ///     "E1M1 Hangar"
312    ///         .with_boundaries(&[Boundary::DIGIT_UPPER, Boundary::SPACE])
313    ///         .to_case(Case::Snake)
314    /// );
315    /// ```
316    fn with_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    ///         .without_boundaries(&Boundary::digits())
327    ///         .to_case(Case::Snake)
328    /// );
329    /// ```
330    fn without_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
347where
348    T: ToString,
349{
350    fn to_case(&self, case: Case) -> String {
351        StateConverter::new(self).to_case(case)
352    }
353
354    fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
355        StateConverter::new(self).with_boundaries(bs)
356    }
357
358    fn without_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
359        StateConverter::new(self).without_boundaries(bs)
360    }
361
362    fn from_case(&self, case: Case) -> StateConverter<T> {
363        StateConverter::new(self).from_case(case)
364    }
365
366    fn is_case(&self, case: Case) -> bool {
367        // TODO: rewrite
368        //&self.to_case(case) == self
369        self.to_case(case) == self.to_string()
370    }
371}
372
373/// Holds information about parsing before converting into a case.
374///
375/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
376/// `Casing`.  For a more fine grained approach to case conversion, consider using the [`Converter`]
377/// struct.
378/// ```
379/// use convert_case::{Case, Casing};
380///
381/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
382/// assert_eq!("Ninety-nine Problems", title);
383/// ```
384pub struct StateConverter<'a, T: AsRef<str>> {
385    s: &'a T,
386    conv: Converter,
387}
388
389impl<'a, T: AsRef<str>> StateConverter<'a, T> {
390    /// Only called by Casing function to_case()
391    fn new(s: &'a T) -> Self {
392        Self {
393            s,
394            conv: Converter::new(),
395        }
396    }
397
398    /// Uses the boundaries associated with `case` for word segmentation.  This
399    /// will overwrite any boundary information initialized before.  This method is
400    /// likely not useful, but provided anyway.
401    /// ```
402    /// use convert_case::{Case, Casing};
403    ///
404    /// let name = "Chuck Schuldiner"
405    ///     .from_case(Case::Snake) // from Casing trait
406    ///     .from_case(Case::Title) // from StateConverter, overwrites previous
407    ///     .to_case(Case::Kebab);
408    /// assert_eq!("chuck-schuldiner", name);
409    /// ```
410    pub fn from_case(self, case: Case) -> Self {
411        Self {
412            conv: self.conv.from_case(case),
413            ..self
414        }
415    }
416
417    /// Overwrites boundaries for word segmentation with those provided.  This will overwrite
418    /// any boundary information initialized before.  This method is likely not useful, but
419    /// provided anyway.
420    /// ```
421    /// use convert_case::{Boundary, Case, Casing};
422    ///
423    /// let song = "theHumbling river-puscifer"
424    ///     .from_case(Case::Kebab) // from Casing trait
425    ///     .with_boundaries(&[Boundary::SPACE, Boundary::LOWER_UPPER]) // overwrites `from_case`
426    ///     .to_case(Case::Pascal);
427    /// assert_eq!("TheHumblingRiver-puscifer", song);  // doesn't split on hyphen `-`
428    /// ```
429    pub fn with_boundaries(self, bs: &[Boundary]) -> Self {
430        Self {
431            s: self.s,
432            conv: self.conv.set_boundaries(bs),
433        }
434    }
435
436    /// Removes any boundaries that were already initialized.  This is particularly useful when a
437    /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
438    /// some.
439    /// ```
440    /// use convert_case::{Boundary, Case, Casing};
441    ///
442    /// assert_eq!(
443    ///     "2d_transformation",
444    ///     "2dTransformation"
445    ///         .from_case(Case::Camel)
446    ///         .without_boundaries(&Boundary::digits())
447    ///         .to_case(Case::Snake)
448    /// );
449    /// ```
450    pub fn without_boundaries(self, bs: &[Boundary]) -> Self {
451        Self {
452            s: self.s,
453            conv: self.conv.remove_boundaries(bs),
454        }
455    }
456
457    /// Consumes the `StateConverter` and returns the converted string.
458    /// ```
459    /// use convert_case::{Boundary, Case, Casing};
460    ///
461    /// assert_eq!(
462    ///     "ice-cream social",
463    ///     "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower)
464    /// );
465    /// ```
466    pub fn to_case(self, case: Case) -> String {
467        self.conv.to_case(case).convert(self.s)
468    }
469}
470
471#[cfg(test)]
472mod test {
473    use super::*;
474
475    use alloc::vec;
476    use alloc::vec::Vec;
477
478    fn possible_cases(s: &str) -> Vec<Case> {
479        Case::deterministic_cases()
480            .iter()
481            .filter(|&case| s.from_case(*case).to_case(*case) == s)
482            .map(|c| *c)
483            .collect()
484    }
485
486    #[test]
487    fn lossless_against_lossless() {
488        let examples = vec![
489            (Case::Lower, "my variable 22 name"),
490            (Case::Upper, "MY VARIABLE 22 NAME"),
491            (Case::Title, "My Variable 22 Name"),
492            (Case::Sentence, "My variable 22 name"),
493            (Case::Camel, "myVariable22Name"),
494            (Case::Pascal, "MyVariable22Name"),
495            (Case::Snake, "my_variable_22_name"),
496            (Case::UpperSnake, "MY_VARIABLE_22_NAME"),
497            (Case::Kebab, "my-variable-22-name"),
498            (Case::Cobol, "MY-VARIABLE-22-NAME"),
499            (Case::Toggle, "mY vARIABLE 22 nAME"),
500            (Case::Train, "My-Variable-22-Name"),
501            (Case::Alternating, "mY vArIaBlE 22 nAmE"),
502        ];
503
504        for (case_a, str_a) in &examples {
505            for (case_b, str_b) in &examples {
506                assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
507            }
508        }
509    }
510
511    #[test]
512    fn obvious_default_parsing() {
513        let examples = vec![
514            "SuperMario64Game",
515            "super-mario64-game",
516            "superMario64 game",
517            "Super Mario 64_game",
518            "SUPERMario 64-game",
519            "super_mario-64 game",
520        ];
521
522        for example in examples {
523            assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
524        }
525    }
526
527    #[test]
528    fn multiline_strings() {
529        assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
530    }
531
532    #[test]
533    fn camel_case_acroynms() {
534        assert_eq!(
535            "xml_http_request",
536            "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
537        );
538        assert_eq!(
539            "xml_http_request",
540            "XMLHttpRequest"
541                .from_case(Case::UpperCamel)
542                .to_case(Case::Snake)
543        );
544        assert_eq!(
545            "xml_http_request",
546            "XMLHttpRequest"
547                .from_case(Case::Pascal)
548                .to_case(Case::Snake)
549        );
550    }
551
552    #[test]
553    fn leading_tailing_delimeters() {
554        assert_eq!(
555            "leading_underscore",
556            "_leading_underscore"
557                .from_case(Case::Snake)
558                .to_case(Case::Snake)
559        );
560        assert_eq!(
561            "tailing_underscore",
562            "tailing_underscore_"
563                .from_case(Case::Snake)
564                .to_case(Case::Snake)
565        );
566        assert_eq!(
567            "leading_hyphen",
568            "-leading-hyphen"
569                .from_case(Case::Kebab)
570                .to_case(Case::Snake)
571        );
572        assert_eq!(
573            "tailing_hyphen",
574            "tailing-hyphen-"
575                .from_case(Case::Kebab)
576                .to_case(Case::Snake)
577        );
578    }
579
580    #[test]
581    fn double_delimeters() {
582        assert_eq!(
583            "many_underscores",
584            "many___underscores"
585                .from_case(Case::Snake)
586                .to_case(Case::Snake)
587        );
588        assert_eq!(
589            "many-underscores",
590            "many---underscores"
591                .from_case(Case::Kebab)
592                .to_case(Case::Kebab)
593        );
594    }
595
596    #[test]
597    fn early_word_boundaries() {
598        assert_eq!(
599            "a_bagel",
600            "aBagel".from_case(Case::Camel).to_case(Case::Snake)
601        );
602    }
603
604    #[test]
605    fn late_word_boundaries() {
606        assert_eq!(
607            "team_a",
608            "teamA".from_case(Case::Camel).to_case(Case::Snake)
609        );
610    }
611
612    #[test]
613    fn empty_string() {
614        for (case_a, case_b) in Case::all_cases()
615            .into_iter()
616            .zip(Case::all_cases().into_iter())
617        {
618            assert_eq!("", "".from_case(*case_a).to_case(*case_b));
619        }
620    }
621
622    #[test]
623    fn owned_string() {
624        assert_eq!(
625            "test_variable",
626            String::from("TestVariable").to_case(Case::Snake)
627        )
628    }
629
630    #[test]
631    fn default_all_boundaries() {
632        assert_eq!(
633            "abc_abc_abc_abc_abc_abc",
634            "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
635        );
636    }
637
638    #[test]
639    fn alternating_ignore_symbols() {
640        assert_eq!("tHaT's", "that's".to_case(Case::Alternating));
641    }
642
643    #[test]
644    fn string_is_snake() {
645        assert!("im_snake_case".is_case(Case::Snake));
646        assert!(!"im_NOTsnake_case".is_case(Case::Snake));
647    }
648
649    #[test]
650    fn string_is_kebab() {
651        assert!("im-kebab-case".is_case(Case::Kebab));
652        assert!(!"im_not_kebab".is_case(Case::Kebab));
653    }
654
655    #[test]
656    fn remove_boundaries() {
657        assert_eq!(
658            "m02_s05_binary_trees.pdf",
659            "M02S05BinaryTrees.pdf"
660                .from_case(Case::Pascal)
661                .without_boundaries(&[Boundary::UPPER_DIGIT])
662                .to_case(Case::Snake)
663        );
664    }
665
666    #[test]
667    fn with_boundaries() {
668        assert_eq!(
669            "my-dumb-file-name",
670            "my_dumbFileName"
671                .with_boundaries(&[Boundary::UNDERSCORE, Boundary::LOWER_UPPER])
672                .to_case(Case::Kebab)
673        );
674    }
675
676    #[cfg(feature = "random")]
677    #[test]
678    fn random_case_boundaries() {
679        for &random_case in Case::random_cases() {
680            assert_eq!(
681                "split_by_spaces",
682                "Split By Spaces"
683                    .from_case(random_case)
684                    .to_case(Case::Snake)
685            );
686        }
687    }
688
689    #[test]
690    fn multiple_from_case() {
691        assert_eq!(
692            "longtime_nosee",
693            "LongTime NoSee"
694                .from_case(Case::Camel)
695                .from_case(Case::Title)
696                .to_case(Case::Snake),
697        )
698    }
699
700    use std::collections::HashSet;
701    use std::iter::FromIterator;
702
703    #[test]
704    fn detect_many_cases() {
705        let lower_cases_vec = possible_cases(&"asef");
706        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
707        let mut actual = HashSet::new();
708        actual.insert(Case::Lower);
709        actual.insert(Case::Camel);
710        actual.insert(Case::Snake);
711        actual.insert(Case::Kebab);
712        actual.insert(Case::Flat);
713        assert_eq!(lower_cases_set, actual);
714
715        let lower_cases_vec = possible_cases(&"asefCase");
716        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
717        let mut actual = HashSet::new();
718        actual.insert(Case::Camel);
719        assert_eq!(lower_cases_set, actual);
720    }
721
722    #[test]
723    fn detect_each_case() {
724        let s = "My String Identifier".to_string();
725        for &case in Case::deterministic_cases() {
726            let new_s = s.from_case(case).to_case(case);
727            let possible = possible_cases(&new_s);
728            assert!(possible.iter().any(|c| c == &case));
729        }
730    }
731
732    // From issue https://github.com/rutrum/convert-case/issues/8
733    #[test]
734    fn accent_mark() {
735        let s = "música moderna".to_string();
736        assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
737    }
738
739    // From issue https://github.com/rutrum/convert-case/issues/4
740    #[test]
741    fn russian() {
742        let s = "ПЕРСПЕКТИВА24".to_string();
743        let _n = s.to_case(Case::Title);
744    }
745}