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 | |
109 | mod case; |
110 | mod words; |
111 | pub use case::Case; |
112 | use words::Words; |
113 | |
114 | /// Describes items that can be converted into a case. |
115 | /// |
116 | /// Implemented for string slices `&str` and owned strings `String`. |
117 | pub 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 | |
126 | impl 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 | |
136 | impl 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 | /// ``` |
156 | pub struct FromCasing { |
157 | name: String, |
158 | case: Case, |
159 | } |
160 | |
161 | impl FromCasing { |
162 | const fn new(name: String, case: Case) -> Self { |
163 | Self { name, case } |
164 | } |
165 | } |
166 | |
167 | impl 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)] |
178 | mod 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 | |