//! Converts to and from various cases.
//!
//! # Command Line Utility `ccase`
//!
//! This library was developed for the purposes of a command line utility for converting
//! the case of strings and filenames. You can check out
//! [`ccase` on Github](https://github.com/rutrum/convert-case/tree/master/ccase).
//!
//! # Rust Library
//!
//! Provides a [`Case`](enum.Case.html) enum which defines a variety of cases to convert into.
//! Strings have implemented the [`Casing`](trait.Casing.html) trait, which adds methods for
//! case conversion.
//!
//! You can convert strings into a case using the [`to_case`](Casing::to_case) method.
//! ```
//! use convert_case::{Case, Casing};
//!
//! assert_eq!("Ronnie James Dio", "ronnie james dio".to_case(Case::Title));
//! assert_eq!("ronnieJamesDio", "Ronnie_James_dio".to_case(Case::Camel));
//! assert_eq!("Ronnie-James-Dio", "RONNIE_JAMES_DIO".to_case(Case::Train));
//! ```
//!
//! By default, `to_case` will split along a set of default word boundaries, that is
//! * space characters ` `,
//! * underscores `_`,
//! * hyphens `-`,
//! * changes in capitalization from lowercase to uppercase `aA`,
//! * adjacent digits and letters `a1`, `1a`, `A1`, `1A`,
//! * and acroynms `AAa` (as in `HTTPRequest`).
//!
//! For more accuracy, the `from_case` method splits based on the word boundaries
//! of a particular case. For example, splitting from snake case will only use
//! underscores as word boundaries.
//! ```
//! use convert_case::{Case, Casing};
//!
//! assert_eq!(
//! "2020 04 16 My Cat Cali",
//! "2020-04-16_my_cat_cali".to_case(Case::Title)
//! );
//! assert_eq!(
//! "2020-04-16 My Cat Cali",
//! "2020-04-16_my_cat_cali".from_case(Case::Snake).to_case(Case::Title)
//! );
//! ```
//!
//! Case conversion can detect acronyms for camel-like strings. It also ignores any leading,
//! trailing, or duplicate delimiters.
//! ```
//! use convert_case::{Case, Casing};
//!
//! assert_eq!("io_stream", "IOStream".to_case(Case::Snake));
//! assert_eq!("my_json_parser", "myJSONParser".to_case(Case::Snake));
//!
//! assert_eq!("weird_var_name", "__weird--var _name-".to_case(Case::Snake));
//! ```
//!
//! It also works non-ascii characters. However, no inferences on the language itself is made.
//! For instance, the digraph `ij` in Dutch will not be capitalized, because it is represented
//! as two distinct Unicode characters. However, `æ` would be capitalized. Accuracy with unicode
//! characters is done using the `unicode-segmentation` crate, the sole dependency of this crate.
//! ```
//! use convert_case::{Case, Casing};
//!
//! assert_eq!("granat-äpfel", "GranatÄpfel".to_case(Case::Kebab));
//! assert_eq!("Перспектива 24", "ПЕРСПЕКТИВА24".to_case(Case::Title));
//!
//! // The example from str::to_lowercase documentation
//! let odysseus = "ὈΔΥΣΣΕΎΣ";
//! assert_eq!("ὀδυσσεύς", odysseus.to_case(Case::Lower));
//! ```
//!
//! By default, characters followed by digits and vice-versa are
//! considered word boundaries. In addition, any special ASCII characters (besides `_` and `-`)
//! are ignored.
//! ```
//! use convert_case::{Case, Casing};
//!
//! assert_eq!("e_5150", "E5150".to_case(Case::Snake));
//! assert_eq!("10,000_days", "10,000Days".to_case(Case::Snake));
//! assert_eq!("HELLO, WORLD!", "Hello, world!".to_case(Case::Upper));
//! assert_eq!("One\ntwo\nthree", "ONE\nTWO\nTHREE".to_case(Case::Title));
//! ```
//!
//! You can also test what case a string is in.
//! ```
//! use convert_case::{Case, Casing};
//!
//! assert!( "css-class-name".is_case(Case::Kebab));
//! assert!(!"css-class-name".is_case(Case::Snake));
//! assert!(!"UPPER_CASE_VAR".is_case(Case::Snake));
//! ```
//!
//! # Note on Accuracy
//!
//! The `Casing` methods `from_case` and `to_case` do not fail. Conversion to a case will always
//! succeed. However, the results can still be unexpected. Failure to detect any word boundaries
//! for a particular case means the entire string will be considered a single word.
//! ```
//! use convert_case::{Case, Casing};
//!
//! // Mistakenly parsing using Case::Snake
//! assert_eq!("My-kebab-var", "my-kebab-var".from_case(Case::Snake).to_case(Case::Title));
//!
//! // Converts using an unexpected method
//! assert_eq!("my_kebab_like_variable", "myKebab-like-variable".to_case(Case::Snake));
//! ```
//!
//! # Boundary Specificity
//!
//! It can be difficult to determine how to split a string into words. That is why this case
//! provides the [`from_case`](Casing::from_case) functionality, but sometimes that isn't enough
//! to meet a specific use case.
//!
//! Take an identifier has the word `2D`, such as `scale2D`. No exclusive usage of `from_case` will
//! be enough to solve the problem. In this case we can further specify which boundaries to split
//! the string on. `convert_case` provides some patterns for achieving this specificity.
//! We can specify what boundaries we want to split on using the [`Boundary` enum](Boundary).
//! ```
//! use convert_case::{Boundary, Case, Casing};
//!
//! // Not quite what we want
//! assert_eq!(
//! "scale_2_d",
//! "scale2D"
//! .from_case(Case::Camel)
//! .to_case(Case::Snake)
//! );
//!
//! // Remove boundary from Case::Camel
//! assert_eq!(
//! "scale_2d",
//! "scale2D"
//! .from_case(Case::Camel)
//! .without_boundaries(&[Boundary::DigitUpper, Boundary::DigitLower])
//! .to_case(Case::Snake)
//! );
//!
//! // Write boundaries explicitly
//! assert_eq!(
//! "scale_2d",
//! "scale2D"
//! .with_boundaries(&[Boundary::LowerDigit])
//! .to_case(Case::Snake)
//! );
//! ```
//!
//! The `Casing` trait provides initial methods, but any subsequent methods that do not resolve
//! the conversion return a [`StateConverter`] struct. It contains similar methods as `Casing`.
//!
//! # Custom Cases
//!
//! Because `Case` is an enum, you can't create your own variant for your use case. However
//! the parameters for case conversion have been encapsulated into the [`Converter`] struct
//! which can be used for specific use cases.
//!
//! Suppose you wanted to format a word like camel case, where the first word is lower case and the
//! rest are capitalized. But you want to include a delimeter like underscore. This case isn't
//! available as a `Case` variant, but you can create it by constructing the parameters of the
//! `Converter`.
//! ```
//! use convert_case::{Case, Casing, Converter, Pattern};
//!
//! let conv = Converter::new()
//! .set_pattern(Pattern::Camel)
//! .set_delim("_");
//!
//! assert_eq!(
//! "my_Special_Case",
//! conv.convert("My Special Case")
//! )
//! ```
//! Just as with the `Casing` trait, you can also manually set the boundaries strings are split
//! on. You can use any of the [`Pattern`] variants available. This even includes [`Pattern::Sentence`]
//! which isn't used in any `Case` variant. You can also set no pattern at all, which will
//! maintain the casing of each letter in the input string. You can also, of course, set any string as your
//! delimeter.
//!
//! For more details on how strings are converted, see the docs for [`Converter`].
//!
//! # Random Feature
//!
//! To ensure this library had zero dependencies, randomness was moved to the _random_ feature,
//! which requires the `rand` crate. You can enable this feature by including the
//! following in your `Cargo.toml`.
//! ```{toml}
//! [dependencies]
//! convert_case = { version = "^0.3.0", features = ["random"] }
//! ```
//! This will add two additional cases: Random and PseudoRandom. You can read about their
//! construction in the [Case enum](enum.Case.html).
mod case;
mod converter;
mod pattern;
mod segmentation;
pub use case::Case;
pub use converter::Converter;
pub use pattern::Pattern;
pub use segmentation::Boundary;
/// Describes items that can be converted into a case. This trait is used
/// in conjunction with the [`StateConverter`] struct which is returned from a couple
/// methods on `Casing`.
///
/// Implemented for strings `&str`, `String`, and `&String`.
pub trait Casing<T: AsRef<str>> {
/// Convert the string into the given case. It will reference `self` and create a new
/// `String` with the same pattern and delimeter as `case`. It will split on boundaries
/// defined at [`Boundary::defaults()`].
/// ```
/// use convert_case::{Case, Casing};
///
/// assert_eq!(
/// "tetronimo-piece-border",
/// "Tetronimo piece border".to_case(Case::Kebab)
/// );
/// ```
fn to_case(&self, case: Case) -> String;
/// Start the case conversion by storing the boundaries associated with the given case.
/// ```
/// use convert_case::{Case, Casing};
///
/// assert_eq!(
/// "2020-08-10_dannie_birthday",
/// "2020-08-10 Dannie Birthday"
/// .from_case(Case::Title)
/// .to_case(Case::Snake)
/// );
/// ```
#[allow(clippy::wrong_self_convention)]
fn from_case(&self, case: Case) -> StateConverter<T>;
/// Creates a `StateConverter` struct initialized with the boundaries
/// provided.
/// ```
/// use convert_case::{Boundary, Case, Casing};
///
/// assert_eq!(
/// "e1_m1_hangar",
/// "E1M1 Hangar"
/// .with_boundaries(&[Boundary::DigitUpper, Boundary::Space])
/// .to_case(Case::Snake)
/// );
/// ```
fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>;
/// Determines if `self` is of the given case. This is done simply by applying
/// the conversion and seeing if the result is the same.
/// ```
/// use convert_case::{Case, Casing};
///
/// assert!( "kebab-case-string".is_case(Case::Kebab));
/// assert!( "Train-Case-String".is_case(Case::Train));
///
/// assert!(!"kebab-case-string".is_case(Case::Snake));
/// assert!(!"kebab-case-string".is_case(Case::Train));
/// ```
fn is_case(&self, case: Case) -> bool;
}
impl<T: AsRef<str>> Casing<T> for T
where
String: PartialEq<T>,
{
fn to_case(&self, case: Case) -> String {
StateConverter::new(self).to_case(case)
}
fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> {
StateConverter::new(self).with_boundaries(bs)
}
fn from_case(&self, case: Case) -> StateConverter<T> {
StateConverter::new_from_case(self, case)
}
fn is_case(&self, case: Case) -> bool {
&self.to_case(case) == self
}
}
/// Holds information about parsing before converting into a case.
///
/// This struct is used when invoking the `from_case` and `with_boundaries` methods on
/// `Casing`. For a more fine grained approach to case conversion, consider using the [`Converter`]
/// struct.
/// ```
/// use convert_case::{Case, Casing};
///
/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
/// assert_eq!("Ninety-nine Problems", title);
/// ```
pub struct StateConverter<'a, T: AsRef<str>> {
s: &'a T,
conv: Converter,
}
impl<'a, T: AsRef<str>> StateConverter<'a, T> {
/// Only called by Casing function to_case()
fn new(s: &'a T) -> Self {
Self {
s,
conv: Converter::new(),
}
}
/// Only called by Casing function from_case()
fn new_from_case(s: &'a T, case: Case) -> Self {
Self {
s,
conv: Converter::new().from_case(case),
}
}
/// Uses the boundaries associated with `case` for word segmentation. This
/// will overwrite any boundary information initialized before. This method is
/// likely not useful, but provided anyway.
/// ```
/// use convert_case::{Case, Casing};
///
/// let name = "Chuck Schuldiner"
/// .from_case(Case::Snake) // from Casing trait
/// .from_case(Case::Title) // from StateConverter, overwrites previous
/// .to_case(Case::Kebab);
/// assert_eq!("chuck-schuldiner", name);
/// ```
pub fn from_case(self, case: Case) -> Self {
Self {
conv: self.conv.from_case(case),
..self
}
}
/// Overwrites boundaries for word segmentation with those provided. This will overwrite
/// any boundary information initialized before. This method is likely not useful, but
/// provided anyway.
/// ```
/// use convert_case::{Boundary, Case, Casing};
///
/// let song = "theHumbling river-puscifer"
/// .from_case(Case::Kebab) // from Casing trait
/// .with_boundaries(&[Boundary::Space, Boundary::LowerUpper]) // overwrites `from_case`
/// .to_case(Case::Pascal);
/// assert_eq!("TheHumblingRiver-puscifer", song); // doesn't split on hyphen `-`
/// ```
pub fn with_boundaries(self, bs: &[Boundary]) -> Self {
Self {
s: self.s,
conv: self.conv.set_boundaries(bs),
}
}
/// Removes any boundaries that were already initialized. This is particularly useful when a
/// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude
/// some.
/// ```
/// use convert_case::{Boundary, Case, Casing};
///
/// assert_eq!(
/// "2d_transformation",
/// "2dTransformation"
/// .from_case(Case::Camel)
/// .without_boundaries(&Boundary::digits())
/// .to_case(Case::Snake)
/// );
/// ```
pub fn without_boundaries(self, bs: &[Boundary]) -> Self {
Self {
s: self.s,
conv: self.conv.remove_boundaries(bs),
}
}
/// Consumes the `StateConverter` and returns the converted string.
/// ```
/// use convert_case::{Boundary, Case, Casing};
///
/// assert_eq!(
/// "ice-cream social",
/// "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower)
/// );
/// ```
pub fn to_case(self, case: Case) -> String {
self.conv.to_case(case).convert(self.s)
}
}
#[cfg(test)]
mod test {
use super::*;
use strum::IntoEnumIterator;
fn possible_cases(s: &str) -> Vec<Case> {
Case::deterministic_cases()
.into_iter()
.filter(|case| s.from_case(*case).to_case(*case) == s)
.collect()
}
#[test]
fn lossless_against_lossless() {
let examples = vec![
(Case::Lower, "my variable 22 name"),
(Case::Upper, "MY VARIABLE 22 NAME"),
(Case::Title, "My Variable 22 Name"),
(Case::Camel, "myVariable22Name"),
(Case::Pascal, "MyVariable22Name"),
(Case::Snake, "my_variable_22_name"),
(Case::UpperSnake, "MY_VARIABLE_22_NAME"),
(Case::Kebab, "my-variable-22-name"),
(Case::Cobol, "MY-VARIABLE-22-NAME"),
(Case::Toggle, "mY vARIABLE 22 nAME"),
(Case::Train, "My-Variable-22-Name"),
(Case::Alternating, "mY vArIaBlE 22 nAmE"),
];
for (case_a, str_a) in examples.iter() {
for (case_b, str_b) in examples.iter() {
assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
}
}
}
#[test]
fn obvious_default_parsing() {
let examples = vec![
"SuperMario64Game",
"super-mario64-game",
"superMario64 game",
"Super Mario 64_game",
"SUPERMario 64-game",
"super_mario-64 game",
];
for example in examples {
assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
}
}
#[test]
fn multiline_strings() {
assert_eq!("One\ntwo\nthree", "one\ntwo\nthree".to_case(Case::Title));
}
#[test]
fn camel_case_acroynms() {
assert_eq!(
"xml_http_request",
"XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
);
assert_eq!(
"xml_http_request",
"XMLHttpRequest"
.from_case(Case::UpperCamel)
.to_case(Case::Snake)
);
assert_eq!(
"xml_http_request",
"XMLHttpRequest"
.from_case(Case::Pascal)
.to_case(Case::Snake)
);
}
#[test]
fn leading_tailing_delimeters() {
assert_eq!(
"leading_underscore",
"_leading_underscore"
.from_case(Case::Snake)
.to_case(Case::Snake)
);
assert_eq!(
"tailing_underscore",
"tailing_underscore_"
.from_case(Case::Snake)
.to_case(Case::Snake)
);
assert_eq!(
"leading_hyphen",
"-leading-hyphen"
.from_case(Case::Kebab)
.to_case(Case::Snake)
);
assert_eq!(
"tailing_hyphen",
"tailing-hyphen-"
.from_case(Case::Kebab)
.to_case(Case::Snake)
);
}
#[test]
fn double_delimeters() {
assert_eq!(
"many_underscores",
"many___underscores"
.from_case(Case::Snake)
.to_case(Case::Snake)
);
assert_eq!(
"many-underscores",
"many---underscores"
.from_case(Case::Kebab)
.to_case(Case::Kebab)
);
}
#[test]
fn early_word_boundaries() {
assert_eq!(
"a_bagel",
"aBagel".from_case(Case::Camel).to_case(Case::Snake)
);
}
#[test]
fn late_word_boundaries() {
assert_eq!(
"team_a",
"teamA".from_case(Case::Camel).to_case(Case::Snake)
);
}
#[test]
fn empty_string() {
for (case_a, case_b) in Case::iter().zip(Case::iter()) {
assert_eq!("", "".from_case(case_a).to_case(case_b));
}
}
#[test]
fn owned_string() {
assert_eq!(
"test_variable",
String::from("TestVariable").to_case(Case::Snake)
)
}
#[test]
fn default_all_boundaries() {
assert_eq!(
"abc_abc_abc_abc_abc_abc",
"ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
);
}
#[test]
fn alternating_ignore_symbols() {
assert_eq!("tHaT's", "that's".to_case(Case::Alternating));
}
#[test]
fn string_is_snake() {
assert!("im_snake_case".is_case(Case::Snake));
assert!(!"im_NOTsnake_case".is_case(Case::Snake));
}
#[test]
fn string_is_kebab() {
assert!("im-kebab-case".is_case(Case::Kebab));
assert!(!"im_not_kebab".is_case(Case::Kebab));
}
#[test]
fn remove_boundaries() {
assert_eq!(
"m02_s05_binary_trees.pdf",
"M02S05BinaryTrees.pdf"
.from_case(Case::Pascal)
.without_boundaries(&[Boundary::UpperDigit])
.to_case(Case::Snake)
);
}
#[test]
fn with_boundaries() {
assert_eq!(
"my-dumb-file-name",
"my_dumbFileName"
.with_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
.to_case(Case::Kebab)
);
}
#[cfg(feature = "random")]
#[test]
fn random_case_boundaries() {
for random_case in Case::random_cases() {
assert_eq!(
"split_by_spaces",
"Split By Spaces"
.from_case(random_case)
.to_case(Case::Snake)
);
}
}
#[test]
fn multiple_from_case() {
assert_eq!(
"longtime_nosee",
"LongTime NoSee"
.from_case(Case::Camel)
.from_case(Case::Title)
.to_case(Case::Snake),
)
}
use std::collections::HashSet;
use std::iter::FromIterator;
#[test]
fn detect_many_cases() {
let lower_cases_vec = possible_cases(&"asef");
let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
let mut actual = HashSet::new();
actual.insert(Case::Lower);
actual.insert(Case::Camel);
actual.insert(Case::Snake);
actual.insert(Case::Kebab);
actual.insert(Case::Flat);
assert_eq!(lower_cases_set, actual);
let lower_cases_vec = possible_cases(&"asefCase");
let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter());
let mut actual = HashSet::new();
actual.insert(Case::Camel);
assert_eq!(lower_cases_set, actual);
}
#[test]
fn detect_each_case() {
let s = "My String Identifier".to_string();
for case in Case::deterministic_cases() {
let new_s = s.from_case(case).to_case(case);
let possible = possible_cases(&new_s);
println!("{} {:?} {:?}", new_s, case, possible);
assert!(possible.iter().any(|c| c == &case));
}
}
// From issue https://github.com/rutrum/convert-case/issues/8
#[test]
fn accent_mark() {
let s = "música moderna".to_string();
assert_eq!("MúsicaModerna", s.to_case(Case::Pascal));
}
// From issue https://github.com/rutrum/convert-case/issues/4
#[test]
fn russian() {
let s = "ПЕРСПЕКТИВА24".to_string();
let _n = s.to_case(Case::Title);
}
}