Skip to content

Commit 978ffc1

Browse files
committed
Titlecase the first-of-word code point without leading adjustment
case::{title,capitalize}_wtf8 pass a single first-of-word code point to TitlecaseMapper::titlecase_segment. Under the default Auto leading adjustment the mapper looks for a Letter/Number/Symbol/Private_Use head and skips anything else, dropping the titlecase mapping of cased marks such as U+0345 (COMBINING GREEK YPOGEGRAMMENI) -> U+0399: 'ͅ'.title() returned 'ͅ' instead of 'Ι'. Use LeadingAdjustment::None so the given code point is titlecased directly, matching CPython str.title and str.capitalize. Assisted-by: Claude
1 parent 6a118b3 commit 978ffc1

1 file changed

Lines changed: 26 additions & 1 deletion

File tree

crates/unicode/src/case.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use alloc::{
1919
vec::Vec,
2020
};
2121

22+
use icu_casemap::options::{LeadingAdjustment, TitlecaseOptions};
2223
use icu_casemap::{CaseMapper, TitlecaseMapper};
2324
use icu_locale::LanguageIdentifier;
2425
use icu_properties::props::{
@@ -236,8 +237,16 @@ fn titlecase_string(s: &str, out: &mut FmtWriter<'_>) {
236237
}
237238

238239
fn titlecase_segment(s: &str, out: &mut FmtWriter<'_>) {
240+
// Callers pass a single first-of-word code point, which Python titlecases
241+
// unconditionally (applying its titlecase mapping). The default `Auto`
242+
// leading adjustment looks for a head in Letter/Number/Symbol/Private_Use
243+
// and skips anything else, dropping the titlecase mapping of cased marks
244+
// such as U+0345 (`ͅ`, general category Mn) -> U+0399 (`Ι`). `None`
245+
// titlecases the code point as given.
246+
let mut options = TitlecaseOptions::default();
247+
options.leading_adjustment = Some(LeadingAdjustment::None);
239248
TitlecaseMapper::new()
240-
.titlecase_segment(s, &LanguageIdentifier::UNKNOWN, Default::default())
249+
.titlecase_segment(s, &LanguageIdentifier::UNKNOWN, options)
241250
.write_to(out)
242251
.expect("writing to an in-memory buffer cannot fail");
243252
}
@@ -353,6 +362,22 @@ mod tests {
353362
assert_eq!(swapcase_str("Hello"), "hELLO");
354363
}
355364

365+
#[test]
366+
fn titlecase_first_of_word_takes_titlecase_mapping() {
367+
// A leading cased combining mark still takes its titlecase mapping:
368+
// U+0345 (ͅ, general category Mn) titlecases to U+0399 (Ι), even though
369+
// it is not a Letter/Number/Symbol head.
370+
assert_eq!(title_str("\u{0345}"), "\u{0399}");
371+
assert_eq!(capitalize_str("\u{0345}"), "\u{0399}");
372+
assert_eq!(title_str("\u{0345}a"), "\u{0399}a");
373+
// Full (one-to-many) titlecase mappings still apply to the first
374+
// character of each word.
375+
assert_eq!(capitalize_str("finnish"), "Finnish");
376+
assert_eq!(title_str("fiNNISH"), "Finnish");
377+
assert_eq!(capitalize_str("dzungla"), "Dzungla");
378+
assert_eq!(capitalize_str("ßhello"), "Sshello");
379+
}
380+
356381
#[test]
357382
fn wtf8_passes_surrogates_through() {
358383
let mut buf = Wtf8Buf::from("ab cd");

0 commit comments

Comments
 (0)