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('é')
.len()counts bytes, not characters. That's what it costs in memory, and it's instant because the String stores it..chars()decodes the bytes intochars, one at a time..chars().count()is the number of characters; it has to walk the whole string..bytes()gives the raw bytes:éis the two bytes 195 169..chars().next()is the first character and.chars().nth(i)the character at positioni. Both return anOption<char>, since the string might be too short.
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.