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/convert-case/tree/master/ccase).
8//!
9//! # Rust Library
10//!
11//! Provides a [`Case`](enum.Case.html) enum which defines a variety of cases to convert into.
12//! Strings have implemented the [`Casing`](trait.Casing.html) trait, which adds methods for
13//! case conversion.
14//!
15//! You can convert strings into a case using the [`to_case`](Casing::to_case) method.
16//! ```
17//! use convert_case::{Case, Casing};
18//!
19//! assert_eq!("Ronnie James Dio", "ronnie james dio".to_case(Case::Title));
20//! assert_eq!("ronnieJamesDio", "Ronnie_James_dio".to_case(Case::Camel));
21//! assert_eq!("Ronnie-James-Dio", "RONNIE_JAMES_DIO".to_case(Case::Train));
22//! ```
23//!
24//! By default, `to_case` will split along a set of default word boundaries, that is
25//! * space characters ` `,
26//! * underscores `_`,
27//! * hyphens `-`,
28//! * changes in capitalization from lowercase to uppercase `aA`,
29//! * adjacent digits and letters `a1`, `1a`, `A1`, `1A`,
30//! * and acroynms `AAa` (as in `HTTPRequest`).
31//!
32//! For more accuracy, the `from_case` method splits based on the word boundaries
33//! of a particular case. For example, splitting from snake case will only use
34//! underscores as word boundaries.
35//! ```
36//! use convert_case::{Case, Casing};
37//!
38//! assert_eq!(
39//! "2020 04 16 My Cat Cali",
40//! "2020-04-16_my_cat_cali".to_case(Case::Title)
41//! );
42//! assert_eq!(
43//! "2020-04-16 My Cat Cali",
44//! "2020-04-16_my_cat_cali".from_case(Case::Snake).to_case(Case::Title)
45//! );
46//! ```
47//!
48//! Case conversion can detect acronyms for camel-like strings. It also ignores any leading,
49//! trailing, or duplicate delimiters.
50//! ```
51//! use convert_case::{Case, Casing};
52//!
53//! assert_eq!("io_stream", "IOStream".to_case(Case::Snake));
54//! assert_eq!("my_json_parser", "myJSONParser".to_case(Case::Snake));
55//!
56//! assert_eq!("weird_var_name", "__weird--var _name-".to_case(Case::Snake));
57//! ```
58//!
59//! It also works non-ascii characters. However, no inferences on the language itself is made.
60//! For instance, the digraph `ij` in Dutch will not be capitalized, because it is represented
61//! as two distinct Unicode characters. However, `æ` would be capitalized. Accuracy with unicode
62//! characters is done using the `unicode-segmentation` crate, the sole dependency of this crate.
63//! ```
64//! use convert_case::{Case, Casing};
65//!
66//! assert_eq!("granat-äpfel", "GranatÄpfel".to_case(Case::Kebab));
67//! assert_eq!("Перспектива 24", "ПЕРСПЕКТИВА24".to_case(Case::Title));
68//!
69//! // The example from str::to_lowercase documentation
70//! let odysseus = "ὈΔΥΣΣΕΎΣ";
71//! assert_eq!("ὀδυσσεύς", odysseus.to_case(Case::Lower));
72//! ```
73//!
74//! By default, characters followed by digits and vice-versa are
75//! considered word boundaries. In addition, any special ASCII characters (besides `_` and `-`)
76//! are ignored.
77//! ```
78//! use convert_case::{Case, Casing};
79//!
80//! assert_eq!("e_5150", "E5150".to_case(Case::Snake));
81//! assert_eq!("10,000_days", "10,000Days".to_case(Case::Snake));
82//! assert_eq!("HELLO, WORLD!", "Hello, world!".to_case(Case::Upper));
83//! assert_eq!("One\ntwo\nthree", "ONE\nTWO\nTHREE".to_case(Case::Title));
84//! ```
85//!
86//! You can also test what case a string is in.
87//! ```
88//! use convert_case::{Case, Casing};
89//!
90//! assert!( "css-class-name".is_case(Case::Kebab));
91//! assert!(!"css-class-name".is_case(Case::Snake));
92//! assert!(!"UPPER_CASE_VAR".is_case(Case::Snake));
93//! ```
94//!
95//! # Note on Accuracy
96//!
97//! The `Casing` methods `from_case` and `to_case` do not fail. Conversion to a case will always
98//! succeed. However, the results can still be unexpected. Failure to detect any word boundaries
99//! for a particular case means the entire string will be considered a single word.
100//! ```
101//! use convert_case::{Case, Casing};
102//!
103//! // Mistakenly parsing using Case::Snake
104//! assert_eq!("My-kebab-var", "my-kebab-var".from_case(Case::Snake).to_case(Case::Title));
105//!
106//! // Converts using an unexpected method
107//! assert_eq!("my_kebab_like_variable", "myKebab-like-variable".to_case(Case::Snake));
108//! ```
109//!
110//! # Boundary Specificity
111//!
112//! It can be difficult to determine how to split a string into words. That is why this case
113//! provides the [`from_case`](Casing::from_case) functionality, but sometimes that isn't enough
114//! to meet a specific use case.
115//!
116//! Take an identifier has the word `2D`, such as `scale2D`. No exclusive usage of `from_case` will
117//! be enough to solve the problem. In this case we can further specify which boundaries to split
118//! the string on. `convert_case` provides some patterns for achieving this specificity.
119//! We can specify what boundaries we want to split on using the [`Boundary` enum](Boundary).
120//! ```
121//! use convert_case::{Boundary, Case, Casing};
122//!
123//! // Not quite what we want
124//! assert_eq!(
125//! "scale_2_d",
126//! "scale2D"
127//! .from_case(Case::Camel)
128//! .to_case(Case::Snake)
129//! );
130//!
131//! // Remove boundary from Case::Camel
132//! assert_eq!(
133//! "scale_2d",
134//! "scale2D"
135//! .from_case(Case::Camel)
136//! .without_boundaries(&[Boundary::DigitUpper, Boundary::DigitLower])
137//! .to_case(Case::Snake)
138//! );
139//!
140//! // Write boundaries explicitly
141//! assert_eq!(
142//! "scale_2d",
143//! "scale2D"
144//! .with_boundaries(&[Boundary::LowerDigit])
145//! .to_case(Case::Snake)
146//! );
147//! ```
148//!
149//! The `Casing` trait provides initial methods, but any subsequent methods that do not resolve
150//! the conversion return a [`StateConverter`] struct. It contains similar methods as `Casing`.
151//!
152//! # Custom Cases
153//!
154//! Because `Case` is an enum, you can't create your own variant for your use case. However
155//! the parameters for case conversion have been encapsulated into the [`Converter`] struct
156//! which can be used for specific use cases.
157//!
158//! Suppose you wanted to format a word like camel case, where the first word is lower case and the
159//! rest are capitalized. But you want to include a delimeter like underscore. This case isn't
160//! available as a `Case` variant, but you can create it by constructing the parameters of the
161//! `Converter`.
162//! ```
163//! use convert_case::{Case, Casing, Converter, Pattern};
164//!
165//! let conv = Converter::new()
166//! .set_pattern(Pattern::Camel)
167//! .set_delim("_");
168//!
169//! assert_eq!(
170//! "my_Special_Case",
171//! conv.convert("My Special Case")
172//! )
173//! ```
174//! Just as with the `Casing` trait, you can also manually set the boundaries strings are split
175//! on. You can use any of the [`Pattern`] variants available. This even includes [`Pattern::Sentence`]
176//! which isn't used in any `Case` variant. You can also set no pattern at all, which will
177//! maintain the casing of each letter in the input string. You can also, of course, set any string as your
178//! delimeter.
179//!
180//! For more details on how strings are converted, see the docs for [`Converter`].
181//!
182//! # Random Feature
183//!
184//! To ensure this library had zero dependencies, randomness was moved to the _random_ feature,
185//! which requires the `rand` crate. You can enable this feature by including the
186//! following in your `Cargo.toml`.
187//! ```{toml}
188//! [dependencies]
189//! convert_case = { version = "^0.3.0", features = ["random"] }
190//! ```
191//! This will add two additional cases: Random and PseudoRandom. You can read about their
192//! construction in the [Case enum](enum.Case.html).
193
194mod case;
195mod converter;
196mod pattern;
197mod segmentation;
198
199pub use case::Case;
200pub use converter::Converter;
201pub use pattern::Pattern;
202pub use segmentation::Boundary;
203
204/// Describes items that can be converted into a case. This trait is used
205/// in conjunction with the [`StateConverter`] struct which is returned from a couple
206/// methods on `Casing`.
207///
208/// Implemented for strings `&str`, `String`, and `&String`.
209pub trait Casing<T: AsRef<str>> {
210
211 /// Convert the string into the given case. It will reference `self` and create a new
212 /// `String` with the same pattern and delimeter as `case`. It will split on boundaries
213 /// defined at [`Boundary::defaults()`].
214 /// ```
215 /// use convert_case::{Case, Casing};
216 ///
217 /// assert_eq!(
218 /// "tetronimo-piece-border",
219 /// "Tetronimo piece border".to_case(Case::Kebab)
220 /// );
221 /// ```
222 fn to_case(&self, case: Case) -> String;
223
224 /// Start the case conversion by storing the boundaries associated with the given case.
225 /// ```
226 /// use convert_case::{Case, Casing};
227 ///
228 /// assert_eq!(
229 /// "2020-08-10_dannie_birthday",
230 /// "2020-08-10 Dannie Birthday"
231 /// .from_case(Case::Title)
232 /// .to_case(Case::Snake)
233 /// );
234 /// ```
235 #[allow(clippy::wrong_self_convention)]
236 fn from_case(&self, case: Case) -> StateConverter<T>;
237
238 /// Creates a `StateConverter` struct initialized with the boundaries
239 /// provided.
240 /// ```
241 /// use convert_case::{Boundary, Case, Casing};
242 ///
243 /// assert_eq!(
244 /// "e1_m1_hangar",
245 /// "E1M1 Hangar"
246 /// .with_boundaries(&[Boundary::DigitUpper, Boundary::Space])
247 /// .to_case(Case::Snake)
248 /// );
249 /// ```
250 fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
251
252 /// Determines if `self` is of the given case. This is done simply by applying
253 /// the conversion and seeing if the result is the same.
254 /// ```
255 /// use convert_case::{Case, Casing};
256 ///
257 /// assert!( "kebab-case-string".is_case(Case::Kebab));
258 /// assert!( "Train-Case-String".is_case(Case::Train));
259 ///
260 /// assert!(!"kebab-case-string".is_case(Case::Snake));
261 /// assert!(!"kebab-case-string".is_case(Case::Train));
262 /// ```
263 fn is_case(&self, case: Case) -> bool;
264}
265
266impl<T: AsRef<str>> Casing<T> for T
267where
268 String: PartialEq<T>,
269{
270 fn to_case(&self, case: Case) -> String {
271 StateConverter::new(self).to_case(case)
272 }
273
274 fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
275 StateConverter::new(self).with_boundaries(bs)
276 }
277
278 fn from_case(&self, case: Case) -> StateConverter<T> {
279 StateConverter::new_from_case(self, case)
280 }
281
282 fn is_case(&self, case: Case) -> bool {
283 &self.to_case(case) == self
284 }
285}
286
287/// Holds information about parsing before converting into a case.
288///
289/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
290/// `Casing`. For a more fine grained approach to case conversion, consider using the [`Converter`]
291/// struct.
292/// ```
293/// use convert_case::{Case, Casing};
294///
295/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
296/// assert_eq!("Ninety-nine Problems", title);
297/// ```
298pub struct StateConverter<'a, T: AsRef<str>> {
299 s: &'a T,
300 conv: Converter,
301}
302
303impl<'a, T: AsRef<str>> StateConverter<'a, T> {
304 /// Only called by Casing function to_case()
305 fn new(s: &'a T) -> Self {
306 Self {
307 s,
308 conv: Converter::new(),
309 }
310 }
311
312 /// Only called by Casing function from_case()
313 fn new_from_case(s: &'a T, case: Case) -> Self {
314 Self {
315 s,
316 conv: Converter::new().from_case(case),
317 }
318 }
319
320 /// Uses the boundaries associated with `case` for word segmentation. This
321 /// will overwrite any boundary information initialized before. This method is
322 /// likely not useful, but provided anyway.
323 /// ```
324 /// use convert_case::{Case, Casing};
325 ///
326 /// let name = "Chuck Schuldiner"
327 /// .from_case(Case::Snake) // from Casing trait
328 /// .from_case(Case::Title) // from StateConverter, overwrites previous
329 /// .to_case(Case::Kebab);
330 /// assert_eq!("chuck-schuldiner", name);
331 /// ```
332 pub fn from_case(self, case: Case) -> Self {
333 Self {
334 conv: self.conv.from_case(case),
335 ..self
336 }
337 }
338
339 /// Overwrites boundaries for word segmentation with those provided. This will overwrite
340 /// any boundary information initialized before. This method is likely not useful, but
341 /// provided anyway.
342 /// ```
343 /// use convert_case::{Boundary, Case, Casing};
344 ///
345 /// let song = "theHumbling river-puscifer"
346 /// .from_case(Case::Kebab) // from Casing trait
347 /// .with_boundaries(&[Boundary::Space, Boundary::LowerUpper]) // overwrites `from_case`
348 /// .to_case(Case::Pascal);
349 /// assert_eq!("TheHumblingRiver-puscifer", song); // doesn't split on hyphen `-`
350 /// ```
351 pub fn with_boundaries(self, bs: &[Boundary]) -> Self {
352 Self {
353 s: self.s,
354 conv: self.conv.set_boundaries(bs),
355 }
356 }
357
358 /// Removes any boundaries that were already initialized. This is particularly useful when a
359 /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
360 /// some.
361 /// ```
362 /// use convert_case::{Boundary, Case, Casing};
363 ///
364 /// assert_eq!(
365 /// "2d_transformation",
366 /// "2dTransformation"
367 /// .from_case(Case::Camel)
368 /// .without_boundaries(&Boundary::digits())
369 /// .to_case(Case::Snake)
370 /// );
371 /// ```
372 pub fn without_boundaries(self, bs: &[Boundary]) -> Self {
373 Self {
374 s: self.s,
375 conv: self.conv.remove_boundaries(bs),
376 }
377 }
378
379 /// Consumes the `StateConverter` and returns the converted string.
380 /// ```
381 /// use convert_case::{Boundary, Case, Casing};
382 ///
383 /// assert_eq!(
384 /// "ice-cream social",
385 /// "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower)
386 /// );
387 /// ```
388 pub fn to_case(self, case: Case) -> String {
389 self.conv.to_case(case).convert(self.s)
390 }
391}
392
393#[cfg(test)]
394mod test {
395 use super::*;
396 use strum::IntoEnumIterator;
397
398 fn possible_cases(s: &str) -> Vec<Case> {
399 Case::deterministic_cases()
400 .into_iter()
401 .filter(|case| s.from_case(*case).to_case(*case) == s)
402 .collect()
403 }
404
405 #[test]
406 fn lossless_against_lossless() {
407 let examples = vec![
408 (Case::Lower, "my variable 22 name"),
409 (Case::Upper, "MY VARIABLE 22 NAME"),
410 (Case::Title, "My Variable 22 Name"),
411 (Case::Camel, "myVariable22Name"),
412 (Case::Pascal, "MyVariable22Name"),
413 (Case::Snake, "my_variable_22_name"),
414 (Case::UpperSnake, "MY_VARIABLE_22_NAME"),
415 (Case::Kebab, "my-variable-22-name"),
416 (Case::Cobol, "MY-VARIABLE-22-NAME"),
417 (Case::Toggle, "mY vARIABLE 22 nAME"),
418 (Case::Train, "My-Variable-22-Name"),
419 (Case::Alternating, "mY vArIaBlE 22 nAmE"),
420 ];
421
422 for (case_a, str_a) in examples.iter() {
423 for (case_b, str_b) in examples.iter() {
424 assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
425 }
426 }
427 }
428
429 #[test]
430 fn obvious_default_parsing() {
431 let examples = vec![
432 "SuperMario64Game",
433 "super-mario64-game",
434 "superMario64 game",
435 "Super Mario 64_game",
436 "SUPERMario 64-game",
437 "super_mario-64 game",
438 ];
439
440 for example in examples {
441 assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
442 }
443 }
444
445 #[test]
446 fn multiline_strings() {
447 assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
448 }
449
450 #[test]
451 fn camel_case_acroynms() {
452 assert_eq!(
453 "xml_http_request",
454 "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
455 );
456 assert_eq!(
457 "xml_http_request",
458 "XMLHttpRequest"
459 .from_case(Case::UpperCamel)
460 .to_case(Case::Snake)
461 );
462 assert_eq!(
463 "xml_http_request",
464 "XMLHttpRequest"
465 .from_case(Case::Pascal)
466 .to_case(Case::Snake)
467 );
468 }
469
470 #[test]
471 fn leading_tailing_delimeters() {
472 assert_eq!(
473 "leading_underscore",
474 "_leading_underscore"
475 .from_case(Case::Snake)
476 .to_case(Case::Snake)
477 );
478 assert_eq!(
479 "tailing_underscore",
480 "tailing_underscore_"
481 .from_case(Case::Snake)
482 .to_case(Case::Snake)
483 );
484 assert_eq!(
485 "leading_hyphen",
486 "-leading-hyphen"
487 .from_case(Case::Kebab)
488 .to_case(Case::Snake)
489 );
490 assert_eq!(
491 "tailing_hyphen",
492 "tailing-hyphen-"
493 .from_case(Case::Kebab)
494 .to_case(Case::Snake)
495 );
496 }
497
498 #[test]
499 fn double_delimeters() {
500 assert_eq!(
501 "many_underscores",
502 "many___underscores"
503 .from_case(Case::Snake)
504 .to_case(Case::Snake)
505 );
506 assert_eq!(
507 "many-underscores",
508 "many---underscores"
509 .from_case(Case::Kebab)
510 .to_case(Case::Kebab)
511 );
512 }
513
514 #[test]
515 fn early_word_boundaries() {
516 assert_eq!(
517 "a_bagel",
518 "aBagel".from_case(Case::Camel).to_case(Case::Snake)
519 );
520 }
521
522 #[test]
523 fn late_word_boundaries() {
524 assert_eq!(
525 "team_a",
526 "teamA".from_case(Case::Camel).to_case(Case::Snake)
527 );
528 }
529
530 #[test]
531 fn empty_string() {
532 for (case_a, case_b) in Case::iter().zip(Case::iter()) {
533 assert_eq!("", "".from_case(case_a).to_case(case_b));
534 }
535 }
536
537 #[test]
538 fn owned_string() {
539 assert_eq!(
540 "test_variable",
541 String::from("TestVariable").to_case(Case::Snake)
542 )
543 }
544
545 #[test]
546 fn default_all_boundaries() {
547 assert_eq!(
548 "abc_abc_abc_abc_abc_abc",
549 "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
550 );
551 }
552
553 #[test]
554 fn alternating_ignore_symbols() {
555 assert_eq!("tHaT's", "that's".to_case(Case::Alternating));
556 }
557
558 #[test]
559 fn string_is_snake() {
560 assert!("im_snake_case".is_case(Case::Snake));
561 assert!(!"im_NOTsnake_case".is_case(Case::Snake));
562 }
563
564 #[test]
565 fn string_is_kebab() {
566 assert!("im-kebab-case".is_case(Case::Kebab));
567 assert!(!"im_not_kebab".is_case(Case::Kebab));
568 }
569
570 #[test]
571 fn remove_boundaries() {
572 assert_eq!(
573 "m02_s05_binary_trees.pdf",
574 "M02S05BinaryTrees.pdf"
575 .from_case(Case::Pascal)
576 .without_boundaries(&[Boundary::UpperDigit])
577 .to_case(Case::Snake)
578 );
579 }
580
581 #[test]
582 fn with_boundaries() {
583 assert_eq!(
584 "my-dumb-file-name",
585 "my_dumbFileName"
586 .with_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
587 .to_case(Case::Kebab)
588 );
589 }
590
591 #[cfg(feature = "random")]
592 #[test]
593 fn random_case_boundaries() {
594 for random_case in Case::random_cases() {
595 assert_eq!(
596 "split_by_spaces",
597 "Split By Spaces"
598 .from_case(random_case)
599 .to_case(Case::Snake)
600 );
601 }
602 }
603
604 #[test]
605 fn multiple_from_case() {
606 assert_eq!(
607 "longtime_nosee",
608 "LongTime NoSee"
609 .from_case(Case::Camel)
610 .from_case(Case::Title)
611 .to_case(Case::Snake),
612 )
613 }
614
615 use std::collections::HashSet;
616 use std::iter::FromIterator;
617
618 #[test]
619 fn detect_many_cases() {
620 let lower_cases_vec = possible_cases(&"asef");
621 let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
622 let mut actual = HashSet::new();
623 actual.insert(Case::Lower);
624 actual.insert(Case::Camel);
625 actual.insert(Case::Snake);
626 actual.insert(Case::Kebab);
627 actual.insert(Case::Flat);
628 assert_eq!(lower_cases_set, actual);
629
630 let lower_cases_vec = possible_cases(&"asefCase");
631 let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
632 let mut actual = HashSet::new();
633 actual.insert(Case::Camel);
634 assert_eq!(lower_cases_set, actual);
635 }
636
637 #[test]
638 fn detect_each_case() {
639 let s = "My String Identifier".to_string();
640 for case in Case::deterministic_cases() {
641 let new_s = s.from_case(case).to_case(case);
642 let possible = possible_cases(&new_s);
643 println!("{} {:?} {:?}", new_s, case, possible);
644 assert!(possible.iter().any(|c| c == &case));
645 }
646 }
647
648 // From issue https://github.com/rutrum/convert-case/issues/8
649 #[test]
650 fn accent_mark() {
651 let s = "música moderna".to_string();
652 assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
653 }
654
655 // From issue https://github.com/rutrum/convert-case/issues/4
656 #[test]
657 fn russian() {
658 let s = "ПЕРСПЕКТИВА24".to_string();
659 let _n = s.to_case(Case::Title);
660 }
661}
662