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