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