Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions library/alloc/src/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions library/alloctests/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
50 changes: 50 additions & 0 deletions library/alloctests/tests/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,56 @@ fn to_uppercase() {
assert_eq!("aéDžßẞfiᾀ".to_uppercase(), "AÉDŽSSẞFIἈΙ");
}

#[test]
fn word_to_titlecase() {
Comment thread
Rachit2323 marked this conversation as resolved.
// 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
});

// 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]
fn to_casefold_unnormalized() {
assert_eq!("".to_casefold_unnormalized(), "");
Expand Down
Loading