RustArena

Step 1 of 7

UTF-8: bytes, chars and why s[0] fails

In module 7 you met String (owned, growable text) and &str (a borrowed view of text). Both store text as UTF-8, the encoding used by the web, by files and by almost every system today. In UTF-8 one character takes 1 to 4 bytes: plain English letters and digits take 1, accented Latin letters (and Greek, Cyrillic, Arabic, Hebrew) 2, Chinese, Japanese, Korean and symbols like € 3, and emoji 4.

fn main() {
    for word in ["crab", "café", "€5", "🦀"] {
        println!("{word}: bytes={} chars={}", word.len(), word.chars().count());
    }

    let s = "née";
    for b in s.bytes() {
        print!("{b} ");
    }
    println!();
    for c in s.chars() {
        print!("{c:?} ");
    }
    println!();
    println!("{:?} {:?}", s.chars().next(), s.chars().nth(1));
}
crab: bytes=4 chars=4
café: bytes=5 chars=4
€5: bytes=4 chars=2
🦀: bytes=4 chars=1
110 195 169 101
'n' 'é' 'e'
Some('n') Some('é')

Why s[0] doesn't compile

fn main() {
    let s = String::from("hello");
    let c = s[0];
    println!("{c}");
}
error[E0277]: the type `str` cannot be indexed by `{integer}`

Two reasons. First, a position in bytes isn't a character: in "née", byte 1 is the first half of é, and returning it as if it were a letter would be wrong. Second, [] is expected to be instant, but finding the nth character means decoding everything before it. Rather than hide that cost or return half-characters, Rust makes you say what you mean:

You want Write
the first character s.chars().next()
the character at position n s.chars().nth(n) (walks from the start)
the last character s.chars().last()
the first byte (for ASCII protocols) s.as_bytes()[0]
the number of characters s.chars().count()

char has handy methods for classifying: is_alphabetic(), is_numeric(), is_ascii_digit(), is_whitespace(), is_uppercase(), is_alphanumeric().

A workplace note on "characters"

A Rust char is a Unicode scalar value, which isn't always one symbol on screen. é can also be written as e followed by a combining accent (two chars), and an emoji like 👩‍💻 is three chars glued together. When you must count what a person sees (for example, limiting a display name), teams use the unicode-segmentation crate. For parsing logs, CSV, numbers and commands, chars and bytes are what you want.

Your task

Write fn initials(full_name: &str) -> String that returns the first character of each word, in uppercase: "ada lovelace" gives "AL". Words are separated by any amount of whitespace, and names can contain accents ("émile zola" gives "ÉZ").

Challenge 2

Fill in the blanks: the method that counts bytes, the method that iterates characters, and the method that takes the first character.

Challenge 3

Write fn is_palindrome(s: &str) -> bool that ignores case and everything that isn't a letter or digit: "A man, a plan, a canal: Panama" is a palindrome. Lowercase the text first, keep only the is_alphanumeric() chars in a Vec<char>, then compare it from both ends.

Next: Slicing safely at char boundaries