Congratulations, you've covered enough of Rust to write a small, useful program without any extra ceremony! Time for a short break to enjoy the view.
A small word-count library puts these concepts to work.
Its implementation combines strings, for loops, and functions without introducing another language feature.
This first version is the running example we'll keep refactoring throughout the course.
The standard library hands you split_whitespace on every &str.
For now, treat it as a "black box" that lets a for loop walk through each word in a string:
for word in "hello world\nrust".split_whitespace() {
println!("{word}"); // hello, world, rust
}
It splits on any run of whitespace (spaces, tabs, newlines) and skips empties, which is what you want for natural text.
.split_whitespace() returns an iterator over the words.
A for loop consumes that iterator without requiring its concrete type.
The same idea works at the character level via .chars():
for c in "hi".chars() {
println!("{c}"); // h, i
}
With .split_whitespace() for words and .chars() for characters, a for loop can count either one.
Start with the smallest piece of the library: given some text, return how many words it contains.
For this exercise, words are anything separated by whitespace, so "hello world" has two and " " has none.
Keep the implementation deliberately manual.
Walk the pieces from text.split_whitespace(), bump a counter for each one, and return the counter when the loop ends.
Useful from the standard library
str::split_whitespacewalks through every whitespace-separated piece of a string. It handles tabs, newlines, and runs of consecutive spaces without any extra work on your part.
/// Returns the total number of words in `text`. Words are pieces
/// separated by whitespace, so `"hello world"` has two words.
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
#[test]
fn test_word_count_simple() {
assert_eq!(word_count("hello world"), 2);
}
#[test]
fn test_word_count_empty() {
assert_eq!(word_count(""), 0);
assert_eq!(word_count(" "), 0);
}
#[test]
fn test_word_count_collapses_whitespace() {
// Multiple spaces, tabs, and newlines all count as a single
// boundary, so this is four words, not seven.
assert_eq!(word_count("one two\tthree\nfour"), 4);
}
This looks like the same exercise again, but it isn't: text.len() does not return the number of characters.
Rust strings are UTF-8 internally, so a single visible character like é can take more than one byte.
text.len() gives the byte length, while text.chars() walks the actual characters.
For "café" that's 5 bytes but 4 characters, and the two only agree on plain ASCII.
So reach for text.chars(): start a counter at 0, walk the characters with a for loop, and bump the counter once per iteration.
Count every character, whitespace included, so "hi there" returns 8 (seven letters plus the space).
The unicode test below pins the bytes-vs-characters difference down.
Useful from the standard library
str::charswalks through everycharin a string, whitespace and all.str::lenreturns the byte length. Reach forchars().count()when you actually mean "how many characters?" The two answers diverge the moment a non-ASCII character shows up.
/// Returns the number of characters in `text`. Counts every `char`
/// the string contains, whitespace included.
fn char_count(text: &str) -> usize {
text.chars().count()
}
#[test]
fn test_char_count_simple() {
// "hi there" → h, i, ' ', t, h, e, r, e = 8
assert_eq!(char_count("hi there"), 8);
}
#[test]
fn test_char_count_empty() {
assert_eq!(char_count(""), 0);
}
#[test]
fn test_char_count_whitespace_counts() {
// Whitespace characters are real characters too:
// 3 spaces + '\n' + '\t' = 5.
assert_eq!(char_count(" \n\t"), 5);
}
#[test]
fn test_char_count_unicode() {
// `café` is 4 characters even though it's 5 bytes in UTF-8.
// `text.len()` would say 5; `text.chars().count()` says 4.
// `"hi café"` → h, i, ' ', c, a, f, é = 7.
assert_eq!(char_count("café"), 4);
assert_eq!(char_count("hi café"), 7);
}
Last one.
Return the length (in characters) of the longest word in the text.
If the text has no words at all, return 0.
This brings together everything from the previous two steps: walk the words with for word in text.split_whitespace(), measure each one with word.chars().count(), and track the running maximum in a let mut max = 0 variable.
The "track the running maximum" pattern shows up everywhere:
let mut max = 0;
for x in candidates {
if x > max {
max = x;
}
}
This is the manual version of "max by some property." The iterator version expresses the same search as a one-liner. Doing it once by hand first makes each part of that shortcut recognizable.
Useful from the standard library
str::charsandIterator::counttogether give you a correct character count:word.chars().count(). Usingword.len()would return the byte length, which differs from the character count for accented or non-Latin text.
/// Returns the length (in characters) of the longest word in
/// `text`. Words are whitespace-separated. Returns 0 when the
/// text has no words.
fn longest_word(text: &str) -> usize {
text.split_whitespace()
.map(|word| word.chars().count())
.max()
.unwrap_or(0)
}
#[test]
fn test_longest_word_simple() {
// "a bb ccc dddd" → 4
assert_eq!(longest_word("a bb ccc dddd"), 4);
}
#[test]
fn test_longest_word_empty() {
assert_eq!(longest_word(""), 0);
assert_eq!(longest_word(" "), 0);
}
#[test]
fn test_longest_word_unicode() {
// "café" is 4 characters even though it's 5 bytes in UTF-8.
// `word.len()` would say 5; `word.chars().count()` says 4.
assert_eq!(longest_word("hi café"), 4);
}
Three tiny functions, all cut from the same template: a counter variable, a for loop, and a return statement.
That's enough to build a real, useful tool, and it's the same shape you'll keep reaching for as the chapters get bigger.
What we learned
text.split_whitespace()walks the words in a string for you. It handles any kind of whitespace and skips empties without ceremony.text.chars()walks every character in a string, whitespace and all. It's the right tool for "how many characters?".- "Track the running maximum" is the same shape every time:
let mut max = 0; for x in xs { if x > max { max = x; } }.word.chars().count()measures string length in characters, which is usually what you want.str::lenreturns bytes, and the two differ the moment you hit a non-ASCII character.
Iterator methods collapse the three loops to:
fn word_count(text: &str) -> usize { text.split_whitespace().count() }
fn char_count(text: &str) -> usize { text.chars().count() }
fn longest_word(text: &str) -> usize { text.split_whitespace().map(|w| w.chars().count()).max().unwrap_or(0) }
The word-frequencies example extends this from counting all words to recording which words appear and how often.