Skip to content

Commit 7427361

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

1 file changed

Lines changed: 142 additions & 11 deletions

File tree

library/alloc/src/str.rs

Lines changed: 142 additions & 11 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
@@ -415,21 +431,136 @@ impl str {
415431
}
416432
}
417433
return s;
434+
}
418435

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

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,
538+
for (i, c) in chars {
539+
if c == 'Σ' {
540+
// Σ maps to σ, except at the end of a word where it maps to ς.
541+
// This is the only conditional (contextual) but language-independent mapping
542+
// in `SpecialCasing.txt`,
543+
// so hard-code it rather than have a generic "condition" mechanism.
544+
// See https://github.com/rust-lang/rust/issues/26035
545+
let sigma_lowercase = map_uppercase_sigma(self, i);
546+
s.push(sigma_lowercase);
547+
} else {
548+
match conversions::to_lower(c) {
549+
[a, '\0', _] => s.push(a),
550+
[a, b, '\0'] => {
551+
s.push(a);
552+
s.push(b);
553+
}
554+
[a, b, c] => {
555+
s.push(a);
556+
s.push(b);
557+
s.push(c);
558+
}
559+
}
431560
}
432561
}
562+
563+
s
433564
}
434565

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

0 commit comments

Comments
 (0)