From 28d994a835e2c388cabd99159dc94a405e06624f Mon Sep 17 00:00:00 2001 From: Rachit2323 Date: Tue, 14 Jul 2026 12:33:29 +0530 Subject: [PATCH 1/2] str: add ASCII fast path to word_to_titlecase --- library/alloc/src/str.rs | 16 +++++++++++---- library/alloctests/tests/lib.rs | 1 + library/alloctests/tests/str.rs | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/library/alloc/src/str.rs b/library/alloc/src/str.rs index 52cdbe20ba176..1bd728170611f 100644 --- a/library/alloc/src/str.rs +++ b/library/alloc/src/str.rs @@ -552,11 +552,10 @@ impl str { without modifying the original"] #[unstable(feature = "titlecase", issue = "153892")] pub fn word_to_titlecase(&self) -> String { - // FIXME: add ASCII fast path - let mut s = String::with_capacity(self.len()); let mut chars = self.char_indices(); + // The first cased character is title-cased; leading uncased characters pass through. 'until_first_cased_char: for (_, c) in chars.by_ref() { if c.is_cased() { s.extend(c.to_titlecase()); @@ -566,14 +565,23 @@ impl str { } } - for (i, c) in chars { + // Everything after the first cased character is lower-cased. Use the ASCII fast + // path (auto-vectorized) for its ASCII prefix, mirroring `to_lowercase`. + let remainder = chars.as_str(); + let rest_start = self.len() - remainder.len(); + // SAFETY: `to_ascii_lowercase` preserves ASCII bytes, so the prefix stays valid UTF-8. + let (ascii, rest) = unsafe { convert_while_ascii(remainder, u8::to_ascii_lowercase) }; + s.push_str(&ascii); + let prefix_len = rest_start + ascii.len(); + + for (i, c) in rest.char_indices() { if c == 'Σ' { // Σ maps to σ, except at the end of a word where it maps to ς. // This is the only conditional (contextual) but language-independent mapping // in `SpecialCasing.txt`, // so hard-code it rather than have a generic "condition" mechanism. // See https://github.com/rust-lang/rust/issues/26035 - let sigma_lowercase = map_uppercase_sigma(self, i); + let sigma_lowercase = map_uppercase_sigma(self, prefix_len + i); s.push(sigma_lowercase); } else { match conversions::to_lower(c) { diff --git a/library/alloctests/tests/lib.rs b/library/alloctests/tests/lib.rs index 96dcde71fc071..d67ea5959765d 100644 --- a/library/alloctests/tests/lib.rs +++ b/library/alloctests/tests/lib.rs @@ -37,6 +37,7 @@ #![feature(string_replace_in_place)] #![feature(test)] #![feature(thin_box)] +#![feature(titlecase)] #![feature(trusted_len)] #![feature(try_reserve_kind)] #![feature(try_with_capacity)] diff --git a/library/alloctests/tests/str.rs b/library/alloctests/tests/str.rs index af4930a0744be..195cbf75cf9c2 100644 --- a/library/alloctests/tests/str.rs +++ b/library/alloctests/tests/str.rs @@ -1889,6 +1889,41 @@ fn to_uppercase() { assert_eq!("aéDžßẞfiᾀ".to_uppercase(), "AÉDŽSSẞFIἈΙ"); } +#[test] +fn word_to_titlecase() { + // ASCII fast path: first cased letter is upper-cased, the rest lower-cased. + assert_eq!("hello WORLD".word_to_titlecase(), "Hello world"); + assert_eq!("HELLO".word_to_titlecase(), "Hello"); + + // Leading uncased characters pass through, then the first cased letter is title-cased. + assert_eq!("'twas".word_to_titlecase(), "'Twas"); + assert_eq!("123 abc".word_to_titlecase(), "123 Abc"); + + // Empty and no-cased-character inputs are unchanged. + assert_eq!("".word_to_titlecase(), ""); + assert_eq!("农历新年".word_to_titlecase(), "农历新年"); + assert_eq!("123 456".word_to_titlecase(), "123 456"); + + // Final-sigma handling: Σ maps to ς at the end of a word, σ elsewhere. + assert_eq!("ὈΔΥΣΣΕΎΣ".word_to_titlecase(), "Ὀδυσσεύς"); + assert_eq!("ΑΣ".word_to_titlecase(), "Ας"); + assert_eq!("ΑΣΑ".word_to_titlecase(), "Ασα"); + + // Mixed ASCII prefix followed by a non-ASCII tail exercises the boundary index math, + // including around the chunk size used by the ASCII prefix optimization. + assert_eq!("HELLO ὈΔΥΣΣΕΎΣ".word_to_titlecase(), "Hello ὀδυσσεύς"); + assert_eq!("ABCDEFGHIJKLMNOΣ".word_to_titlecase(), "Abcdefghijklmnoς"); + assert_eq!("ABCDEFGHIJKLMNOPΣ".word_to_titlecase(), "Abcdefghijklmnopς"); + assert_eq!("ABCDEFGHIJKLMNOPQΣ".word_to_titlecase(), "Abcdefghijklmnopqς"); + + // A long ASCII-only string exercises the auto-vectorized fast path. + assert_eq!(str::repeat("A", 511).word_to_titlecase(), { + let mut expected = String::from("A"); + expected.push_str(&str::repeat("a", 510)); + expected + }); +} + #[test] fn to_casefold_unnormalized() { assert_eq!("".to_casefold_unnormalized(), ""); From ac53b43f85694fbb0878d476265d54612ac9ba50 Mon Sep 17 00:00:00 2001 From: Rachit2323 Date: Tue, 14 Jul 2026 21:08:36 +0530 Subject: [PATCH 2/2] add ligature test cases to word_to_titlecase --- library/alloctests/tests/str.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/alloctests/tests/str.rs b/library/alloctests/tests/str.rs index 195cbf75cf9c2..013149d1623fa 100644 --- a/library/alloctests/tests/str.rs +++ b/library/alloctests/tests/str.rs @@ -1922,6 +1922,21 @@ fn word_to_titlecase() { expected.push_str(&str::repeat("a", 510)); expected }); + + // LJ ligatures and title-case characters. + // Lj is already a title-case letter, so it stays as the first char. + assert_eq!("Ljj".word_to_titlecase(), "Ljj"); + assert_eq!("LjJ".word_to_titlecase(), "Ljj"); + // l is the first cased char (uppercases to L), Lj lowercases to lj. + assert_eq!("lLjfi".word_to_titlecase(), "Lljfi"); + assert_eq!("LLjfi".word_to_titlecase(), "Lljfi"); + + // LJ ligatures: lower=lj (U+01C9), upper=LJ (U+01C7), title=Lj (U+01C8). + // ß decomposes to "Ss" in title case (first char) but stays ß elsewhere. + assert_eq!("ßljLJLj".word_to_titlecase(), "Ssljljlj"); + assert_eq!("ljLJLjß".word_to_titlecase(), "Ljljljß"); + assert_eq!("LJLjßlj".word_to_titlecase(), "Ljljßlj"); + assert_eq!("LjßljLJ".word_to_titlecase(), "Ljßljlj"); } #[test]