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.Case.html) enum which defines a variety of cases to convert into.
12//! Strings have implemented the [`Casing`](trait.Casing.html) 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//! * space characters ` `,
26//! * underscores `_`,
27//! * hyphens `-`,
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 accuracy, 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//! Case conversion can detect acronyms for 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 the [`Boundary` struct](Boundary).
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 Cases
189//!
190//! Because `Case` is an enum, you can't create your own variant for your use case.  However
191//! the parameters for case conversion have been encapsulated into the [`Converter`] struct
192//! which can be used for specific use cases.
193//!
194//! Suppose you wanted to format a word like camel case, where the first word is lower case and the
195//! rest are capitalized.  But you want to include a delimeter like underscore.  This case isn't
196//! available as a `Case` variant, but you can create it by constructing the parameters of the
197//! `Converter`.
198//! ```
199//! use convert_case::{Case, Casing, Converter, Pattern};
200//!
201//! let conv = Converter::new()
202//!     .set_pattern(Pattern::Camel)
203//!     .set_delim("_");
204//!
205//! assert_eq!(
206//!     "my_Special_Case",
207//!     conv.convert("My Special Case")
208//! )
209//! ```
210//! Just as with the `Casing` trait, you can also manually set the boundaries strings are split
211//! on.  You can use any of the [`Pattern`] variants available.  This even includes [`Pattern::Sentence`]
212//! which isn't used in any `Case` variant.  You can also set no pattern at all, which will
213//! maintain the casing of each letter in the input string.  You can also, of course, set any string as your
214//! delimeter.
215//!
216//! For more details on how strings are converted, see the docs for [`Converter`].
217//!
218//! # Random Feature
219//!
220//! To ensure this library had zero dependencies, randomness was moved to the _random_ feature,
221//! which requires the `rand` crate. You can enable this feature by including the
222//! following in your `Cargo.toml`.
223//! ```{toml}
224//! [dependencies]
225//! convert_case = { version = "^0.3.0", features = ["random"] }
226//! ```
227//! This will add two additional cases: Random and PseudoRandom.  You can read about their
228//! construction in the [Case enum](enum.Case.html).
229
230mod boundary;
231mod case;
232mod converter;
233mod pattern;
234
235pub use boundary::{split, Boundary};
236pub use case::Case;
237pub use converter::Converter;
238pub use pattern::Pattern;
239
240/// Describes items that can be converted into a case.  This trait is used
241/// in conjunction with the [`StateConverter`] struct which is returned from a couple
242/// methods on `Casing`.
243pub trait Casing<T: AsRef<str>> {
244    /// Convert the string into the given case.  It will reference `self` and create a new
245    /// `String` with the same pattern and delimeter as `case`.  It will split on boundaries
246    /// defined at [`Boundary::defaults()`].
247    /// ```
248    /// use convert_case::{Case, Casing};
249    ///
250    /// assert_eq!(
251    ///     "tetronimo-piece-border",
252    ///     "Tetronimo piece border".to_case(Case::Kebab)
253    /// );
254    /// ```
255    fn to_case(&self, case: Case) -> String;
256
257    /// Start the case conversion by storing the boundaries associated with the given case.
258    /// ```
259    /// use convert_case::{Case, Casing};
260    ///
261    /// assert_eq!(
262    ///     "2020-08-10_dannie_birthday",
263    ///     "2020-08-10 Dannie Birthday"
264    ///         .from_case(Case::Title)
265    ///         .to_case(Case::Snake)
266    /// );
267    /// ```
268    #[allow(clippy::wrong_self_convention)]
269    fn from_case(&self, case: Case) -> StateConverter<T>;
270
271    /// Creates a `StateConverter` struct initialized with the boundaries
272    /// provided.
273    /// ```
274    /// use convert_case::{Boundary, Case, Casing};
275    ///
276    /// assert_eq!(
277    ///     "e1_m1_hangar",
278    ///     "E1M1 Hangar"
279    ///         .with_boundaries(&[Boundary::DIGIT_UPPER, Boundary::SPACE])
280    ///         .to_case(Case::Snake)
281    /// );
282    /// ```
283    fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
284
285    /// Creates a `StateConverter` struct initialized without the boundaries
286    /// provided.
287    /// ```
288    /// use convert_case::{Boundary, Case, Casing};
289    ///
290    /// assert_eq!(
291    ///     "2d_transformation",
292    ///     "2dTransformation"
293    ///         .without_boundaries(&Boundary::digits())
294    ///         .to_case(Case::Snake)
295    /// );
296    /// ```
297    fn without_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
298
299    /// Determines if `self` is of the given case.  This is done simply by applying
300    /// the conversion and seeing if the result is the same.
301    /// ```
302    /// use convert_case::{Case, Casing};
303    ///
304    /// assert!( "kebab-case-string".is_case(Case::Kebab));
305    /// assert!( "Train-Case-String".is_case(Case::Train));
306    ///
307    /// assert!(!"kebab-case-string".is_case(Case::Snake));
308    /// assert!(!"kebab-case-string".is_case(Case::Train));
309    /// ```
310    fn is_case(&self, case: Case) -> bool;
311}
312
313impl<T: AsRef<str>> Casing<T> for T
314where
315    T: ToString,
316{
317    fn to_case(&self, case: Case) -> String {
318        StateConverter::new(self).to_case(case)
319    }
320
321    fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
322        StateConverter::new(self).with_boundaries(bs)
323    }
324
325    fn without_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
326        StateConverter::new(self).without_boundaries(bs)
327    }
328
329    fn from_case(&self, case: Case) -> StateConverter<T> {
330        StateConverter::new_from_case(self, case)
331    }
332
333    fn is_case(&self, case: Case) -> bool {
334        // TODO: rewrite
335        //&self.to_case(case) == self
336        self.to_case(case) == self.to_string()
337    }
338}
339
340/// Holds information about parsing before converting into a case.
341///
342/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
343/// `Casing`.  For a more fine grained approach to case conversion, consider using the [`Converter`]
344/// struct.
345/// ```
346/// use convert_case::{Case, Casing};
347///
348/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
349/// assert_eq!("Ninety-nine Problems", title);
350/// ```
351pub struct StateConverter<'a, T: AsRef<str>> {
352    s: &'a T,
353    conv: Converter,
354}
355
356impl<'a, T: AsRef<str>> StateConverter<'a, T> {
357    /// Only called by Casing function to_case()
358    fn new(s: &'a T) -> Self {
359        Self {
360            s,
361            conv: Converter::new(),
362        }
363    }
364
365    /// Only called by Casing function from_case()
366    fn new_from_case(s: &'a T, case: Case) -> Self {
367        Self {
368            s,
369            conv: Converter::new().from_case(case),
370        }
371    }
372
373    /// Uses the boundaries associated with `case` for word segmentation.  This
374    /// will overwrite any boundary information initialized before.  This method is
375    /// likely not useful, but provided anyway.
376    /// ```
377    /// use convert_case::{Case, Casing};
378    ///
379    /// let name = "Chuck Schuldiner"
380    ///     .from_case(Case::Snake) // from Casing trait
381    ///     .from_case(Case::Title) // from StateConverter, overwrites previous
382    ///     .to_case(Case::Kebab);
383    /// assert_eq!("chuck-schuldiner", name);
384    /// ```
385    pub fn from_case(self, case: Case) -> Self {
386        Self {
387            conv: self.conv.from_case(case),
388            ..self
389        }
390    }
391
392    /// Overwrites boundaries for word segmentation with those provided.  This will overwrite
393    /// any boundary information initialized before.  This method is likely not useful, but
394    /// provided anyway.
395    /// ```
396    /// use convert_case::{Boundary, Case, Casing};
397    ///
398    /// let song = "theHumbling river-puscifer"
399    ///     .from_case(Case::Kebab) // from Casing trait
400    ///     .with_boundaries(&[Boundary::SPACE, Boundary::LOWER_UPPER]) // overwrites `from_case`
401    ///     .to_case(Case::Pascal);
402    /// assert_eq!("TheHumblingRiver-puscifer", song);  // doesn't split on hyphen `-`
403    /// ```
404    pub fn with_boundaries(self, bs: &[Boundary]) -> Self {
405        Self {
406            s: self.s,
407            conv: self.conv.set_boundaries(bs),
408        }
409    }
410
411    /// Removes any boundaries that were already initialized.  This is particularly useful when a
412    /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
413    /// some.
414    /// ```
415    /// use convert_case::{Boundary, Case, Casing};
416    ///
417    /// assert_eq!(
418    ///     "2d_transformation",
419    ///     "2dTransformation"
420    ///         .from_case(Case::Camel)
421    ///         .without_boundaries(&Boundary::digits())
422    ///         .to_case(Case::Snake)
423    /// );
424    /// ```
425    pub fn without_boundaries(self, bs: &[Boundary]) -> Self {
426        Self {
427            s: self.s,
428            conv: self.conv.remove_boundaries(bs),
429        }
430    }
431
432    /// Consumes the `StateConverter` and returns the converted string.
433    /// ```
434    /// use convert_case::{Boundary, Case, Casing};
435    ///
436    /// assert_eq!(
437    ///     "ice-cream social",
438    ///     "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower)
439    /// );
440    /// ```
441    pub fn to_case(self, case: Case) -> String {
442        self.conv.to_case(case).convert(self.s)
443    }
444}
445
446#[cfg(test)]
447mod test {
448    use super::*;
449    use strum::IntoEnumIterator;
450
451    fn possible_cases(s: &str) -> Vec<Case> {
452        Case::deterministic_cases()
453            .into_iter()
454            .filter(|case| s.from_case(*case).to_case(*case) == s)
455            .collect()
456    }
457
458    #[test]
459    fn lossless_against_lossless() {
460        let examples = vec![
461            (Case::Lower, "my variable 22 name"),
462            (Case::Upper, "MY VARIABLE 22 NAME"),
463            (Case::Title, "My Variable 22 Name"),
464            (Case::Sentence, "My variable 22 name"),
465            (Case::Camel, "myVariable22Name"),
466            (Case::Pascal, "MyVariable22Name"),
467            (Case::Snake, "my_variable_22_name"),
468            (Case::UpperSnake, "MY_VARIABLE_22_NAME"),
469            (Case::Kebab, "my-variable-22-name"),
470            (Case::Cobol, "MY-VARIABLE-22-NAME"),
471            (Case::Toggle, "mY vARIABLE 22 nAME"),
472            (Case::Train, "My-Variable-22-Name"),
473            (Case::Alternating, "mY vArIaBlE 22 nAmE"),
474        ];
475
476        for (case_a, str_a) in examples.iter() {
477            for (case_b, str_b) in examples.iter() {
478                assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
479            }
480        }
481    }
482
483    #[test]
484    fn obvious_default_parsing() {
485        let examples = vec![
486            "SuperMario64Game",
487            "super-mario64-game",
488            "superMario64 game",
489            "Super Mario 64_game",
490            "SUPERMario 64-game",
491            "super_mario-64 game",
492        ];
493
494        for example in examples {
495            assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
496        }
497    }
498
499    #[test]
500    fn multiline_strings() {
501        assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
502    }
503
504    #[test]
505    fn camel_case_acroynms() {
506        assert_eq!(
507            "xml_http_request",
508            "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
509        );
510        assert_eq!(
511            "xml_http_request",
512            "XMLHttpRequest"
513                .from_case(Case::UpperCamel)
514                .to_case(Case::Snake)
515        );
516        assert_eq!(
517            "xml_http_request",
518            "XMLHttpRequest"
519                .from_case(Case::Pascal)
520                .to_case(Case::Snake)
521        );
522    }
523
524    #[test]
525    fn leading_tailing_delimeters() {
526        assert_eq!(
527            "leading_underscore",
528            "_leading_underscore"
529                .from_case(Case::Snake)
530                .to_case(Case::Snake)
531        );
532        assert_eq!(
533            "tailing_underscore",
534            "tailing_underscore_"
535                .from_case(Case::Snake)
536                .to_case(Case::Snake)
537        );
538        assert_eq!(
539            "leading_hyphen",
540            "-leading-hyphen"
541                .from_case(Case::Kebab)
542                .to_case(Case::Snake)
543        );
544        assert_eq!(
545            "tailing_hyphen",
546            "tailing-hyphen-"
547                .from_case(Case::Kebab)
548                .to_case(Case::Snake)
549        );
550    }
551
552    #[test]
553    fn double_delimeters() {
554        assert_eq!(
555            "many_underscores",
556            "many___underscores"
557                .from_case(Case::Snake)
558                .to_case(Case::Snake)
559        );
560        assert_eq!(
561            "many-underscores",
562            "many---underscores"
563                .from_case(Case::Kebab)
564                .to_case(Case::Kebab)
565        );
566    }
567
568    #[test]
569    fn early_word_boundaries() {
570        assert_eq!(
571            "a_bagel",
572            "aBagel".from_case(Case::Camel).to_case(Case::Snake)
573        );
574    }
575
576    #[test]
577    fn late_word_boundaries() {
578        assert_eq!(
579            "team_a",
580            "teamA".from_case(Case::Camel).to_case(Case::Snake)
581        );
582    }
583
584    #[test]
585    fn empty_string() {
586        for (case_a, case_b) in Case::iter().zip(Case::iter()) {
587            assert_eq!("", "".from_case(case_a).to_case(case_b));
588        }
589    }
590
591    #[test]
592    fn owned_string() {
593        assert_eq!(
594            "test_variable",
595            String::from("TestVariable").to_case(Case::Snake)
596        )
597    }
598
599    #[test]
600    fn default_all_boundaries() {
601        assert_eq!(
602            "abc_abc_abc_abc_abc_abc",
603            "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
604        );
605    }
606
607    #[test]
608    fn alternating_ignore_symbols() {
609        assert_eq!("tHaT's", "that's".to_case(Case::Alternating));
610    }
611
612    #[test]
613    fn string_is_snake() {
614        assert!("im_snake_case".is_case(Case::Snake));
615        assert!(!"im_NOTsnake_case".is_case(Case::Snake));
616    }
617
618    #[test]
619    fn string_is_kebab() {
620        assert!("im-kebab-case".is_case(Case::Kebab));
621        assert!(!"im_not_kebab".is_case(Case::Kebab));
622    }
623
624    #[test]
625    fn remove_boundaries() {
626        assert_eq!(
627            "m02_s05_binary_trees.pdf",
628            "M02S05BinaryTrees.pdf"
629                .from_case(Case::Pascal)
630                .without_boundaries(&[Boundary::UPPER_DIGIT])
631                .to_case(Case::Snake)
632        );
633    }
634
635    #[test]
636    fn with_boundaries() {
637        assert_eq!(
638            "my-dumb-file-name",
639            "my_dumbFileName"
640                .with_boundaries(&[Boundary::UNDERSCORE, Boundary::LOWER_UPPER])
641                .to_case(Case::Kebab)
642        );
643    }
644
645    #[cfg(feature = "random")]
646    #[test]
647    fn random_case_boundaries() {
648        for random_case in Case::random_cases() {
649            assert_eq!(
650                "split_by_spaces",
651                "Split By Spaces"
652                    .from_case(random_case)
653                    .to_case(Case::Snake)
654            );
655        }
656    }
657
658    #[test]
659    fn multiple_from_case() {
660        assert_eq!(
661            "longtime_nosee",
662            "LongTime NoSee"
663                .from_case(Case::Camel)
664                .from_case(Case::Title)
665                .to_case(Case::Snake),
666        )
667    }
668
669    use std::collections::HashSet;
670    use std::iter::FromIterator;
671
672    #[test]
673    fn detect_many_cases() {
674        let lower_cases_vec = possible_cases(&"asef");
675        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
676        let mut actual = HashSet::new();
677        actual.insert(Case::Lower);
678        actual.insert(Case::Camel);
679        actual.insert(Case::Snake);
680        actual.insert(Case::Kebab);
681        actual.insert(Case::Flat);
682        assert_eq!(lower_cases_set, actual);
683
684        let lower_cases_vec = possible_cases(&"asefCase");
685        let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
686        let mut actual = HashSet::new();
687        actual.insert(Case::Camel);
688        assert_eq!(lower_cases_set, actual);
689    }
690
691    #[test]
692    fn detect_each_case() {
693        let s = "My String Identifier".to_string();
694        for case in Case::deterministic_cases() {
695            let new_s = s.from_case(case).to_case(case);
696            let possible = possible_cases(&new_s);
697            assert!(possible.iter().any(|c| c == &case));
698        }
699    }
700
701    // From issue https://github.com/rutrum/convert-case/issues/8
702    #[test]
703    fn accent_mark() {
704        let s = "música moderna".to_string();
705        assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
706    }
707
708    // From issue https://github.com/rutrum/convert-case/issues/4
709    #[test]
710    fn russian() {
711        let s = "ПЕРСПЕКТИВА24".to_string();
712        let _n = s.to_case(Case::Title);
713    }
714}