Skip to content

Commit a8e5970

Browse files
authored
Unrolled build for #155705
Rollup merge of #155705 - Jules-Bertholet:word-to-titlecase, r=Mark-Simulacrum Add `str::word_to_titlecase()` to `alloc` A small addition to #153892. Hasn't gone through ACP, so needs libs-API signoff. @rustbot label A-Unicode T-libs-api
2 parents ce89c89 + 40da877 commit a8e5970

1 file changed

Lines changed: 152 additions & 16 deletions

File tree

library/alloc/src/str.rs

Lines changed: 152 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ where
208208
result
209209
}
210210

211+
/// Helper for final sigma lowercase
212+
#[cfg(not(no_global_oom_handling))]
213+
fn map_uppercase_sigma(from: &str, i: usize) -> char {
214+
fn case_ignorable_then_cased<I: Iterator<Item = char>>(iter: I) -> bool {
215+
match iter.skip_while(|&c| c.is_case_ignorable()).next() {
216+
Some(c) => c.is_cased(),
217+
None => false,
218+
}
219+
}
220+
221+
// See https://www.unicode.org/versions/latest/core-spec/chapter-3/#G54277
222+
// for the definition of `Final_Sigma`.
223+
let is_word_final = case_ignorable_then_cased(from[..i].chars().rev())
224+
&& !case_ignorable_then_cased(from[i + const { 'Σ'.len_utf8() }..].chars());
225+
if is_word_final { 'ς' } else { 'σ' }
226+
}
227+
211228
#[stable(feature = "rust1", since = "1.0.0")]
212229
impl Borrow<str> for String {
213230
#[inline]
@@ -368,20 +385,20 @@ impl str {
368385
///
369386
/// Unlike [`char::to_lowercase()`], this method fully handles the context-dependent
370387
/// casing of Greek sigma. However, like that method, it does not handle locale-specific
371-
/// casing, like Turkish and Azeri I/ı/İ/i. See that method's documentation
388+
/// casing, like Turkish and Azeri I/ı/İ/i. See its documentation
372389
/// for more information.
373390
///
374391
/// # Examples
375392
///
376393
/// Basic usage:
377394
///
378395
/// ```
379-
/// let s = "HELLO";
396+
/// let s = "HELLO WORLD";
380397
///
381-
/// assert_eq!("hello", s.to_lowercase());
398+
/// assert_eq!("hello world", s.to_lowercase());
382399
/// ```
383400
///
384-
/// A tricky example, with sigma:
401+
/// Tricky examples, with sigma:
385402
///
386403
/// ```
387404
/// let sigma = "Σ";
@@ -392,6 +409,10 @@ impl str {
392409
/// let odysseus = "ὈΔΥΣΣΕΎΣ";
393410
///
394411
/// assert_eq!("ὀδυσσεύς", odysseus.to_lowercase());
412+
///
413+
/// let odysseus_king_of_ithaca = "Ο ΟΔΥΣΣΈΑΣ ΒΑΣΙΛΙΆΣ ΤΗΣ ΙΘΆΚΗΣ";
414+
///
415+
/// assert_eq!("ο οδυσσέας βασιλιάς της ιθάκης", odysseus_king_of_ithaca.to_lowercase());
395416
/// ```
396417
///
397418
/// Languages without case are not changed:
@@ -438,21 +459,136 @@ impl str {
438459
}
439460
}
440461
return s;
462+
}
441463

442-
fn map_uppercase_sigma(from: &str, i: usize) -> char {
443-
// See https://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G33992
444-
// for the definition of `Final_Sigma`.
445-
let is_word_final = case_ignorable_then_cased(from[..i].chars().rev())
446-
&& !case_ignorable_then_cased(from[i + const { 'Σ'.len_utf8() }..].chars());
447-
if is_word_final { 'ς' } else { 'σ' }
464+
/// Returns the titlecase equivalent of this string slice,
465+
/// which is assumed to represent a single word,
466+
/// as a new [`String`].
467+
///
468+
/// Essentially, this consists of uppercasing the first cased letter
469+
/// (with [`char::to_titlecase()`]), and lowercasing everything that follows.
470+
///
471+
/// 'Titlecase' is defined according to the terms of
472+
/// [Chapter 3 (Conformance)](https://www.unicode.org/versions/latest/core-spec/chapter-3/#G34082)
473+
/// of the Unicode standard.
474+
///
475+
/// Since some characters can expand into multiple characters when changing
476+
/// the case, this function returns a [`String`] instead of modifying the
477+
/// parameter in-place.
478+
///
479+
/// Unlike [`char::to_lowercase()`], this method fully handles the context-dependent
480+
/// casing of Greek sigma. However, like that method, it does not handle locale-specific
481+
/// casing, like Turkish and Azeri I/ı/İ/i. See its documentation
482+
/// for more information.
483+
///
484+
/// This method does not perform any kind of word segmentation.
485+
///
486+
/// # Examples
487+
///
488+
/// Basic usage:
489+
///
490+
/// ```
491+
/// #![feature(titlecase)]
492+
/// let s = "HELLO WORLD";
493+
///
494+
/// assert_eq!("Hello world", s.word_to_titlecase());
495+
/// ```
496+
///
497+
/// The first *cased* letter is uppercased:
498+
///
499+
/// ```
500+
/// #![feature(titlecase)]
501+
/// let the_night_before_christmas = "'twas";
502+
///
503+
/// assert_eq!("'Twas", the_night_before_christmas.word_to_titlecase());
504+
/// ```
505+
///
506+
/// Languages without case are not changed:
507+
///
508+
/// ```
509+
/// #![feature(titlecase)]
510+
/// let new_year = "农历新年";
511+
///
512+
/// assert_eq!(new_year, new_year.word_to_titlecase());
513+
/// ```
514+
///
515+
/// Georgian uppercase ("Mtavruli") letters are not used in titlecase:
516+
///
517+
/// ```
518+
/// #![feature(titlecase)]
519+
/// let georgian = "ერთობაშია";
520+
///
521+
/// assert_eq!(georgian, georgian.word_to_titlecase());
522+
/// ```
523+
///
524+
/// No word segmentation is performed,
525+
/// so only the first cased letter in the whole string gets uppercased:
526+
///
527+
/// ```
528+
/// #![feature(titlecase)]
529+
/// let blazingly_fast = "ferris and I";
530+
///
531+
/// assert_eq!("Ferris and i", blazingly_fast.word_to_titlecase());
532+
/// ```
533+
///
534+
/// Tricky examples, with sigma:
535+
///
536+
/// ```
537+
/// #![feature(titlecase)]
538+
/// let odysseus = "ὈΔΥΣΣΕΎΣ";
539+
///
540+
/// assert_eq!("Ὀδυσσεύς", odysseus.word_to_titlecase());
541+
///
542+
/// let odysseus_king_of_ithaca = "Ο ΟΔΥΣΣΈΑΣ ΒΑΣΙΛΙΆΣ ΤΗΣ ΙΘΆΚΗΣ";
543+
///
544+
/// assert_eq!("Ο οδυσσέας βασιλιάς της ιθάκης", odysseus_king_of_ithaca.word_to_titlecase());
545+
/// ```
546+
#[cfg(not(no_global_oom_handling))]
547+
#[rustc_allow_incoherent_impl]
548+
#[must_use = "this returns the titlecase word as a new String, \
549+
without modifying the original"]
550+
#[unstable(feature = "titlecase", issue = "153892")]
551+
pub fn word_to_titlecase(&self) -> String {
552+
// FIXME: add ASCII fast path
553+
554+
let mut s = String::with_capacity(self.len());
555+
let mut chars = self.char_indices();
556+
557+
'until_first_cased_char: for (_, c) in chars.by_ref() {
558+
if c.is_cased() {
559+
s.extend(c.to_titlecase());
560+
break 'until_first_cased_char;
561+
} else {
562+
s.push(c);
563+
}
448564
}
449565

450-
fn case_ignorable_then_cased<I: Iterator<Item = char>>(iter: I) -> bool {
451-
match iter.skip_while(|&c| c.is_case_ignorable()).next() {
452-
Some(c) => c.is_cased(),
453-
None => false,
566+
for (i, c) in chars {
567+
if c == 'Σ' {
568+
// Σ maps to σ, except at the end of a word where it maps to ς.
569+
// This is the only conditional (contextual) but language-independent mapping
570+
// in `SpecialCasing.txt`,
571+
// so hard-code it rather than have a generic "condition" mechanism.
572+
// See https://github.com/rust-lang/rust/issues/26035
573+
let sigma_lowercase = map_uppercase_sigma(self, i);
574+
s.push(sigma_lowercase);
575+
} else {
576+
match conversions::to_lower(c) {
577+
[a, '\0', _] => s.push(a),
578+
[a, b, '\0'] => {
579+
s.push(a);
580+
s.push(b);
581+
}
582+
[a, b, c] => {
583+
s.push(a);
584+
s.push(b);
585+
s.push(c);
586+
}
587+
}
454588
}
455589
}
590+
591+
s
456592
}
457593

458594
/// Returns the uppercase equivalent of this string slice, as a new [`String`].
@@ -474,9 +610,9 @@ impl str {
474610
/// Basic usage:
475611
///
476612
/// ```
477-
/// let s = "hello";
613+
/// let s = "hello world";
478614
///
479-
/// assert_eq!("HELLO", s.to_uppercase());
615+
/// assert_eq!("HELLO WORLD", s.to_uppercase());
480616
/// ```
481617
///
482618
/// Scripts without case are not changed:

0 commit comments

Comments
 (0)