Skip to content

Commit af4cac9

Browse files
authored
Rollup merge of #159271 - Rachit2323:fix-titlecase-ascii-fastpath, r=clarfonthey
str: add ASCII fast path to word_to_titlecase The word_to_titlecase function had a FIXME comment since a long time asking to add a fast path for ASCII text. Before this change, even simple English text like "hello world" was going through slow Unicode lookup tables. That was unnecessary. This fixes it by using the same trick that to_lowercase already uses — plain English letters are now handled much faster, and only Greek/Chinese/etc still use the Unicode tables. No behavior changes, just faster for normal English text.
2 parents f907604 + ac53b43 commit af4cac9

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

library/alloc/src/str.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -552,11 +552,10 @@ impl str {
552552
without modifying the original"]
553553
#[unstable(feature = "titlecase", issue = "153892")]
554554
pub fn word_to_titlecase(&self) -> String {
555-
// FIXME: add ASCII fast path
556-
557555
let mut s = String::with_capacity(self.len());
558556
let mut chars = self.char_indices();
559557

558+
// The first cased character is title-cased; leading uncased characters pass through.
560559
'until_first_cased_char: for (_, c) in chars.by_ref() {
561560
if c.is_cased() {
562561
s.extend(c.to_titlecase());
@@ -566,14 +565,23 @@ impl str {
566565
}
567566
}
568567

569-
for (i, c) in chars {
568+
// Everything after the first cased character is lower-cased. Use the ASCII fast
569+
// path (auto-vectorized) for its ASCII prefix, mirroring `to_lowercase`.
570+
let remainder = chars.as_str();
571+
let rest_start = self.len() - remainder.len();
572+
// SAFETY: `to_ascii_lowercase` preserves ASCII bytes, so the prefix stays valid UTF-8.
573+
let (ascii, rest) = unsafe { convert_while_ascii(remainder, u8::to_ascii_lowercase) };
574+
s.push_str(&ascii);
575+
let prefix_len = rest_start + ascii.len();
576+
577+
for (i, c) in rest.char_indices() {
570578
if c == 'Σ' {
571579
// Σ maps to σ, except at the end of a word where it maps to ς.
572580
// This is the only conditional (contextual) but language-independent mapping
573581
// in `SpecialCasing.txt`,
574582
// so hard-code it rather than have a generic "condition" mechanism.
575583
// See https://github.com/rust-lang/rust/issues/26035
576-
let sigma_lowercase = map_uppercase_sigma(self, i);
584+
let sigma_lowercase = map_uppercase_sigma(self, prefix_len + i);
577585
s.push(sigma_lowercase);
578586
} else {
579587
match conversions::to_lower(c) {

library/alloctests/tests/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#![feature(string_replace_in_place)]
3939
#![feature(test)]
4040
#![feature(thin_box)]
41+
#![feature(titlecase)]
4142
#![feature(trusted_len)]
4243
#![feature(try_reserve_kind)]
4344
#![feature(try_with_capacity)]

library/alloctests/tests/str.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,6 +1904,56 @@ fn to_uppercase() {
19041904
assert_eq!("aéDžßẞfiᾀ".to_uppercase(), "AÉDŽSSẞFIἈΙ");
19051905
}
19061906

1907+
#[test]
1908+
fn word_to_titlecase() {
1909+
// ASCII fast path: first cased letter is upper-cased, the rest lower-cased.
1910+
assert_eq!("hello WORLD".word_to_titlecase(), "Hello world");
1911+
assert_eq!("HELLO".word_to_titlecase(), "Hello");
1912+
1913+
// Leading uncased characters pass through, then the first cased letter is title-cased.
1914+
assert_eq!("'twas".word_to_titlecase(), "'Twas");
1915+
assert_eq!("123 abc".word_to_titlecase(), "123 Abc");
1916+
1917+
// Empty and no-cased-character inputs are unchanged.
1918+
assert_eq!("".word_to_titlecase(), "");
1919+
assert_eq!("农历新年".word_to_titlecase(), "农历新年");
1920+
assert_eq!("123 456".word_to_titlecase(), "123 456");
1921+
1922+
// Final-sigma handling: Σ maps to ς at the end of a word, σ elsewhere.
1923+
assert_eq!("ὈΔΥΣΣΕΎΣ".word_to_titlecase(), "Ὀδυσσεύς");
1924+
assert_eq!("ΑΣ".word_to_titlecase(), "Ας");
1925+
assert_eq!("ΑΣΑ".word_to_titlecase(), "Ασα");
1926+
1927+
// Mixed ASCII prefix followed by a non-ASCII tail exercises the boundary index math,
1928+
// including around the chunk size used by the ASCII prefix optimization.
1929+
assert_eq!("HELLO ὈΔΥΣΣΕΎΣ".word_to_titlecase(), "Hello ὀδυσσεύς");
1930+
assert_eq!("ABCDEFGHIJKLMNOΣ".word_to_titlecase(), "Abcdefghijklmnoς");
1931+
assert_eq!("ABCDEFGHIJKLMNOPΣ".word_to_titlecase(), "Abcdefghijklmnopς");
1932+
assert_eq!("ABCDEFGHIJKLMNOPQΣ".word_to_titlecase(), "Abcdefghijklmnopqς");
1933+
1934+
// A long ASCII-only string exercises the auto-vectorized fast path.
1935+
assert_eq!(str::repeat("A", 511).word_to_titlecase(), {
1936+
let mut expected = String::from("A");
1937+
expected.push_str(&str::repeat("a", 510));
1938+
expected
1939+
});
1940+
1941+
// LJ ligatures and title-case characters.
1942+
// Lj is already a title-case letter, so it stays as the first char.
1943+
assert_eq!("Ljj".word_to_titlecase(), "Ljj");
1944+
assert_eq!("LjJ".word_to_titlecase(), "Ljj");
1945+
// l is the first cased char (uppercases to L), Lj lowercases to lj.
1946+
assert_eq!("lLjfi".word_to_titlecase(), "Lljfi");
1947+
assert_eq!("LLjfi".word_to_titlecase(), "Lljfi");
1948+
1949+
// LJ ligatures: lower=lj (U+01C9), upper=LJ (U+01C7), title=Lj (U+01C8).
1950+
// ß decomposes to "Ss" in title case (first char) but stays ß elsewhere.
1951+
assert_eq!("ßljLJLj".word_to_titlecase(), "Ssljljlj");
1952+
assert_eq!("ljLJLjß".word_to_titlecase(), "Ljljljß");
1953+
assert_eq!("LJLjßlj".word_to_titlecase(), "Ljljßlj");
1954+
assert_eq!("LjßljLJ".word_to_titlecase(), "Ljßljlj");
1955+
}
1956+
19071957
#[test]
19081958
fn to_casefold_unnormalized() {
19091959
assert_eq!("".to_casefold_unnormalized(), "");

0 commit comments

Comments
 (0)