convert_case/lib.rs
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
230mod boundary;
231mod case;
232mod converter;
233mod pattern;
234
235pub use boundary::{split, Boundary};
236pub use case::Case;
237pub use converter::Converter;
238pub 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`.
243pub 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
313impl<T: AsRef<str>> Casing<T> for T
314where
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/// ```
351pub struct StateConverter<'a, T: AsRef<str>> {
352 s: &'a T,
353 conv: Converter,
354}
355
356impl<'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)]
447mod 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}