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