1 | //! Converts to and from various cases. |
2 | //! |
3 | //! # Command Line Utility `ccase` |
4 | //! |
5 | //! This library was developed for the purposes of a command line utility for converting |
6 | //! the case of strings and filenames. You can check out |
7 | //! [`ccase` on Github](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 | //! Strings have implemented the [`Casing`](trait.Casing.html) trait, which adds methods for |
13 | //! case conversion. |
14 | //! |
15 | //! You can convert strings into a case using the [`to_case`](Casing::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 a set of default word boundaries, that is |
25 | //! * space characters ` `, |
26 | //! * underscores `_`, |
27 | //! * hyphens `-`, |
28 | //! * changes in capitalization from lowercase to uppercase `aA`, |
29 | //! * adjacent digits and letters `a1`, `1a`, `A1`, `1A`, |
30 | //! * and acroynms `AAa` (as in `HTTPRequest`). |
31 | //! |
32 | //! For more accuracy, the `from_case` method splits based on the word boundaries |
33 | //! of a particular case. For example, splitting from snake case will only use |
34 | //! underscores as word boundaries. |
35 | //! ``` |
36 | //! use convert_case::{Case, Casing}; |
37 | //! |
38 | //! assert_eq!( |
39 | //! "2020 04 16 My Cat Cali" , |
40 | //! "2020-04-16_my_cat_cali" .to_case(Case::Title) |
41 | //! ); |
42 | //! assert_eq!( |
43 | //! "2020-04-16 My Cat Cali" , |
44 | //! "2020-04-16_my_cat_cali" .from_case(Case::Snake).to_case(Case::Title) |
45 | //! ); |
46 | //! ``` |
47 | //! |
48 | //! Case conversion can detect acronyms for camel-like strings. It also ignores any leading, |
49 | //! trailing, or duplicate delimiters. |
50 | //! ``` |
51 | //! use convert_case::{Case, Casing}; |
52 | //! |
53 | //! assert_eq!("io_stream" , "IOStream" .to_case(Case::Snake)); |
54 | //! assert_eq!("my_json_parser" , "myJSONParser" .to_case(Case::Snake)); |
55 | //! |
56 | //! assert_eq!("weird_var_name" , "__weird--var _name-" .to_case(Case::Snake)); |
57 | //! ``` |
58 | //! |
59 | //! It also works non-ascii characters. However, no inferences on the language itself is made. |
60 | //! For instance, the digraph `ij` in Dutch will not be capitalized, because it is represented |
61 | //! as two distinct Unicode characters. However, `æ` would be capitalized. Accuracy with unicode |
62 | //! characters is done using the `unicode-segmentation` crate, the sole dependency of this crate. |
63 | //! ``` |
64 | //! use convert_case::{Case, Casing}; |
65 | //! |
66 | //! assert_eq!("granat-äpfel" , "GranatÄpfel" .to_case(Case::Kebab)); |
67 | //! assert_eq!("Перспектива 24" , "ПЕРСПЕКТИВА24" .to_case(Case::Title)); |
68 | //! |
69 | //! // The example from str::to_lowercase documentation |
70 | //! let odysseus = "ὈΔΥΣΣΕΎΣ" ; |
71 | //! assert_eq!("ὀδυσσεύς" , odysseus.to_case(Case::Lower)); |
72 | //! ``` |
73 | //! |
74 | //! By default, characters followed by digits and vice-versa are |
75 | //! considered word boundaries. In addition, any special ASCII characters (besides `_` and `-`) |
76 | //! are ignored. |
77 | //! ``` |
78 | //! use convert_case::{Case, Casing}; |
79 | //! |
80 | //! assert_eq!("e_5150" , "E5150" .to_case(Case::Snake)); |
81 | //! assert_eq!("10,000_days" , "10,000Days" .to_case(Case::Snake)); |
82 | //! assert_eq!("HELLO, WORLD!" , "Hello, world!" .to_case(Case::Upper)); |
83 | //! assert_eq!("One \ntwo \nthree" , "ONE \nTWO \nTHREE" .to_case(Case::Title)); |
84 | //! ``` |
85 | //! |
86 | //! You can also test what case a string is in. |
87 | //! ``` |
88 | //! use convert_case::{Case, Casing}; |
89 | //! |
90 | //! assert!( "css-class-name" .is_case(Case::Kebab)); |
91 | //! assert!(!"css-class-name" .is_case(Case::Snake)); |
92 | //! assert!(!"UPPER_CASE_VAR" .is_case(Case::Snake)); |
93 | //! ``` |
94 | //! |
95 | //! # Note on Accuracy |
96 | //! |
97 | //! The `Casing` methods `from_case` and `to_case` do not fail. Conversion to a case will always |
98 | //! succeed. However, the results can still be unexpected. Failure to detect any word boundaries |
99 | //! for a particular case means the entire string will be considered a single word. |
100 | //! ``` |
101 | //! use convert_case::{Case, Casing}; |
102 | //! |
103 | //! // Mistakenly parsing using Case::Snake |
104 | //! assert_eq!("My-kebab-var" , "my-kebab-var" .from_case(Case::Snake).to_case(Case::Title)); |
105 | //! |
106 | //! // Converts using an unexpected method |
107 | //! assert_eq!("my_kebab_like_variable" , "myKebab-like-variable" .to_case(Case::Snake)); |
108 | //! ``` |
109 | //! |
110 | //! # Boundary Specificity |
111 | //! |
112 | //! It can be difficult to determine how to split a string into words. That is why this case |
113 | //! provides the [`from_case`](Casing::from_case) functionality, but sometimes that isn't enough |
114 | //! to meet a specific use case. |
115 | //! |
116 | //! Take an identifier has the word `2D`, such as `scale2D`. No exclusive usage of `from_case` will |
117 | //! be enough to solve the problem. In this case we can further specify which boundaries to split |
118 | //! the string on. `convert_case` provides some patterns for achieving this specificity. |
119 | //! We can specify what boundaries we want to split on using the [`Boundary` enum](Boundary). |
120 | //! ``` |
121 | //! use convert_case::{Boundary, Case, Casing}; |
122 | //! |
123 | //! // Not quite what we want |
124 | //! assert_eq!( |
125 | //! "scale_2_d" , |
126 | //! "scale2D" |
127 | //! .from_case(Case::Camel) |
128 | //! .to_case(Case::Snake) |
129 | //! ); |
130 | //! |
131 | //! // Remove boundary from Case::Camel |
132 | //! assert_eq!( |
133 | //! "scale_2d" , |
134 | //! "scale2D" |
135 | //! .from_case(Case::Camel) |
136 | //! .without_boundaries(&[Boundary::DigitUpper, Boundary::DigitLower]) |
137 | //! .to_case(Case::Snake) |
138 | //! ); |
139 | //! |
140 | //! // Write boundaries explicitly |
141 | //! assert_eq!( |
142 | //! "scale_2d" , |
143 | //! "scale2D" |
144 | //! .with_boundaries(&[Boundary::LowerDigit]) |
145 | //! .to_case(Case::Snake) |
146 | //! ); |
147 | //! ``` |
148 | //! |
149 | //! The `Casing` trait provides initial methods, but any subsequent methods that do not resolve |
150 | //! the conversion return a [`StateConverter`] struct. It contains similar methods as `Casing`. |
151 | //! |
152 | //! # Custom Cases |
153 | //! |
154 | //! Because `Case` is an enum, you can't create your own variant for your use case. However |
155 | //! the parameters for case conversion have been encapsulated into the [`Converter`] struct |
156 | //! which can be used for specific use cases. |
157 | //! |
158 | //! Suppose you wanted to format a word like camel case, where the first word is lower case and the |
159 | //! rest are capitalized. But you want to include a delimeter like underscore. This case isn't |
160 | //! available as a `Case` variant, but you can create it by constructing the parameters of the |
161 | //! `Converter`. |
162 | //! ``` |
163 | //! use convert_case::{Case, Casing, Converter, Pattern}; |
164 | //! |
165 | //! let conv = Converter::new() |
166 | //! .set_pattern(Pattern::Camel) |
167 | //! .set_delim("_" ); |
168 | //! |
169 | //! assert_eq!( |
170 | //! "my_Special_Case" , |
171 | //! conv.convert("My Special Case" ) |
172 | //! ) |
173 | //! ``` |
174 | //! Just as with the `Casing` trait, you can also manually set the boundaries strings are split |
175 | //! on. You can use any of the [`Pattern`] variants available. This even includes [`Pattern::Sentence`] |
176 | //! which isn't used in any `Case` variant. You can also set no pattern at all, which will |
177 | //! maintain the casing of each letter in the input string. You can also, of course, set any string as your |
178 | //! delimeter. |
179 | //! |
180 | //! For more details on how strings are converted, see the docs for [`Converter`]. |
181 | //! |
182 | //! # Random Feature |
183 | //! |
184 | //! To ensure this library had zero dependencies, randomness was moved to the _random_ feature, |
185 | //! which requires the `rand` crate. You can enable this feature by including the |
186 | //! following in your `Cargo.toml`. |
187 | //! ```{toml} |
188 | //! [dependencies] |
189 | //! convert_case = { version = "^0.3.0", features = ["random"] } |
190 | //! ``` |
191 | //! This will add two additional cases: Random and PseudoRandom. You can read about their |
192 | //! construction in the [Case enum](enum.Case.html). |
193 | |
194 | mod case; |
195 | mod converter; |
196 | mod pattern; |
197 | mod segmentation; |
198 | |
199 | pub use case::Case; |
200 | pub use converter::Converter; |
201 | pub use pattern::Pattern; |
202 | pub use segmentation::Boundary; |
203 | |
204 | /// Describes items that can be converted into a case. This trait is used |
205 | /// in conjunction with the [`StateConverter`] struct which is returned from a couple |
206 | /// methods on `Casing`. |
207 | /// |
208 | /// Implemented for strings `&str`, `String`, and `&String`. |
209 | pub trait Casing<T: AsRef<str>> { |
210 | |
211 | /// Convert the string into the given case. It will reference `self` and create a new |
212 | /// `String` with the same pattern and delimeter as `case`. It will split on boundaries |
213 | /// defined at [`Boundary::defaults()`]. |
214 | /// ``` |
215 | /// use convert_case::{Case, Casing}; |
216 | /// |
217 | /// assert_eq!( |
218 | /// "tetronimo-piece-border" , |
219 | /// "Tetronimo piece border" .to_case(Case::Kebab) |
220 | /// ); |
221 | /// ``` |
222 | fn to_case(&self, case: Case) -> String; |
223 | |
224 | /// Start the case conversion by storing the boundaries associated with the given case. |
225 | /// ``` |
226 | /// use convert_case::{Case, Casing}; |
227 | /// |
228 | /// assert_eq!( |
229 | /// "2020-08-10_dannie_birthday" , |
230 | /// "2020-08-10 Dannie Birthday" |
231 | /// .from_case(Case::Title) |
232 | /// .to_case(Case::Snake) |
233 | /// ); |
234 | /// ``` |
235 | #[allow (clippy::wrong_self_convention)] |
236 | fn from_case(&self, case: Case) -> StateConverter<T>; |
237 | |
238 | /// Creates a `StateConverter` struct initialized with the boundaries |
239 | /// provided. |
240 | /// ``` |
241 | /// use convert_case::{Boundary, Case, Casing}; |
242 | /// |
243 | /// assert_eq!( |
244 | /// "e1_m1_hangar" , |
245 | /// "E1M1 Hangar" |
246 | /// .with_boundaries(&[Boundary::DigitUpper, Boundary::Space]) |
247 | /// .to_case(Case::Snake) |
248 | /// ); |
249 | /// ``` |
250 | fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T>; |
251 | |
252 | /// Determines if `self` is of the given case. This is done simply by applying |
253 | /// the conversion and seeing if the result is the same. |
254 | /// ``` |
255 | /// use convert_case::{Case, Casing}; |
256 | /// |
257 | /// assert!( "kebab-case-string" .is_case(Case::Kebab)); |
258 | /// assert!( "Train-Case-String" .is_case(Case::Train)); |
259 | /// |
260 | /// assert!(!"kebab-case-string" .is_case(Case::Snake)); |
261 | /// assert!(!"kebab-case-string" .is_case(Case::Train)); |
262 | /// ``` |
263 | fn is_case(&self, case: Case) -> bool; |
264 | } |
265 | |
266 | impl<T: AsRef<str>> Casing<T> for T |
267 | where |
268 | String: PartialEq<T>, |
269 | { |
270 | fn to_case(&self, case: Case) -> String { |
271 | StateConverter::new(self).to_case(case) |
272 | } |
273 | |
274 | fn with_boundaries(&self, bs: &[Boundary]) -> StateConverter<T> { |
275 | StateConverter::new(self).with_boundaries(bs) |
276 | } |
277 | |
278 | fn from_case(&self, case: Case) -> StateConverter<T> { |
279 | StateConverter::new_from_case(self, case) |
280 | } |
281 | |
282 | fn is_case(&self, case: Case) -> bool { |
283 | &self.to_case(case) == self |
284 | } |
285 | } |
286 | |
287 | /// Holds information about parsing before converting into a case. |
288 | /// |
289 | /// This struct is used when invoking the `from_case` and `with_boundaries` methods on |
290 | /// `Casing`. For a more fine grained approach to case conversion, consider using the [`Converter`] |
291 | /// struct. |
292 | /// ``` |
293 | /// use convert_case::{Case, Casing}; |
294 | /// |
295 | /// let title = "ninety-nine_problems" .from_case(Case::Snake).to_case(Case::Title); |
296 | /// assert_eq!("Ninety-nine Problems" , title); |
297 | /// ``` |
298 | pub struct StateConverter<'a, T: AsRef<str>> { |
299 | s: &'a T, |
300 | conv: Converter, |
301 | } |
302 | |
303 | impl<'a, T: AsRef<str>> StateConverter<'a, T> { |
304 | /// Only called by Casing function to_case() |
305 | fn new(s: &'a T) -> Self { |
306 | Self { |
307 | s, |
308 | conv: Converter::new(), |
309 | } |
310 | } |
311 | |
312 | /// Only called by Casing function from_case() |
313 | fn new_from_case(s: &'a T, case: Case) -> Self { |
314 | Self { |
315 | s, |
316 | conv: Converter::new().from_case(case), |
317 | } |
318 | } |
319 | |
320 | /// Uses the boundaries associated with `case` for word segmentation. This |
321 | /// will overwrite any boundary information initialized before. This method is |
322 | /// likely not useful, but provided anyway. |
323 | /// ``` |
324 | /// use convert_case::{Case, Casing}; |
325 | /// |
326 | /// let name = "Chuck Schuldiner" |
327 | /// .from_case(Case::Snake) // from Casing trait |
328 | /// .from_case(Case::Title) // from StateConverter, overwrites previous |
329 | /// .to_case(Case::Kebab); |
330 | /// assert_eq!("chuck-schuldiner" , name); |
331 | /// ``` |
332 | pub fn from_case(self, case: Case) -> Self { |
333 | Self { |
334 | conv: self.conv.from_case(case), |
335 | ..self |
336 | } |
337 | } |
338 | |
339 | /// Overwrites boundaries for word segmentation with those provided. This will overwrite |
340 | /// any boundary information initialized before. This method is likely not useful, but |
341 | /// provided anyway. |
342 | /// ``` |
343 | /// use convert_case::{Boundary, Case, Casing}; |
344 | /// |
345 | /// let song = "theHumbling river-puscifer" |
346 | /// .from_case(Case::Kebab) // from Casing trait |
347 | /// .with_boundaries(&[Boundary::Space, Boundary::LowerUpper]) // overwrites `from_case` |
348 | /// .to_case(Case::Pascal); |
349 | /// assert_eq!("TheHumblingRiver-puscifer" , song); // doesn't split on hyphen `-` |
350 | /// ``` |
351 | pub fn with_boundaries(self, bs: &[Boundary]) -> Self { |
352 | Self { |
353 | s: self.s, |
354 | conv: self.conv.set_boundaries(bs), |
355 | } |
356 | } |
357 | |
358 | /// Removes any boundaries that were already initialized. This is particularly useful when a |
359 | /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude |
360 | /// some. |
361 | /// ``` |
362 | /// use convert_case::{Boundary, Case, Casing}; |
363 | /// |
364 | /// assert_eq!( |
365 | /// "2d_transformation" , |
366 | /// "2dTransformation" |
367 | /// .from_case(Case::Camel) |
368 | /// .without_boundaries(&Boundary::digits()) |
369 | /// .to_case(Case::Snake) |
370 | /// ); |
371 | /// ``` |
372 | pub fn without_boundaries(self, bs: &[Boundary]) -> Self { |
373 | Self { |
374 | s: self.s, |
375 | conv: self.conv.remove_boundaries(bs), |
376 | } |
377 | } |
378 | |
379 | /// Consumes the `StateConverter` and returns the converted string. |
380 | /// ``` |
381 | /// use convert_case::{Boundary, Case, Casing}; |
382 | /// |
383 | /// assert_eq!( |
384 | /// "ice-cream social" , |
385 | /// "Ice-Cream Social" .from_case(Case::Title).to_case(Case::Lower) |
386 | /// ); |
387 | /// ``` |
388 | pub fn to_case(self, case: Case) -> String { |
389 | self.conv.to_case(case).convert(self.s) |
390 | } |
391 | } |
392 | |
393 | #[cfg (test)] |
394 | mod test { |
395 | use super::*; |
396 | use strum::IntoEnumIterator; |
397 | |
398 | fn possible_cases(s: &str) -> Vec<Case> { |
399 | Case::deterministic_cases() |
400 | .into_iter() |
401 | .filter(|case| s.from_case(*case).to_case(*case) == s) |
402 | .collect() |
403 | } |
404 | |
405 | #[test ] |
406 | fn lossless_against_lossless() { |
407 | let examples = vec![ |
408 | (Case::Lower, "my variable 22 name" ), |
409 | (Case::Upper, "MY VARIABLE 22 NAME" ), |
410 | (Case::Title, "My Variable 22 Name" ), |
411 | (Case::Camel, "myVariable22Name" ), |
412 | (Case::Pascal, "MyVariable22Name" ), |
413 | (Case::Snake, "my_variable_22_name" ), |
414 | (Case::UpperSnake, "MY_VARIABLE_22_NAME" ), |
415 | (Case::Kebab, "my-variable-22-name" ), |
416 | (Case::Cobol, "MY-VARIABLE-22-NAME" ), |
417 | (Case::Toggle, "mY vARIABLE 22 nAME" ), |
418 | (Case::Train, "My-Variable-22-Name" ), |
419 | (Case::Alternating, "mY vArIaBlE 22 nAmE" ), |
420 | ]; |
421 | |
422 | for (case_a, str_a) in examples.iter() { |
423 | for (case_b, str_b) in examples.iter() { |
424 | assert_eq!(*str_a, str_b.from_case(*case_b).to_case(*case_a)) |
425 | } |
426 | } |
427 | } |
428 | |
429 | #[test ] |
430 | fn obvious_default_parsing() { |
431 | let examples = vec![ |
432 | "SuperMario64Game" , |
433 | "super-mario64-game" , |
434 | "superMario64 game" , |
435 | "Super Mario 64_game" , |
436 | "SUPERMario 64-game" , |
437 | "super_mario-64 game" , |
438 | ]; |
439 | |
440 | for example in examples { |
441 | assert_eq!("super_mario_64_game" , example.to_case(Case::Snake)); |
442 | } |
443 | } |
444 | |
445 | #[test ] |
446 | fn multiline_strings() { |
447 | assert_eq!("One \ntwo \nthree" , "one \ntwo \nthree" .to_case(Case::Title)); |
448 | } |
449 | |
450 | #[test ] |
451 | fn camel_case_acroynms() { |
452 | assert_eq!( |
453 | "xml_http_request" , |
454 | "XMLHttpRequest" .from_case(Case::Camel).to_case(Case::Snake) |
455 | ); |
456 | assert_eq!( |
457 | "xml_http_request" , |
458 | "XMLHttpRequest" |
459 | .from_case(Case::UpperCamel) |
460 | .to_case(Case::Snake) |
461 | ); |
462 | assert_eq!( |
463 | "xml_http_request" , |
464 | "XMLHttpRequest" |
465 | .from_case(Case::Pascal) |
466 | .to_case(Case::Snake) |
467 | ); |
468 | } |
469 | |
470 | #[test ] |
471 | fn leading_tailing_delimeters() { |
472 | assert_eq!( |
473 | "leading_underscore" , |
474 | "_leading_underscore" |
475 | .from_case(Case::Snake) |
476 | .to_case(Case::Snake) |
477 | ); |
478 | assert_eq!( |
479 | "tailing_underscore" , |
480 | "tailing_underscore_" |
481 | .from_case(Case::Snake) |
482 | .to_case(Case::Snake) |
483 | ); |
484 | assert_eq!( |
485 | "leading_hyphen" , |
486 | "-leading-hyphen" |
487 | .from_case(Case::Kebab) |
488 | .to_case(Case::Snake) |
489 | ); |
490 | assert_eq!( |
491 | "tailing_hyphen" , |
492 | "tailing-hyphen-" |
493 | .from_case(Case::Kebab) |
494 | .to_case(Case::Snake) |
495 | ); |
496 | } |
497 | |
498 | #[test ] |
499 | fn double_delimeters() { |
500 | assert_eq!( |
501 | "many_underscores" , |
502 | "many___underscores" |
503 | .from_case(Case::Snake) |
504 | .to_case(Case::Snake) |
505 | ); |
506 | assert_eq!( |
507 | "many-underscores" , |
508 | "many---underscores" |
509 | .from_case(Case::Kebab) |
510 | .to_case(Case::Kebab) |
511 | ); |
512 | } |
513 | |
514 | #[test ] |
515 | fn early_word_boundaries() { |
516 | assert_eq!( |
517 | "a_bagel" , |
518 | "aBagel" .from_case(Case::Camel).to_case(Case::Snake) |
519 | ); |
520 | } |
521 | |
522 | #[test ] |
523 | fn late_word_boundaries() { |
524 | assert_eq!( |
525 | "team_a" , |
526 | "teamA" .from_case(Case::Camel).to_case(Case::Snake) |
527 | ); |
528 | } |
529 | |
530 | #[test ] |
531 | fn empty_string() { |
532 | for (case_a, case_b) in Case::iter().zip(Case::iter()) { |
533 | assert_eq!("" , "" .from_case(case_a).to_case(case_b)); |
534 | } |
535 | } |
536 | |
537 | #[test ] |
538 | fn owned_string() { |
539 | assert_eq!( |
540 | "test_variable" , |
541 | String::from("TestVariable" ).to_case(Case::Snake) |
542 | ) |
543 | } |
544 | |
545 | #[test ] |
546 | fn default_all_boundaries() { |
547 | assert_eq!( |
548 | "abc_abc_abc_abc_abc_abc" , |
549 | "ABC-abc_abcAbc ABCAbc" .to_case(Case::Snake) |
550 | ); |
551 | } |
552 | |
553 | #[test ] |
554 | fn alternating_ignore_symbols() { |
555 | assert_eq!("tHaT's" , "that's" .to_case(Case::Alternating)); |
556 | } |
557 | |
558 | #[test ] |
559 | fn string_is_snake() { |
560 | assert!("im_snake_case" .is_case(Case::Snake)); |
561 | assert!(!"im_NOTsnake_case" .is_case(Case::Snake)); |
562 | } |
563 | |
564 | #[test ] |
565 | fn string_is_kebab() { |
566 | assert!("im-kebab-case" .is_case(Case::Kebab)); |
567 | assert!(!"im_not_kebab" .is_case(Case::Kebab)); |
568 | } |
569 | |
570 | #[test ] |
571 | fn remove_boundaries() { |
572 | assert_eq!( |
573 | "m02_s05_binary_trees.pdf" , |
574 | "M02S05BinaryTrees.pdf" |
575 | .from_case(Case::Pascal) |
576 | .without_boundaries(&[Boundary::UpperDigit]) |
577 | .to_case(Case::Snake) |
578 | ); |
579 | } |
580 | |
581 | #[test ] |
582 | fn with_boundaries() { |
583 | assert_eq!( |
584 | "my-dumb-file-name" , |
585 | "my_dumbFileName" |
586 | .with_boundaries(&[Boundary::Underscore, Boundary::LowerUpper]) |
587 | .to_case(Case::Kebab) |
588 | ); |
589 | } |
590 | |
591 | #[cfg (feature = "random" )] |
592 | #[test ] |
593 | fn random_case_boundaries() { |
594 | for random_case in Case::random_cases() { |
595 | assert_eq!( |
596 | "split_by_spaces" , |
597 | "Split By Spaces" |
598 | .from_case(random_case) |
599 | .to_case(Case::Snake) |
600 | ); |
601 | } |
602 | } |
603 | |
604 | #[test ] |
605 | fn multiple_from_case() { |
606 | assert_eq!( |
607 | "longtime_nosee" , |
608 | "LongTime NoSee" |
609 | .from_case(Case::Camel) |
610 | .from_case(Case::Title) |
611 | .to_case(Case::Snake), |
612 | ) |
613 | } |
614 | |
615 | use std::collections::HashSet; |
616 | use std::iter::FromIterator; |
617 | |
618 | #[test ] |
619 | fn detect_many_cases() { |
620 | let lower_cases_vec = possible_cases(&"asef" ); |
621 | let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter()); |
622 | let mut actual = HashSet::new(); |
623 | actual.insert(Case::Lower); |
624 | actual.insert(Case::Camel); |
625 | actual.insert(Case::Snake); |
626 | actual.insert(Case::Kebab); |
627 | actual.insert(Case::Flat); |
628 | assert_eq!(lower_cases_set, actual); |
629 | |
630 | let lower_cases_vec = possible_cases(&"asefCase" ); |
631 | let lower_cases_set = HashSet::from_iter(lower_cases_vec.into_iter()); |
632 | let mut actual = HashSet::new(); |
633 | actual.insert(Case::Camel); |
634 | assert_eq!(lower_cases_set, actual); |
635 | } |
636 | |
637 | #[test ] |
638 | fn detect_each_case() { |
639 | let s = "My String Identifier" .to_string(); |
640 | for case in Case::deterministic_cases() { |
641 | let new_s = s.from_case(case).to_case(case); |
642 | let possible = possible_cases(&new_s); |
643 | println!(" {} {:?} {:?}" , new_s, case, possible); |
644 | assert!(possible.iter().any(|c| c == &case)); |
645 | } |
646 | } |
647 | |
648 | // From issue https://github.com/rutrum/convert-case/issues/8 |
649 | #[test ] |
650 | fn accent_mark() { |
651 | let s = "música moderna" .to_string(); |
652 | assert_eq!("MúsicaModerna" , s.to_case(Case::Pascal)); |
653 | } |
654 | |
655 | // From issue https://github.com/rutrum/convert-case/issues/4 |
656 | #[test ] |
657 | fn russian() { |
658 | let s = "ПЕРСПЕКТИВА24" .to_string(); |
659 | let _n = s.to_case(Case::Title); |
660 | } |
661 | } |
662 | |