@@ -759,7 +759,9 @@ impl PyStr {
759759 let mut chars = s. char_indices ( ) ;
760760 let mut out = VecFmtWriter ( Vec :: with_capacity ( s. len ( ) ) ) ;
761761 titlecase_first ( s, & mut chars, & mut out) ;
762- lowercase_and_sigma ( s, & mut chars, & mut out) ;
762+ for ( i, ch) in chars {
763+ lowercase_or_sigma ( ch, s, i, & mut out) ;
764+ }
763765 unsafe { Wtf8Buf :: from_bytes_unchecked ( out. 0 ) }
764766 }
765767 PyKindStr :: Wtf8 ( s) => {
@@ -770,15 +772,18 @@ impl PyStr {
770772 let s = first. valid ( ) ;
771773 let mut chars = s. char_indices ( ) ;
772774 titlecase_first ( s, & mut chars, & mut out) ;
773- lowercase_and_sigma ( s, & mut chars, & mut out) ;
775+ for ( i, ch) in chars {
776+ lowercase_or_sigma ( ch, s, i, & mut out) ;
777+ }
774778 out. 0 . extend ( first. invalid ( ) ) ;
775779 }
776780 // This loop is only hit if the WTF-8 buffer contains invalid Unicode. Otherwise,
777781 // everything is handled above without chunking.
778782 for chunk in chunks {
779783 let s = chunk. valid ( ) ;
780- let mut chars = s. char_indices ( ) ;
781- lowercase_and_sigma ( s, & mut chars, & mut out) ;
784+ for ( i, ch) in s. char_indices ( ) {
785+ lowercase_or_sigma ( ch, s, i, & mut out) ;
786+ }
782787 out. 0 . extend ( chunk. invalid ( ) ) ;
783788 }
784789
@@ -1053,28 +1058,22 @@ impl PyStr {
10531058 PyKindStr :: Ascii ( _) => unsafe {
10541059 Wtf8Buf :: from_bytes_unchecked ( crate :: bytes_inner:: title_ascii ( self . as_bytes ( ) ) )
10551060 } ,
1056- PyKindStr :: Utf8 ( s) => TitlecaseMapper :: new ( )
1057- . titlecase_segment_to_string ( s, & LanguageIdentifier :: UNKNOWN , Default :: default ( ) )
1058- . to_string ( )
1059- . into ( ) ,
1061+ PyKindStr :: Utf8 ( s) => {
1062+ let mut out = VecFmtWriter ( Vec :: with_capacity ( s. len ( ) ) ) ;
1063+ titlecase_string ( s, & mut out) ;
1064+ // SAFETY: `s` is valid UTF-8 and titlecase_string only works on Unicode.
1065+ unsafe { Wtf8Buf :: from_bytes_unchecked ( out. 0 ) }
1066+ }
10601067 PyKindStr :: Wtf8 ( s) => {
1061- let mut buf = VecFmtWriter ( Vec :: with_capacity ( s. len ( ) ) ) ;
1062- let mapper = TitlecaseMapper :: new ( ) ;
1068+ let mut out = VecFmtWriter ( Vec :: with_capacity ( s. len ( ) ) ) ;
10631069 for chunk in s. as_bytes ( ) . utf8_chunks ( ) {
1064- mapper
1065- . titlecase_segment (
1066- chunk. valid ( ) ,
1067- & LanguageIdentifier :: UNKNOWN ,
1068- Default :: default ( ) ,
1069- )
1070- . write_to ( & mut buf)
1071- . expect ( "Writing to an in-memory buffer cannot fail." ) ;
1072- buf. 0 . extend ( chunk. invalid ( ) ) ;
1070+ titlecase_string ( chunk. valid ( ) , & mut out) ;
1071+ out. 0 . extend ( chunk. invalid ( ) ) ;
10731072 }
10741073 // SAFETY:
10751074 // * `s` is valid WTF-8; surrogate bytes were appended without processing.
10761075 // * TitlecaseMapper produces valid UTF-8.
1077- unsafe { Wtf8Buf :: from_bytes_unchecked ( buf . 0 ) }
1076+ unsafe { Wtf8Buf :: from_bytes_unchecked ( out . 0 ) }
10781077 }
10791078 }
10801079 }
@@ -1549,6 +1548,11 @@ impl PyStr {
15491548 }
15501549}
15511550
1551+ /// Title case first char if it is cased or write as is.
1552+ ///
1553+ /// This matches CPython's behavior:
1554+ /// "123abc" -> "123abc"
1555+ /// "abc" -> "Abc"
15521556fn titlecase_first ( s : & str , chars : & mut core:: str:: CharIndices < ' _ > , out : & mut VecFmtWriter ) {
15531557 if let Some ( ( first_pos, first_ch) ) = chars. next ( ) {
15541558 let first = & s[ ..first_pos + first_ch. len_utf8 ( ) ] ;
@@ -1559,20 +1563,43 @@ fn titlecase_first(s: &str, chars: &mut core::str::CharIndices<'_>, out: &mut Ve
15591563 }
15601564}
15611565
1562- fn lowercase_and_sigma ( s : & str , chars : & mut core:: str:: CharIndices < ' _ > , out : & mut VecFmtWriter ) {
1566+ /// Title case a string following CPython conventions.
1567+ ///
1568+ /// CPython title cases each char in a segment. A "segment" is split by case ignorable characters
1569+ /// rather than whitespace.
1570+ /// "123abc" -> "123Abc"
1571+ /// "123abc456def" -> "123Abc456Def"
1572+ /// "123 abc" -> "123 Abc"
1573+ fn titlecase_string ( s : & str , out : & mut VecFmtWriter ) {
1574+ let mut previous_is_cased = false ;
1575+ let mapper = TitlecaseMapper :: new ( ) ;
1576+ for ( i, ch) in s. char_indices ( ) {
1577+ if previous_is_cased {
1578+ lowercase_or_sigma ( ch, s, i, out) ;
1579+ } else {
1580+ let s = & s[ i..i + ch. len_utf8 ( ) ] ;
1581+ mapper
1582+ . titlecase_segment ( s, & LanguageIdentifier :: UNKNOWN , Default :: default ( ) )
1583+ . write_to ( out)
1584+ . expect ( "Writing to an in-memory buffer cannot fail." ) ;
1585+ }
1586+
1587+ previous_is_cased = Cased :: for_char ( ch) ;
1588+ }
1589+ }
1590+
1591+ fn lowercase_or_sigma ( ch : char , s : & str , i : usize , out : & mut VecFmtWriter ) {
15631592 let sigma = 'Σ' ;
1564- for ( i, ch) in chars {
1565- if ch == sigma {
1566- let sigma_cased = handle_capital_sigma ( s, i) ;
1593+ if ch == sigma {
1594+ let sigma_cased = handle_capital_sigma ( s, i) ;
1595+ let mut buf = [ 0u8 ; 4 ] ;
1596+ let s = sigma_cased. encode_utf8 ( & mut buf) ;
1597+ out. 0 . extend ( s. as_bytes ( ) ) ;
1598+ } else {
1599+ for ch in ch. to_lowercase ( ) {
15671600 let mut buf = [ 0u8 ; 4 ] ;
1568- let s = sigma_cased . encode_utf8 ( & mut buf) ;
1601+ let s = ch . encode_utf8 ( & mut buf) ;
15691602 out. 0 . extend ( s. as_bytes ( ) ) ;
1570- } else {
1571- for ch in ch. to_lowercase ( ) {
1572- let mut buf = [ 0u8 ; 4 ] ;
1573- let s = ch. encode_utf8 ( & mut buf) ;
1574- out. 0 . extend ( s. as_bytes ( ) ) ;
1575- }
15761603 }
15771604 }
15781605}
0 commit comments