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}