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