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}
338