Skip to content

← Ground Rules: Values, Types, Control Flow step 24 of 24

Hard Primitives

UTF-8 strings: bytes, scalar values and slicing

Return a string's UTF-8 byte length, its Unicode scalar-value count, and a new string with those scalar values in reverse order.

pub fn char_stats(text: &str) -> (usize, usize, String)
Input Output
"héllo" (6, 5, "olléh")
"abc" (3, 3, "cba")
"" (0, 0, "")

Preserve every scalar value, including spaces, control characters and combining marks. Do not normalize the input or group grapheme clusters. For example, "e\u{301}" becomes "\u{301}e", with byte length three and scalar count two.

Choose the view you need

A str stores valid UTF-8. One Unicode scalar value occupies between one and four bytes, so a byte position and a scalar position are different:

fn main() {
    let text = "héllo";
    assert_eq!(text.len(), 6);
    assert_eq!(text.chars().count(), 5);
    assert_eq!(text.as_bytes()[1], 0xC3);
    assert_eq!(text.chars().nth(1), Some('é'));
    assert_eq!(text.char_indices().collect::<Vec<_>>(),
               [(0, 'h'), (1, 'é'), (3, 'l'), (4, 'l'), (5, 'o')]);
}

len() reads the stored byte length in constant time. chars().count() scans the string. chars().nth(k) advances an iterator to the requested scalar position and returns None if it does not exist. Repeatedly creating a fresh iterator to find successive positions can repeat the same work; use one iterator when traversing the whole string.

char_indices() pairs each scalar value with its starting byte offset. These offsets are useful when you need a substring from the original text. For repeated random access, storing offsets or collecting scalar values is another design option, with its own setup and storage costs.

Integer indexing is not supported for str

The starter uses text[i], which fails with E0277 because str does not implement integer indexing. Even ASCII-only input does not make that expression valid. Choose a byte-oriented or scalar-oriented operation explicitly rather than replacing the index with a cast.

For a byte, text.as_bytes().get(i) returns Option<&u8>. For a scalar, text.chars().nth(i) returns Option<char>. The starter also returns the byte length twice; change the second result to a scalar count.

String slices use byte offsets

&text[start..end] borrows the half-open byte interval without copying it. It requires start <= end <= text.len() and both endpoints to be UTF-8 boundaries. Otherwise, indexing panics. For "héllo", byte offsets one and three surround é, while offset two lies inside its encoding.

Use get when an invalid range should return None instead of panicking:

fn main() {
    let text = "héllo";
    assert_eq!(&text[1..3], "é");
    assert_eq!(text.get(1..3), Some("é"));
    assert_eq!(text.get(0..2), None);
    assert_eq!(text.get(0..100), None);
    assert_eq!(text.get(text.len()..text.len()), Some(""));
    assert!(!text.is_char_boundary(2));
    assert!(text.is_char_boundary(text.len()));
}

is_char_boundary is true at the start and end of a string and false beyond its length. Checking endpoints alone does not establish that the start precedes the end; get checks the whole range. A slice failure can come from an out-of-bounds range, reversed endpoints or a split UTF-8 encoding.

Reverse scalar values, then encode them again

text.chars().rev().collect::<String>() iterates from the end and builds a valid UTF-8 string. Reversing individual bytes instead does not preserve UTF-8 encoding when the input contains multibyte scalar values.

Scalar reversal is the contract for this exercise. It need not preserve the groups a reader perceives as characters:

fn main() {
    let text = "e\u{301}";
    let reversed: String = text.chars().rev().collect();
    assert_eq!(reversed, "\u{301}e");
    assert_eq!(reversed.len(), text.len());
    assert_eq!(reversed.chars().count(), 2);
}

Grapheme clusters may combine a base letter with marks, or several scalar values into an emoji. The standard library has no built-in grapheme iterator. Applications that need grapheme segmentation can use a suitable Unicode library; that is a different operation from the reversal requested here.

Clippy's default iter_nth_zero warning suggests next() instead of nth(0). Its chars_next_cmp warning suggests a prefix check such as starts_with('x') instead of chars().next() == Some('x'). Submissions must compile, pass the tests and pass Clippy with warnings treated as errors.