Skip to content

Commit 8f3319e

Browse files
authored
Merge pull request RustPython#827 from RustPython/joey/titlecase
str: improve {is,}title impl and add tests
2 parents c0d328a + 762c281 commit 8f3319e

File tree

1 file changed

+106
-20
lines changed

1 file changed

+106
-20
lines changed

vm/src/obj/objstr.rs

Lines changed: 106 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ impl PyString {
4343
}
4444
}
4545

46+
impl From<&str> for PyString {
47+
fn from(s: &str) -> PyString {
48+
PyString {
49+
value: s.to_string(),
50+
}
51+
}
52+
}
53+
4654
pub type PyStringRef = PyRef<PyString>;
4755

4856
impl fmt::Display for PyString {
@@ -396,9 +404,33 @@ impl PyString {
396404
}
397405
}
398406

407+
/// Return a titlecased version of the string where words start with an
408+
/// uppercase character and the remaining characters are lowercase.
399409
#[pymethod]
400410
fn title(&self, _vm: &VirtualMachine) -> String {
401-
make_title(&self.value)
411+
let mut title = String::new();
412+
let mut previous_is_cased = false;
413+
for c in self.value.chars() {
414+
if c.is_lowercase() {
415+
if !previous_is_cased {
416+
title.extend(c.to_uppercase());
417+
} else {
418+
title.push(c);
419+
}
420+
previous_is_cased = true;
421+
} else if c.is_uppercase() {
422+
if previous_is_cased {
423+
title.extend(c.to_lowercase());
424+
} else {
425+
title.push(c);
426+
}
427+
previous_is_cased = true;
428+
} else {
429+
previous_is_cased = false;
430+
title.push(c);
431+
}
432+
}
433+
title
402434
}
403435

404436
#[pymethod]
@@ -609,13 +641,34 @@ impl PyString {
609641
vm.ctx.new_tuple(new_tup)
610642
}
611643

644+
/// Return `true` if the sequence is ASCII titlecase and the sequence is not
645+
/// empty, `false` otherwise.
612646
#[pymethod]
613647
fn istitle(&self, _vm: &VirtualMachine) -> bool {
614648
if self.value.is_empty() {
615-
false
616-
} else {
617-
self.value.split(' ').all(|word| word == make_title(word))
649+
return false;
618650
}
651+
652+
let mut cased = false;
653+
let mut previous_is_cased = false;
654+
for c in self.value.chars() {
655+
if c.is_uppercase() {
656+
if previous_is_cased {
657+
return false;
658+
}
659+
previous_is_cased = true;
660+
cased = true;
661+
} else if c.is_lowercase() {
662+
if !previous_is_cased {
663+
return false;
664+
}
665+
previous_is_cased = true;
666+
cased = true;
667+
} else {
668+
previous_is_cased = false;
669+
}
670+
}
671+
cased
619672
}
620673

621674
#[pymethod]
@@ -981,22 +1034,55 @@ fn adjust_indices(
9811034
}
9821035
}
9831036

984-
// helper function to title strings
985-
fn make_title(s: &str) -> String {
986-
let mut titled_str = String::new();
987-
let mut capitalize_char: bool = true;
988-
for c in s.chars() {
989-
if c.is_alphabetic() {
990-
if !capitalize_char {
991-
titled_str.push(c);
992-
} else if capitalize_char {
993-
titled_str.push(c.to_ascii_uppercase());
994-
capitalize_char = false;
995-
}
996-
} else {
997-
titled_str.push(c);
998-
capitalize_char = true;
1037+
#[cfg(test)]
1038+
mod tests {
1039+
use super::*;
1040+
1041+
#[test]
1042+
fn str_title() {
1043+
let vm = VirtualMachine::new();
1044+
1045+
let tests = vec![
1046+
(" Hello ", " hello "),
1047+
("Hello ", "hello "),
1048+
("Hello ", "Hello "),
1049+
("Format This As Title String", "fOrMaT thIs aS titLe String"),
1050+
("Format,This-As*Title;String", "fOrMaT,thIs-aS*titLe;String"),
1051+
("Getint", "getInt"),
1052+
("Greek Ωppercases ...", "greek ωppercases ..."),
1053+
];
1054+
for (title, input) in tests {
1055+
assert_eq!(PyString::from(input).title(&vm).as_str(), title);
1056+
}
1057+
}
1058+
1059+
#[test]
1060+
fn str_istitle() {
1061+
let vm = VirtualMachine::new();
1062+
1063+
let pos = vec![
1064+
"A",
1065+
"A Titlecased Line",
1066+
"A\nTitlecased Line",
1067+
"A Titlecased, Line",
1068+
"Greek Ωppercases ...",
1069+
];
1070+
1071+
for s in pos {
1072+
assert!(PyString::from(s).istitle(&vm));
1073+
}
1074+
1075+
let neg = vec![
1076+
"",
1077+
"a",
1078+
"\n",
1079+
"Not a capitalized String",
1080+
"Not\ta Titlecase String",
1081+
"Not--a Titlecase String",
1082+
"NOT",
1083+
];
1084+
for s in neg {
1085+
assert!(!PyString::from(s).istitle(&vm));
9991086
}
10001087
}
1001-
titled_str
10021088
}

0 commit comments

Comments
 (0)