convert_case/
lib.rs

1//! Converts to and from various cases.
2//!
3//! # Command Line Utility `ccase`
4//!
5//! Since version "0.3.0" this crate is just a case conversion _library_.  The command line utility
6//! that uses the tools in this library has been moved to the `ccase` crate.  You can read about it
7//! at the [github repository](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//! A `Case` can be used with an item that implements the [`Casing`](trait.Casing.html) trait,
13//! which allows the item to be converted to a given case.
14//!
15//! You can convert a string or string slice into a case using the `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 all word boundaries, that is
25//! * space characters ` `,
26//! * underscores `_`,
27//! * hyphens `-`,
28//! * and changes in capitalization `aA`.
29//!
30//! For more accuracy, the `from_case` method splits based on the word boundaries
31//! of a particular case.  For example, splitting from snake case will only treat
32//! underscores as word boundaries.
33//! ```
34//! use convert_case::{Case, Casing};
35//!
36//! assert_eq!(
37//!     "2020 04 16 My Cat Cali",
38//!     "2020-04-16_my_cat_cali".to_case(Case::Title)
39//! );
40//! assert_eq!(
41//!     "2020-04-16 My Cat Cali",
42//!     "2020-04-16_my_cat_cali".from_case(Case::Snake).to_case(Case::Title)
43//! );
44//! ```
45//!
46//! By default (and when converting from camel case or similar cases) `convert_case`
47//! will detect acronyms.  It also ignores any leading, trailing, or deplicate delimeters.
48//! ```
49//! use convert_case::{Case, Casing};
50//!
51//! assert_eq!("io_stream", "IOStream".to_case(Case::Snake));
52//! assert_eq!("my_json_parser", "myJSONParser".to_case(Case::Snake));
53//!
54//! assert_eq!("weird_var_name", "__weird--var _name-".to_case(Case::Snake));
55//! ```
56//!
57//! It also works non-ascii characters.  However, no inferences on the language itself is made.
58//! For instance, the diagraph `ij` in dutch will not be capitalized, because it is represented
59//! as two distinct unicode characters.  However, `æ` would be capitalized.
60//! ```
61//! use convert_case::{Case, Casing};
62//!
63//! assert_eq!("granat-äpfel", "GranatÄpfel".to_case(Case::Kebab));
64//!
65//! // The example from str::to_lowercase documentation
66//! let odysseus = "ὈΔΥΣΣΕΎΣ";
67//! assert_eq!("ὀδυσσεύς", odysseus.to_case(Case::Lower));
68//! ```
69//! 
70//! For the purposes of case conversion, characters followed by numerics and vice-versa are 
71//! considered word boundaries.  In addition, any special ascii characters (besides `_` and `-`) 
72//! are ignored.
73//! ```
74//! use convert_case::{Case, Casing};
75//! 
76//! assert_eq!("e_5150", "E5150".to_case(Case::Snake));
77//! assert_eq!("10,000_days", "10,000Days".to_case(Case::Snake));
78//! assert_eq!("HELLO, WORLD!", "Hello, world!".to_case(Case::Upper));
79//! assert_eq!("One\ntwo\nthree", "ONE\nTWO\nTHREE".to_case(Case::Title));
80//! ```
81//!
82//! # Note on Accuracy
83//!
84//! The `Casing` methods `from_case` and `to_case` do not fail.  Conversion to a case will always
85//! succeed.  However, the results can still be unexpected.  Failure to detect any word boundaries
86//! for a particular case means the entire string will be considered a single word.
87//! ```
88//! use convert_case::{Case, Casing};
89//!
90//! // Mistakenly parsing using Case::Snake
91//! assert_eq!("My-kebab-var", "my-kebab-var".from_case(Case::Snake).to_case(Case::Title));
92//!
93//! // Converts using an unexpected method
94//! assert_eq!("my_kebab_like_variable", "myKebab-like-variable".to_case(Case::Snake));
95//! ```
96//!
97//! # Random Feature
98//!
99//! To ensure this library had zero dependencies, randomness was moved to the _random_ feature,
100//! which requires the `rand` crate. You can enable this feature by including the 
101//! following in your `Cargo.toml`.
102//! ```{toml}
103//! [dependencies]
104//! convert_case = { version = "^0.3, features = ["random"] }
105//! ```
106//! This will add two additional cases: Random and PseudoRandom.  You can read about their
107//! construction in the [Case enum](enum.Case.html).
108
109mod case;
110mod words;
111pub use case::Case;
112use words::Words;
113
114/// Describes items that can be converted into a case.
115///
116/// Implemented for string slices `&str` and owned strings `String`.
117pub trait Casing {
118    /// References `self` and converts to the given case.
119    fn to_case(&self, case: Case) -> String;
120
121    /// Creates a `FromCasing` struct, which saves information about
122    /// how to parse `self` before converting to a case.
123    fn from_case(&self, case: Case) -> FromCasing;
124}
125
126impl Casing for str {
127    fn to_case(&self, case: Case) -> String {
128        Words::new(self).into_case(case)
129    }
130
131    fn from_case(&self, case: Case) -> FromCasing {
132        FromCasing::new(self.to_string(), case)
133    }
134}
135
136impl Casing for String {
137    fn to_case(&self, case: Case) -> String {
138        Words::new(self).into_case(case)
139    }
140
141    fn from_case(&self, case: Case) -> FromCasing {
142        FromCasing::new(self.to_string(), case)
143    }
144}
145
146/// Holds information about parsing before converting into a case.
147///
148/// This struct is used when invoking the `from_case` method on
149/// `Casing`.  `FromCasing` also implements `Casing`.
150/// ```
151/// use convert_case::{Case, Casing};
152///
153/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
154/// assert_eq!("Ninety-nine Problems", title);
155/// ```
156pub struct FromCasing {
157    name: String,
158    case: Case,
159}
160
161impl FromCasing {
162    const fn new(name: String, case: Case) -> Self {
163        Self { name, case }
164    }
165}
166
167impl Casing for FromCasing {
168    fn to_case(&self, case: Case) -> String {
169        Words::from_casing(&self.name, self.case).into_case(case)
170    }
171
172    fn from_case(&self, case: Case) -> Self {
173        Self::new(self.name.to_string(), case)
174    }
175}
176
177#[cfg(test)]
178mod test {
179    use super::*;
180    use strum::IntoEnumIterator;
181
182    #[test]
183    fn lossless_against_lossless() {
184        let examples = vec![
185            (Case::Lower, "my variable 22 name"),
186            (Case::Upper, "MY VARIABLE 22 NAME"),
187            (Case::Title, "My Variable 22 Name"),
188            (Case::Camel, "myVariable22Name"),
189            (Case::Pascal, "MyVariable22Name"),
190            (Case::Snake, "my_variable_22_name"),
191            (Case::ScreamingSnake, "MY_VARIABLE_22_NAME"),
192            (Case::Kebab, "my-variable-22-name"),
193            (Case::Cobol, "MY-VARIABLE-22-NAME"),
194            (Case::Toggle, "mY vARIABLE 22 nAME"),
195            (Case::Train, "My-Variable-22-Name"),
196            (Case::Alternating, "mY vArIaBlE 22 nAmE"),
197        ];
198
199        for (case_a, str_a) in examples.iter() {
200            for (case_b, str_b) in examples.iter() {
201                assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a))
202            }
203        }
204    }
205
206    #[test]
207    fn obvious_default_parsing() {
208        let examples = vec![
209            "SuperMario64Game",
210            "super-mario64-game",
211            "superMario64 game",
212            "Super Mario 64_game",
213            "SUPERMario 64-game",
214            "super_mario-64 game",
215        ];
216
217        for example in examples {
218            assert_eq!("super_mario_64_game", example.to_case(Case::Snake));
219        }
220    }
221
222    #[test]
223    fn multiline_strings() {
224        assert_eq!(
225            "One\ntwo\nthree",
226            "one\ntwo\nthree".to_case(Case::Title)
227        );
228    }
229
230    #[test]
231    fn camel_case_acroynms() {
232        assert_eq!(
233            "xml_http_request",
234            "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake)
235        );
236        assert_eq!(
237            "xml_http_request",
238            "XMLHttpRequest"
239                .from_case(Case::UpperCamel)
240                .to_case(Case::Snake)
241        );
242        assert_eq!(
243            "xml_http_request",
244            "XMLHttpRequest"
245                .from_case(Case::Pascal)
246                .to_case(Case::Snake)
247        );
248    }
249
250    #[test]
251    fn leading_tailing_delimeters() {
252        assert_eq!(
253            "leading_underscore",
254            "_leading_underscore"
255                .from_case(Case::Snake)
256                .to_case(Case::Snake)
257        );
258        assert_eq!(
259            "tailing_underscore",
260            "tailing_underscore_"
261                .from_case(Case::Snake)
262                .to_case(Case::Snake)
263        );
264        assert_eq!(
265            "leading_hyphen",
266            "-leading-hyphen"
267                .from_case(Case::Kebab)
268                .to_case(Case::Snake)
269        );
270        assert_eq!(
271            "tailing_hyphen",
272            "tailing-hyphen-"
273                .from_case(Case::Kebab)
274                .to_case(Case::Snake)
275        );
276    }
277
278    #[test]
279    fn double_delimeters() {
280        assert_eq!(
281            "many_underscores",
282            "many___underscores"
283                .from_case(Case::Snake)
284                .to_case(Case::Snake)
285        );
286        assert_eq!(
287            "many-underscores",
288            "many---underscores"
289                .from_case(Case::Kebab)
290                .to_case(Case::Kebab)
291        );
292    }
293
294    #[test]
295    fn early_word_boundaries() {
296        assert_eq!(
297            "a_bagel",
298            "aBagel".from_case(Case::Camel).to_case(Case::Snake)
299        );
300    }
301
302    #[test]
303    fn late_word_boundaries() {
304        assert_eq!(
305            "team_a",
306            "teamA".from_case(Case::Camel).to_case(Case::Snake)
307        );
308    }
309
310    #[test]
311    fn empty_string() {
312        for (case_a, case_b) in Case::iter().zip(Case::iter()) {
313            assert_eq!("", "".from_case(case_a).to_case(case_b));
314        }
315    }
316
317    #[test]
318    fn owned_string() {
319        assert_eq!(
320            "test_variable",
321            String::from("TestVariable").to_case(Case::Snake)
322        )
323    }
324
325    #[test]
326    fn default_all_boundaries() {
327        assert_eq!(
328            "abc_abc_abc_abc_abc_abc",
329            "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake)
330        );
331    }
332
333    #[test]
334    fn alternating_ignore_symbols() {
335        assert_eq!("tHaT's", "that's".to_case(Case::Alternating));
336    }
337}