diff options
Diffstat (limited to 'vendor/convert_case/src')
-rw-r--r-- | vendor/convert_case/src/case.rs | 212 | ||||
-rw-r--r-- | vendor/convert_case/src/lib.rs | 337 | ||||
-rw-r--r-- | vendor/convert_case/src/words.rs | 377 |
3 files changed, 926 insertions, 0 deletions
diff --git a/vendor/convert_case/src/case.rs b/vendor/convert_case/src/case.rs new file mode 100644 index 000000000..f7a9c8c9c --- /dev/null +++ b/vendor/convert_case/src/case.rs @@ -0,0 +1,212 @@ +#[cfg(test)] +use strum_macros::EnumIter; + +/// Defines the type of casing a string can be. +/// +/// ``` +/// use convert_case::{Case, Casing}; +/// +/// let super_mario_title: String = "super_mario_64".to_case(Case::Title); +/// assert_eq!("Super Mario 64", super_mario_title); +/// ``` +#[cfg_attr(test, derive(EnumIter))] +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum Case { + + /// Uppercase strings are delimited by spaces and all characters are uppercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("MY VARIABLE NAME", "My variable NAME".to_case(Case::Upper)) + /// ``` + Upper, + + /// Lowercase strings are delimited by spaces and all characters are lowercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("my variable name", "My variable NAME".to_case(Case::Lower)) + /// ``` + Lower, + + /// Title case strings are delimited by spaces. Only the leading character of + /// each word is uppercase. No inferences are made about language, so words + /// like "as", "to", and "for" will still be capitalized. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("My Variable Name", "My variable NAME".to_case(Case::Title)) + /// ``` + Title, + + /// Toggle case strings are delimited by spaces. All characters are uppercase except + /// for the leading character of each word, which is lowercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("mY vARIABLE nAME", "My variable NAME".to_case(Case::Toggle)) + /// ``` + Toggle, + + /// Camel case strings are lowercase, but for every word _except the first_ the + /// first letter is capitalized. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("myVariableName", "My variable NAME".to_case(Case::Camel)) + /// ``` + Camel, + + /// Pascal case strings are lowercase, but for every word the + /// first letter is capitalized. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("MyVariableName", "My variable NAME".to_case(Case::Pascal)) + /// ``` + Pascal, + + /// Upper camel case is an alternative name for Pascal case. + UpperCamel, + + /// Snake case strings are delimited by underscores `_` and are all lowercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("my_variable_name", "My variable NAME".to_case(Case::Snake)) + /// ``` + Snake, + + /// Upper snake case strings are delimited by underscores `_` and are all uppercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("MY_VARIABLE_NAME", "My variable NAME".to_case(Case::UpperSnake)) + /// ``` + UpperSnake, + + /// Screaming snake case is an alternative name for upper snake case. + ScreamingSnake, + + /// Kebab case strings are delimited by hyphens `-` and are all lowercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("my-variable-name", "My variable NAME".to_case(Case::Kebab)) + /// ``` + Kebab, + + /// Cobol case strings are delimited by hyphens `-` and are all uppercase. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("MY-VARIABLE-NAME", "My variable NAME".to_case(Case::Cobol)) + /// ``` + Cobol, + + /// Train case strings are delimited by hyphens `-`. All characters are lowercase + /// except for the leading character of each word. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("My-Variable-Name", "My variable NAME".to_case(Case::Train)) + /// ``` + Train, + + /// Flat case strings are all lowercase, with no delimiter. Converting to + /// this case is **lossy**. That is, word boundaries are lost. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("myvariablename", "My variable NAME".to_case(Case::Flat)) + /// ``` + Flat, + + /// Upper flat case strings are all uppercase, with no delimiter. Converting to + /// this case is **lossy**. That is, word boundaries are lost. + /// + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("MYVARIABLENAME", "My variable NAME".to_case(Case::UpperFlat)) + /// ``` + UpperFlat, + + /// Alternating case strings are delimited by spaces. Characters alternate between uppercase + /// and lowercase. + /// ``` + /// use convert_case::{Case, Casing}; + /// assert_eq!("mY vArIaBlE nAmE", "My variable NAME".to_case(Case::Alternating)); + /// ``` + Alternating, + + /// Random case strings are delimited by spaces and characters are + /// randomly upper case or lower case. This uses the `rand` crate + /// and is only available with the "random" feature. + /// ``` + /// use convert_case::{Case, Casing}; + /// let new = "My variable NAME".to_case(Case::Random); + /// ``` + /// `new` could be "My vaRIAbLE nAme" for example. + #[cfg(feature = "random")] + Random, + + /// Pseudo-random case strings are delimited by spaces and characters are randomly + /// upper case or lower case, but there will never more than two consecutive lower + /// case or upper case letters in a row. This uses the `rand` crate and is + /// only available with the "random" feature. + /// ``` + /// use convert_case::{Case, Casing}; + /// let new = "My variable NAME".to_case(Case::Random); + /// ``` + /// `new` could be "mY vArIAblE NamE" for example. + #[cfg(feature = "random")] + PseudoRandom, +} + +impl Case { + // Created to avoid using the EnumIter trait from strum in + // final library. A test confirms that all cases are listed here. + /// Returns a vector with all case enum variants. This was + /// created for use in the `ccase` binary. + pub fn all_cases() -> Vec<Case> { + use Case::*; + vec![ + Upper, + Lower, + Title, + Toggle, + Camel, + Pascal, + UpperCamel, + Snake, + UpperSnake, + ScreamingSnake, + Kebab, + Cobol, + Train, + Flat, + UpperFlat, + Alternating, + + #[cfg(feature = "random")] + Random, + #[cfg(feature = "random")] + PseudoRandom, + ] + } +} + +#[cfg(test)] +mod test { + + use super::*; + use strum::IntoEnumIterator; + + #[test] + fn all_cases_in_iter() { + let all = Case::all_cases(); + for case in Case::iter() { + assert!(all.contains(&case)); + } + } +} diff --git a/vendor/convert_case/src/lib.rs b/vendor/convert_case/src/lib.rs new file mode 100644 index 000000000..29711655f --- /dev/null +++ b/vendor/convert_case/src/lib.rs @@ -0,0 +1,337 @@ +//! Converts to and from various cases. +//! +//! # Command Line Utility `ccase` +//! +//! Since version "0.3.0" this crate is just a case conversion _library_. The command line utility +//! that uses the tools in this library has been moved to the `ccase` crate. You can read about it +//! at the [github repository](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. +//! A `Case` can be used with an item that implements the [`Casing`](trait.Casing.html) trait, +//! which allows the item to be converted to a given case. +//! +//! You can convert a string or string slice into a case using the `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 all word boundaries, that is +//! * space characters ` `, +//! * underscores `_`, +//! * hyphens `-`, +//! * and changes in capitalization `aA`. +//! +//! 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 treat +//! 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) +//! ); +//! ``` +//! +//! By default (and when converting from camel case or similar cases) `convert_case` +//! will detect acronyms. It also ignores any leading, trailing, or deplicate delimeters. +//! ``` +//! 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 diagraph `ij` in dutch will not be capitalized, because it is represented +//! as two distinct unicode characters. However, `æ` would be capitalized. +//! ``` +//! use convert_case::{Case, Casing}; +//! +//! assert_eq!("granat-äpfel", "GranatÄpfel".to_case(Case::Kebab)); +//! +//! // The example from str::to_lowercase documentation +//! let odysseus = "ὈΔΥΣΣΕΎΣ"; +//! assert_eq!("ὀδυσσεύς", odysseus.to_case(Case::Lower)); +//! ``` +//! +//! For the purposes of case conversion, characters followed by numerics 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)); +//! ``` +//! +//! # 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)); +//! ``` +//! +//! # 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, 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 words; +pub use case::Case; +use words::Words; + +/// Describes items that can be converted into a case. +/// +/// Implemented for string slices `&str` and owned strings `String`. +pub trait Casing { + /// References `self` and converts to the given case. + fn to_case(&self, case: Case) -> String; + + /// Creates a `FromCasing` struct, which saves information about + /// how to parse `self` before converting to a case. + fn from_case(&self, case: Case) -> FromCasing; +} + +impl Casing for str { + fn to_case(&self, case: Case) -> String { + Words::new(self).into_case(case) + } + + fn from_case(&self, case: Case) -> FromCasing { + FromCasing::new(self.to_string(), case) + } +} + +impl Casing for String { + fn to_case(&self, case: Case) -> String { + Words::new(self).into_case(case) + } + + fn from_case(&self, case: Case) -> FromCasing { + FromCasing::new(self.to_string(), case) + } +} + +/// Holds information about parsing before converting into a case. +/// +/// This struct is used when invoking the `from_case` method on +/// `Casing`. `FromCasing` also implements `Casing`. +/// ``` +/// 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 FromCasing { + name: String, + case: Case, +} + +impl FromCasing { + const fn new(name: String, case: Case) -> Self { + Self { name, case } + } +} + +impl Casing for FromCasing { + fn to_case(&self, case: Case) -> String { + Words::from_casing(&self.name, self.case).into_case(case) + } + + fn from_case(&self, case: Case) -> Self { + Self::new(self.name.to_string(), case) + } +} + +#[cfg(test)] +mod test { + use super::*; + use strum::IntoEnumIterator; + + #[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::ScreamingSnake, "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)); + } +} diff --git a/vendor/convert_case/src/words.rs b/vendor/convert_case/src/words.rs new file mode 100644 index 000000000..e7640107a --- /dev/null +++ b/vendor/convert_case/src/words.rs @@ -0,0 +1,377 @@ +use crate::Case; + +#[cfg(feature = "random")] +use rand::prelude::*; + +pub(super) struct Words { + words: Vec<String>, +} + +impl Words { + pub fn new(name: &str) -> Self { + let words = name + .split(|c| "-_ ".contains(c)) + .flat_map(Self::split_camel) + .filter(|s| !s.is_empty()) + .collect(); + Words { words } + } + + pub fn from_casing(name: &str, case: Case) -> Self { + use Case::*; + let words = match case { + Title | Upper | Lower | Toggle | Alternating => name + .split_ascii_whitespace() + .map(ToString::to_string) + .collect(), + Kebab | Cobol | Train => name + .split('-') + .map(ToString::to_string) + .filter(|s| !s.is_empty()) + .collect(), + Snake | UpperSnake | ScreamingSnake => name + .split('_') + .map(ToString::to_string) + .filter(|s| !s.is_empty()) + .collect(), + Pascal | Camel | UpperCamel => Self::split_camel(name), + Flat | UpperFlat => vec![name.to_string()], + + // Same behavior as title, upper, etc. + #[cfg(feature = "random")] + Random | PseudoRandom => name + .split_ascii_whitespace() + .map(ToString::to_string) + .collect(), + }; + Self { words } + } + + fn split_camel(name: &str) -> Vec<String> { + let left_iter = name.chars(); + let mid_iter = name.chars().skip(1); + let right_iter = name.chars().skip(2); + + let mut split_indices = left_iter + .zip(mid_iter) + .zip(right_iter) + .enumerate() + .filter(|(_, ((f, s), t))| Self::three_char_is_boundary(*f, *s, *t)) + .map(|(i, _)| i + 1) + .collect::<Vec<usize>>(); + + // Check for boundary in the last two characters + // Can be rewritten nicer (use fold) + let mut backwards_seek = name.chars().rev(); + let last = backwards_seek.next(); + let second_last = backwards_seek.next(); + if let (Some(a), Some(b)) = (second_last, last) { + if Self::two_char_is_boundary(a, b) { + split_indices.push(name.len() - 1); + } + } + + Self::split_at_indices(name, split_indices) + } + + /// Allowed boundaries are (lower upper) (digit (!digit and !punc)) ((!digit and !punc) digit). + fn two_char_is_boundary(f: char, s: char) -> bool { + (f.is_lowercase() && s.is_uppercase()) + || (f.is_ascii_digit() && !(s.is_ascii_digit() || s.is_ascii_punctuation())) + || (!(f.is_ascii_digit() || f.is_ascii_punctuation()) && s.is_ascii_digit()) + } + + /// Checks if three characters are the end of an acronym, otherwise + /// calls `two_char_is_boundary`. + fn three_char_is_boundary(f: char, s: char, t: char) -> bool { + (f.is_uppercase() && s.is_uppercase() && t.is_lowercase()) + || Self::two_char_is_boundary(f, s) + } + + fn split_at_indices(name: &str, indices: Vec<usize>) -> Vec<String> { + let mut words = Vec::new(); + + let mut first = name; + let mut second; + for &x in indices.iter().rev() { + let pair = first.split_at(x); + first = pair.0; + second = pair.1; + words.push(second); + } + words.push(first); + + words.iter().rev().map(ToString::to_string).collect() + } + + pub fn into_case(mut self, case: Case) -> String { + use Case::*; + match case { + Camel => { + self.make_camel_case(); + self.join("") + } + Title => { + self.capitalize_first_letter(); + self.join(" ") + } + Pascal | UpperCamel => { + self.capitalize_first_letter(); + self.join("") + } + Toggle => { + self.lower_first_letter(); + self.join(" ") + } + Snake => { + self.make_lowercase(); + self.join("_") + } + Cobol => { + self.make_uppercase(); + self.join("-") + } + Kebab => { + self.make_lowercase(); + self.join("-") + } + UpperSnake | ScreamingSnake => { + self.make_uppercase(); + self.join("_") + } + Lower => { + self.make_lowercase(); + self.join(" ") + } + Upper => { + self.make_uppercase(); + self.join(" ") + } + Flat => { + self.make_lowercase(); + self.join("") + } + Train => { + self.capitalize_first_letter(); + self.join("-") + } + UpperFlat => { + self.make_uppercase(); + self.join("") + } + Alternating => { + self.make_alternating(); + self.join(" ") + } + #[cfg(feature = "random")] + Random => { + self.randomize(); + self.join(" ") + } + #[cfg(feature = "random")] + PseudoRandom => { + self.pseudo_randomize(); + self.join(" ") + } + } + } + + // Randomly picks whether to be upper case or lower case + #[cfg(feature = "random")] + fn randomize(&mut self) { + let mut rng = rand::thread_rng(); + self.words = self + .words + .iter() + .map(|word| { + word.chars() + .map(|letter| { + if rng.gen::<f32>() > 0.5 { + letter.to_uppercase().to_string() + } else { + letter.to_lowercase().to_string() + } + }) + .collect() + }) + .collect(); + } + + // Randomly selects patterns: [upper, lower] or [lower, upper] + // for a more random feeling pattern. + #[cfg(feature = "random")] + fn pseudo_randomize(&mut self) { + let mut rng = rand::thread_rng(); + + // Keeps track of when to alternate + let mut alt: Option<bool> = None; + self.words = self + .words + .iter() + .map(|word| { + word.chars() + .map(|letter| { + match alt { + // No existing pattern, start one + None => { + if rng.gen::<f32>() > 0.5 { + alt = Some(false); // Make the next char lower + letter.to_uppercase().to_string() + } else { + alt = Some(true); // Make the next char upper + letter.to_lowercase().to_string() + } + } + // Existing pattern, do what it says + Some(upper) => { + alt = None; + if upper { + letter.to_uppercase().to_string() + } else { + letter.to_lowercase().to_string() + } + } + } + }) + .collect() + }) + .collect(); + } + + fn make_camel_case(&mut self) { + self.words = self + .words + .iter() + .enumerate() + .map(|(i, word)| { + if i != 0 { + let mut chars = word.chars(); + if let Some(a) = chars.next() { + a.to_uppercase() + .chain(chars.as_str().to_lowercase().chars()) + .collect() + } else { + String::new() + } + } else { + word.to_lowercase() + } + }) + .collect(); + } + + fn make_alternating(&mut self) { + let mut upper = false; + self.words = self + .words + .iter() + .map(|word| { + word.chars() + .map(|letter| { + if letter.is_uppercase() || letter.is_lowercase() { + if upper { + upper = false; + letter.to_uppercase().to_string() + } else { + upper = true; + letter.to_lowercase().to_string() + } + } else { + letter.to_string() + } + }) + .collect() + }) + .collect(); + } + + fn make_uppercase(&mut self) { + self.words = self.words.iter().map(|word| word.to_uppercase()).collect(); + } + + fn make_lowercase(&mut self) { + self.words = self.words.iter().map(|word| word.to_lowercase()).collect(); + } + + fn capitalize_first_letter(&mut self) { + self.words = self + .words + .iter() + .map(|word| { + let mut chars = word.chars(); + if let Some(a) = chars.next() { + a.to_uppercase() + .chain(chars.as_str().to_lowercase().chars()) + .collect() + } else { + String::new() + } + }) + .collect(); + } + + fn lower_first_letter(&mut self) { + self.words = self + .words + .iter() + .map(|word| { + let mut chars = word.chars(); + if let Some(a) = chars.next() { + a.to_lowercase() + .chain(chars.as_str().to_uppercase().chars()) + .collect() + } else { + String::new() + } + }) + .collect(); + } + + // Alternative: construct [my, -, variable, -, name] then collect + fn join(self, delim: &str) -> String { + self.words + .iter() + .enumerate() + .map(|(i, val)| { + if i == 0 { + val.to_owned() + } else { + delim.to_owned() + val + } + }) + .collect() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn correct_two_char_boundaries() { + assert!(!Words::two_char_is_boundary('a', 'a')); + assert!(Words::two_char_is_boundary('a', 'A')); + assert!(Words::two_char_is_boundary('a', '5')); + assert!(!Words::two_char_is_boundary('a', ',')); + assert!(!Words::two_char_is_boundary('A', 'A')); + assert!(!Words::two_char_is_boundary('A', 'a')); + assert!(Words::two_char_is_boundary('A', '5')); + assert!(!Words::two_char_is_boundary('A', ',')); + assert!(Words::two_char_is_boundary('5', 'a')); + assert!(Words::two_char_is_boundary('5', 'A')); + assert!(!Words::two_char_is_boundary('5', '5')); + assert!(!Words::two_char_is_boundary('5', ',')); + assert!(!Words::two_char_is_boundary(',', 'a')); + assert!(!Words::two_char_is_boundary(',', 'A')); + assert!(!Words::two_char_is_boundary(',', '5')); + assert!(!Words::two_char_is_boundary(',', ',')); + } + + #[test] + fn correct_three_char_boundaries() { + assert!(Words::three_char_is_boundary('A', 'A', 'a')); + assert!(!Words::three_char_is_boundary('A', 'a', 'a')); + assert!(!Words::three_char_is_boundary('A', 'a', 'A')); + assert!(!Words::three_char_is_boundary('A', 'A', '3')); + } +} |