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 propagates 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//! To remove empty words before joining, you can call `remove_empty` from the
145//! `Casing` trait before finishing the conversion.
146//! ```
147//! # use convert_case::{Casing, Case};
148//! assert_eq!(
149//! "_empty__first_word".remove_empty().to_case(Case::Camel),
150//! "emptyFirstWord"
151//! )
152//! ```
153//!
154//! # Customizing Behavior
155//!
156//! Case conversion takes place in three steps:
157//! 1. Splitting the identifier into a list of words
158//! 2. Mutating the letter case of graphemes within each word
159//! 3. Joining the words back into an identifier using a delimiter
160//!
161//! Those are defined by boundaries, patterns, and delimiters respectively. Graphically:
162//!
163//! ```md
164//! Identifier Identifier'
165//! | ^
166//! | boundaries | delimiter
167//! V |
168//! Words ----------> Words'
169//! patterns
170//! ```
171//!
172//! ## Patterns
173//!
174//! How to change the case of letters across a list of words is called a _pattern_.
175//! A pattern is a function that when passed a `&[&str]`, produces a
176//! `Vec<String>`. The [`Pattern`] enum encapsulates the common transformations
177//! used across all cases. Although custom functions can be supplied with the
178//! [`Custom`](Pattern::Custom) variant.
179//!
180//! ## Boundaries
181//!
182//! The condition for splitting at part of an identifier, where to perform
183//! the split, and if any characters are removed are defined by [boundaries](Boundary).
184//! By default, identifiers are split based on [`Boundary::defaults`]. This list
185//! contains word boundaries that you would likely see after creating a multi-word
186//! identifier of typical cases.
187//!
188//! Custom boundary conditions can also be created. Commonly, you might split based on some
189//! character or list of characters. The [`separator`] macro builds
190//! a boundary that splits on the presence of a string, and then removes the string
191//! while producing the list of words.
192//!
193//! You can also use [`Boundary::Custom`] to explicitly define boundary
194//! conditions. If you actually need to create a
195//! boundary condition from scratch, you should file an issue to let the author know
196//! how you used it. I'm not certain what other boundary condition would be helpful.
197//!
198//! ## Cases
199//!
200//! A case is defined by a list of boundaries, a pattern, and a _delimiter_: the string to
201//! intersperse between words before concatenation. [`Case::Custom`] is a struct enum variant with
202//! exactly those three fields. You could create your own case like so.
203//! ```
204//! use convert_case::{Case, Casing, separator, Pattern};
205//!
206//! let dot_case = Case::Custom {
207//! boundaries: &[separator!(".")],
208//! pattern: Pattern::Lowercase,
209//! delimiter: ".",
210//! };
211//!
212//! assert_eq!("AnimalFactoryFactory".to_case(dot_case), "animal.factory.factory");
213//!
214//! assert_eq!(
215//! "pd.options.mode.copy_on_write"
216//! .from_case(dot_case)
217//! .to_case(Case::Title),
218//! "Pd Options Mode Copy_on_write",
219//! )
220//! ```
221//!
222//! ## Converter
223//!
224//! Case conversion with `convert_case` allows using attributes from two cases. From
225//! the first case is how you split the identifier (the _from_ case), and
226//! from the second is how to mutate and join the words (the _to_ case.) The
227//! [`Converter`] is used to define the _conversion_ process, not a case directly.
228//!
229//! It has the same fields as case, but is exposed via a builder interface
230//! and can be used to apply a conversion on a string directly, without
231//! specifying all the parameters at the time of conversion.
232//!
233//! In the below example, we build a converter that maps the double colon
234//! delimited module path in rust into a series of file directories.
235//!
236//! ```
237//! use convert_case::{Case, Converter, separator};
238//!
239//! let modules_into_path = Converter::new()
240//! .set_boundaries(&[separator!("::")])
241//! .set_delimiter("/");
242//!
243//! assert_eq!(
244//! modules_into_path.convert("std::os::unix"),
245//! "std/os/unix",
246//! );
247//! ```
248//!
249//! # Associated Projects
250//!
251//! ## Rust library `convert_case_extras`
252//!
253//! Some extra utilities for convert_case that don't need to be in the main library.
254//! You can read more here: [`convert_case_extras`](https://docs.rs/convert_case_extras).
255//!
256//! ## stringcase.org
257//!
258//! While developing `convert_case`, the author became fascinated in the naming conventions
259//! used for cases as well as different implementations for conversion. On [stringcase.org](https://stringcase.org)
260//! is documentation of the history of naming conventions, a catalogue of case conversion tools,
261//! and a more rigorous definition of what it means to "convert the case of an identifier."
262//!
263//! ## Command Line Utility `ccase`
264//!
265//! `convert_case` was originally developed for the purposes of a command line utility
266//! for converting the case of strings and filenames. You can check out
267//! [`ccase` on Github](https://github.com/rutrum/ccase).
268#![cfg_attr(not(test), no_std)]
269extern crate alloc;
270
271use alloc::string::String;
272
273mod boundary;
274mod case;
275mod converter;
276mod pattern;
277
278pub use boundary::{split, Boundary};
279pub use case::Case;
280pub use converter::Converter;
281pub use pattern::Pattern;
282
283/// Describes items that can be converted into a case. This trait is used
284/// in conjunction with the [`StateConverter`] struct which is returned from a couple
285/// methods on `Casing`.
286pub trait Casing<T: AsRef<str>> {
287 /// Convert the string into the given case. It will reference `self` and create a new
288 /// `String` with the same pattern and delimiter as `case`. It will split on boundaries
289 /// defined at [`Boundary::defaults()`].
290 /// ```
291 /// use convert_case::{Case, Casing};
292 ///
293 /// assert_eq!(
294 /// "Tetronimo piece border".to_case(Case::Kebab),
295 /// "tetronimo-piece-border",
296 /// );
297 /// ```
298 fn to_case(&self, case: Case) -> String;
299
300 /// Start the case conversion by storing the boundaries associated with the given case.
301 /// ```
302 /// use convert_case::{Case, Casing};
303 ///
304 /// assert_eq!(
305 /// "2020-08-10 Dannie Birthday"
306 /// .from_case(Case::Title)
307 /// .to_case(Case::Snake),
308 /// "2020-08-10_dannie_birthday",
309 /// );
310 /// ```
311 #[allow(clippy::wrong_self_convention)]
312 fn from_case(&self, case: Case) -> StateConverter<'_, T>;
313
314 /// Creates a `StateConverter` struct initialized with the boundaries provided.
315 /// ```
316 /// use convert_case::{Boundary, Case, Casing};
317 ///
318 /// assert_eq!(
319 /// "E1M1 Hangar"
320 /// .set_boundaries(&[Boundary::DigitUpper, Boundary::Space])
321 /// .to_case(Case::Snake),
322 /// "e1_m1_hangar",
323 /// );
324 /// ```
325 fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T>;
326
327 /// Creates a `StateConverter` struct initialized without the boundaries
328 /// provided.
329 /// ```
330 /// use convert_case::{Boundary, Case, Casing};
331 ///
332 /// assert_eq!(
333 /// "2d_transformation",
334 /// "2dTransformation"
335 /// .remove_boundaries(&Boundary::digits())
336 /// .to_case(Case::Snake)
337 /// );
338 /// ```
339 fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T>;
340
341 /// Creates a `StateConverter` with the `RemoveEmpty` pattern prepended.
342 /// This filters out empty words before conversion, useful when splitting
343 /// produces empty words from leading, trailing, and duplicate delimiters.
344 /// ```
345 /// use convert_case::{Case, Casing};
346 ///
347 /// assert_eq!(
348 /// "--leading-delims"
349 /// .from_case(Case::Kebab)
350 /// .remove_empty()
351 /// .to_case(Case::Camel),
352 /// "leadingDelims",
353 /// );
354 /// ```
355 fn remove_empty(&self) -> StateConverter<'_, T>;
356}
357
358impl<T: AsRef<str>> Casing<T> for T {
359 fn to_case(&self, case: Case) -> String {
360 StateConverter::new(self).to_case(case)
361 }
362
363 fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T> {
364 StateConverter::new(self).set_boundaries(bs)
365 }
366
367 fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T> {
368 StateConverter::new(self).remove_boundaries(bs)
369 }
370
371 fn from_case(&self, case: Case) -> StateConverter<'_, T> {
372 StateConverter::new(self).from_case(case)
373 }
374
375 fn remove_empty(&self) -> StateConverter<'_, T> {
376 StateConverter::new(self).remove_empty()
377 }
378}
379
380/// Holds information about parsing before converting into a case.
381///
382/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
383/// `Casing`. For a more fine grained approach to case conversion, consider using the [`Converter`]
384/// struct.
385/// ```
386/// # use convert_case::{Case, Casing};
387/// assert_eq!(
388/// "By-Tor And The Snow Dog".from_case(Case::Title).to_case(Case::Snake),
389/// "by-tor_and_the_snow_dog",
390/// );
391/// ```
392pub struct StateConverter<'a, T: AsRef<str>> {
393 s: &'a T,
394 conv: Converter,
395}
396
397impl<'a, T: AsRef<str>> StateConverter<'a, T> {
398 /// Only called by Casing function to_case()
399 fn new(s: &'a T) -> Self {
400 Self {
401 s,
402 conv: Converter::new(),
403 }
404 }
405
406 /// Uses the boundaries associated with `case` for word segmentation. This
407 /// will overwrite any boundary information initialized before. This method is
408 /// likely not useful, but provided anyway.
409 /// ```
410 /// use convert_case::{Case, Casing};
411 /// assert_eq!(
412 /// "Alan Turing"
413 /// .from_case(Case::Snake) // from Casing trait
414 /// .from_case(Case::Title) // from StateConverter, overwrites previous
415 /// .to_case(Case::Kebab),
416 /// "alan-turing"
417 /// );
418 /// ```
419 pub fn from_case(self, case: Case) -> Self {
420 Self {
421 conv: self.conv.from_case(case),
422 ..self
423 }
424 }
425
426 /// Overwrites boundaries for word segmentation with those provided. This will overwrite
427 /// any boundary information initialized before. This method is likely not useful, but
428 /// provided anyway.
429 /// ```
430 /// use convert_case::{Boundary, Case, Casing};
431 /// assert_eq!(
432 /// "Vector5d Transformation"
433 /// .from_case(Case::Title) // from Casing trait
434 /// .set_boundaries(&[Boundary::Space, Boundary::LowerDigit]) // overwrites `from_case`
435 /// .to_case(Case::Kebab),
436 /// "vector-5d-transformation"
437 /// );
438 /// ```
439 pub fn set_boundaries(self, bs: &[Boundary]) -> Self {
440 Self {
441 s: self.s,
442 conv: self.conv.set_boundaries(bs),
443 }
444 }
445
446 /// Removes any boundaries that were already initialized. This is particularly useful when a
447 /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
448 /// some.
449 /// ```
450 /// use convert_case::{Boundary, Case, Casing};
451 /// assert_eq!(
452 /// "2dTransformation"
453 /// .from_case(Case::Camel)
454 /// .remove_boundaries(&Boundary::digits())
455 /// .to_case(Case::Snake),
456 /// "2d_transformation"
457 /// );
458 /// ```
459 pub fn remove_boundaries(self, bs: &[Boundary]) -> Self {
460 Self {
461 s: self.s,
462 conv: self.conv.remove_boundaries(bs),
463 }
464 }
465
466 /// Prepends the `RemoveEmpty` pattern to filter out empty words before conversion.
467 /// This is useful when splitting produces empty words from leading, trailing, and
468 /// duplicate delimiters.
469 /// ```
470 /// use convert_case::{Case, Casing};
471 /// assert_eq!(
472 /// "_leading_underscore"
473 /// .from_case(Case::Snake)
474 /// .remove_empty()
475 /// .to_case(Case::Camel),
476 /// "leadingUnderscore"
477 /// );
478 /// ```
479 pub fn remove_empty(self) -> Self {
480 Self {
481 s: self.s,
482 conv: self.conv.add_pattern(pattern::Pattern::RemoveEmpty),
483 }
484 }
485
486 /// Consumes the `StateConverter` and returns the converted string.
487 /// ```
488 /// use convert_case::{Boundary, Case, Casing};
489 /// assert_eq!(
490 /// "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower),
491 /// "ice-cream social",
492 /// );
493 /// ```
494 pub fn to_case(self, case: Case) -> String {
495 self.conv.to_case(case).convert(self.s)
496 }
497}
498
499/// The variant of `case` from a token.
500///
501/// The token associated with each variant is the variant written in snake case.
502/// To do conversion with a macro, see [`ccase`].
503#[macro_export]
504macro_rules! case {
505 (snake) => {
506 convert_case::Case::Snake
507 };
508 (constant) => {
509 convert_case::Case::Constant
510 };
511 (upper_snake) => {
512 convert_case::Case::UpperSnake
513 };
514 (ada) => {
515 convert_case::Case::Ada;
516 };
517 (kebab) => {
518 convert_case::Case::Kebab
519 };
520 (cobol) => {
521 convert_case::Case::Cobol
522 };
523 (upper_kebab) => {
524 convert_case::Case::UpperKebab
525 };
526 (train) => {
527 convert_case::Case::Train
528 };
529 (flat) => {
530 convert_case::Case::Flat
531 };
532 (upper_flat) => {
533 convert_case::Case::UpperFlat
534 };
535 (pascal) => {
536 convert_case::Case::Pascal
537 };
538 (upper_camel) => {
539 convert_case::Case::UpperCamel
540 };
541 (camel) => {
542 convert_case::Case::Camel
543 };
544 (lower) => {
545 convert_case::Case::Lower
546 };
547 (upper) => {
548 convert_case::Case::Upper
549 };
550 (title) => {
551 convert_case::Case::Title
552 };
553 (sentence) => {
554 convert_case::Case::Sentence
555 };
556}
557
558/// Convert an identifier into a case.
559///
560/// You can convert a string by writing the case name as a token.
561/// ```
562/// use convert_case::ccase;
563///
564/// assert_eq!(ccase!(snake, "myVarName"), "my_var_name");
565/// // equivalent to
566/// // "myVarName".to_case(Case::Snake)
567/// ```
568/// You can also specify a _from_ case, or the case that determines how the input
569/// string is split into words.
570/// ```
571/// use convert_case::ccase;
572///
573/// assert_eq!(ccase!(sentence -> snake, "Ice-cream sales"), "ice-cream_sales");
574/// // equivalent to
575/// // "Ice-cream sales".from_case(Case::Sentence).to_case(Case::Snake)
576/// ```
577#[macro_export]
578macro_rules! ccase {
579 ($case:ident, $e:expr) => {
580 convert_case::Converter::new()
581 .to_case(convert_case::case!($case))
582 .convert($e)
583 };
584 ($from:ident -> $to:ident, $e:expr) => {
585 convert_case::Converter::new()
586 .from_case(convert_case::case!($from))
587 .to_case(convert_case::case!($to))
588 .convert($e)
589 };
590}
591
592#[cfg(test)]
593mod test {
594 use super::*;
595
596 use alloc::vec;
597
598 #[test]
599 fn lossless_against_lossless() {
600 let examples = vec![
601 (Case::Snake, "my_variable_22_name"),
602 (Case::Constant, "MY_VARIABLE_22_NAME"),
603 (Case::Ada, "My_Variable_22_Name"),
604 (Case::Kebab, "my-variable-22-name"),
605 (Case::Cobol, "MY-VARIABLE-22-NAME"),
606 (Case::Train, "My-Variable-22-Name"),
607 (Case::Pascal, "MyVariable22Name"),
608 (Case::Camel, "myVariable22Name"),
609 (Case::Lower, "my variable 22 name"),
610 (Case::Upper, "MY VARIABLE 22 NAME"),
611 (Case::Title, "My Variable 22 Name"),
612 (Case::Sentence, "My variable 22 name"),
613 ];
614
615 for (case_a, str_a) in &examples {
616 for (case_b, str_b) in &examples {
617 assert_eq!(str_b.to_case(*case_a), *str_a);
618 assert_eq!(str_b.from_case(*case_b).to_case(*case_a), *str_a);
619 }
620 }
621 }
622
623 #[test]
624 fn obvious_default_parsing() {
625 let examples = vec![
626 "SuperMario64Game",
627 "super-mario64-game",
628 "superMario64 game",
629 "Super Mario 64_game",
630 "SUPERMario 64-game",
631 "super_mario-64 game",
632 ];
633
634 for example in examples {
635 assert_eq!(example.to_case(Case::Snake), "super_mario_64_game");
636 }
637 }
638
639 #[test]
640 fn multiline_strings() {
641 assert_eq!("one\ntwo\nthree".to_case(Case::Title), "One\ntwo\nthree");
642 }
643
644 #[test]
645 fn camel_case_acronyms() {
646 assert_eq!(
647 "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake),
648 "xml_http_request"
649 );
650 assert_eq!(
651 "XMLHttpRequest"
652 .from_case(Case::UpperCamel)
653 .to_case(Case::Snake),
654 "xml_http_request"
655 );
656 assert_eq!(
657 "XMLHttpRequest"
658 .from_case(Case::Pascal)
659 .to_case(Case::Snake),
660 "xml_http_request"
661 );
662 }
663
664 #[test]
665 fn leading_tailing_double_delimiters() {
666 let words = ["first", "second"];
667 let delimited_cases = &[
668 Case::Snake,
669 Case::Kebab,
670 Case::Lower,
671 Case::Custom {
672 boundaries: &[Boundary::Custom {
673 condition: |s| *s.get(0).unwrap() == ".",
674 start: 0,
675 len: 1,
676 }],
677 pattern: Pattern::Lowercase,
678 delimiter: ".",
679 },
680 ];
681
682 for &case in delimited_cases {
683 let delim = case.delimiter();
684 let double = format!("{delim}{delim}");
685
686 let identifiers = [
687 format!("{delim}{}", words.join(delim)),
688 format!("{}{delim}", words.join(delim)),
689 format!("{delim}{}{delim}", words.join(delim)),
690 format!("{}", words.join(&double)),
691 format!("{delim}{}", words.join(&double)),
692 format!("{}{delim}", words.join(&double)),
693 format!("{delim}{}{delim}", words.join(&double)),
694 ];
695
696 for identifier in identifiers {
697 assert_eq!(identifier.to_case(case), identifier);
698 assert_eq!(identifier.from_case(case).to_case(case), identifier);
699 }
700 }
701 }
702
703 #[test]
704 fn early_word_boundaries() {
705 assert_eq!(
706 "aBagel".from_case(Case::Camel).to_case(Case::Snake),
707 "a_bagel"
708 );
709 }
710
711 #[test]
712 fn late_word_boundaries() {
713 assert_eq!(
714 "teamA".from_case(Case::Camel).to_case(Case::Snake),
715 "team_a"
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_abcAbc ABCAbc".to_case(Case::Snake),
733 "abc_abc_abc_abc_abc_abc"
734 );
735 assert_eq!("8a8A8".to_case(Case::Snake), "8_a_8_a_8");
736 }
737
738 #[test]
739 fn remove_boundaries() {
740 assert_eq!(
741 "M02S05BinaryTrees.pdf"
742 .from_case(Case::Pascal)
743 .remove_boundaries(&[Boundary::UpperDigit])
744 .to_case(Case::Snake),
745 "m02_s05_binary_trees.pdf"
746 );
747 }
748
749 #[test]
750 fn with_boundaries() {
751 assert_eq!(
752 "my_dumbFileName"
753 .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
754 .to_case(Case::Kebab),
755 "my-dumb-file-name"
756 );
757 }
758
759 // From issue https://github.com/rutrum/convert-case/issues/4
760 // From issue https://github.com/rutrum/convert-case/issues/8
761 #[test]
762 fn unicode_words() {
763 let strings = &["ПЕРСПЕКТИВА24", "música moderna"];
764 for s in strings {
765 for &case in Case::all_cases() {
766 assert!(!s.to_case(case).is_empty());
767 }
768 for &from in Case::all_cases() {
769 for &to in Case::all_cases() {
770 assert!(!s.from_case(from).to_case(to).is_empty());
771 }
772 }
773 }
774 }
775
776 // idea for asserting the associated boundaries are correct
777 #[test]
778 fn appropriate_associated_boundaries() {
779 let word_groups = &[
780 vec!["my", "var", "name"],
781 vec!["MY", "var", "Name"],
782 vec!["another", "vAR"],
783 vec!["XML", "HTTP", "Request"],
784 ];
785
786 for words in word_groups {
787 for case in Case::all_cases() {
788 if case == &Case::Flat || case == &Case::UpperFlat {
789 continue;
790 }
791 assert_eq!(
792 case.pattern().mutate(&split(
793 &case.pattern().mutate(words).join(case.delimiter()),
794 case.boundaries()
795 )),
796 case.pattern().mutate(words),
797 "Test boundaries on Case::{:?} with {:?}",
798 case,
799 words,
800 );
801 }
802 }
803 }
804}