Skip to content

Commit 8ab92b6

Browse files
Add char::word_to_titlecase() to alloc
1 parent 36ba2c7 commit 8ab92b6

1 file changed

Lines changed: 147 additions & 12 deletions

File tree

library/alloc/src/str.rs

Lines changed: 147 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,22 @@ where
185185
result
186186
}
187187

188+
/// Helper for final sigma lowercase
189+
fn map_uppercase_sigma(from: &str, i: usize) -> char {
190+
fn case_ignorable_then_cased<I: Iterator<Item = char>>(iter: I) -> bool {
191+
match iter.skip_while(|&c| c.is_case_ignorable()).next() {
192+
Some(c) => c.is_cased(),
193+
None => false,
194+
}
195+
}
196+
197+
// See https://www.unicode.org/versions/latest/core-spec/chapter-3/#G54277
198+
// for the definition of `Final_Sigma`.
199+
let is_word_final = case_ignorable_then_cased(from[..i].chars().rev())
200+
&& !case_ignorable_then_cased(from[i + const { 'Σ'.len_utf8() }..].chars());
201+
if is_word_final { 'ς' } else { 'σ' }
202+
}
203+
188204
#[stable(feature = "rust1", since = "1.0.0")]
189205
impl Borrow<str> for String {
190206
#[inline]
@@ -345,7 +361,7 @@ impl str {
345361
///
346362
/// Unlike [`char::to_lowercase()`], this method fully handles the context-dependent
347363
/// casing of Greek sigma. However, like that method, it does not handle locale-specific
348-
/// casing, like Turkish and Azeri I/ı/İ/i. See that method's documentation
364+
/// casing, like Turkish and Azeri I/ı/İ/i. See its documentation
349365
/// for more information.
350366
///
351367
/// # Examples
@@ -358,7 +374,7 @@ impl str {
358374
/// assert_eq!("hello", s.to_lowercase());
359375
/// ```
360376
///
361-
/// A tricky example, with sigma:
377+
/// Tricky examples, with sigma:
362378
///
363379
/// ```
364380
/// let sigma = "Σ";
@@ -369,6 +385,10 @@ impl str {
369385
/// let odysseus = "ὈΔΥΣΣΕΎΣ";
370386
///
371387
/// assert_eq!("ὀδυσσεύς", odysseus.to_lowercase());
388+
///
389+
/// let odysseus_king_of_ithaca = "Ο ΟΔΥΣΣΈΑΣ ΒΑΣΙΛΙΆΣ ΤΗΣ ΙΘΆΚΗΣ";
390+
///
391+
/// assert_eq!("ο οδυσσέας βασιλιάς της ιθάκης", odysseus_king_of_ithaca.to_lowercase());
372392
/// ```
373393
///
374394
/// Languages without case are not changed:
@@ -415,21 +435,136 @@ impl str {
415435
}
416436
}
417437
return s;
438+
}
418439

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

427-
fn case_ignorable_then_cased<I: Iterator<Item = char>>(iter: I) -> bool {
428-
match iter.skip_while(|&c| c.is_case_ignorable()).next() {
429-
Some(c) => c.is_cased(),
430-
None => false,
542+
for (i, c) in chars {
543+
if c == 'Σ' {
544+
// Σ maps to σ, except at the end of a word where it maps to ς.
545+
// This is the only conditional (contextual) but language-independent mapping
546+
// in `SpecialCasing.txt`,
547+
// so hard-code it rather than have a generic "condition" mechanism.
548+
// See https://github.com/rust-lang/rust/issues/26035
549+
let sigma_lowercase = map_uppercase_sigma(self, i);
550+
s.push(sigma_lowercase);
551+
} else {
552+
match conversions::to_lower(c) {
553+
[a, '\0', _] => s.push(a),
554+
[a, b, '\0'] => {
555+
s.push(a);
556+
s.push(b);
557+
}
558+
[a, b, c] => {
559+
s.push(a);
560+
s.push(b);
561+
s.push(c);
562+
}
563+
}
431564
}
432565
}
566+
567+
s
433568
}
434569

435570
/// Returns the uppercase equivalent of this string slice, as a new [`String`].

0 commit comments

Comments
 (0)