Step 3 of 6
Counting with entry
The most common thing to do with a map is count: words in a text, hits per page, errors per server. Each time you see a key you want "add one, or start at one if it's new". With what you know so far, that's a get_mut and an insert: two lookups and a match. The entry API does it in one:
use std::collections::HashMap;
fn main() {
let text = "the crab and the gull and the crab";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
let mut pairs: Vec<(&str, u32)> = Vec::new();
for (word, n) in &counts {
pairs.push((*word, *n));
}
pairs.sort();
for (word, n) in pairs {
println!("{word}: {n}");
}
}
and: 2
crab: 2
gull: 1
the: 3
Read *counts.entry(word).or_insert(0) += 1; from the inside out:
counts.entry(word)finds the slot forword. That slot, an Entry, is either occupied (the word was seen before) or vacant..or_insert(0)puts0in the slot if it was vacant. Either way it returns a&mut u32pointing at the count inside the map.*... += 1adds one through that reference.
One lookup, no match, and it reads like the sentence "add one to this word's count, starting from zero".
Other ways to fill an entry
use std::collections::BTreeMap;
fn main() {
let visits = ["home", "about", "home", "shop", "home"];
let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
for page in visits {
counts.entry(page).and_modify(|n| *n += 1).or_insert(1);
}
println!("{:?}", counts);
let mut totals: BTreeMap<&str, u32> = BTreeMap::new();
for page in visits {
*totals.entry(page).or_default() += 1;
}
println!("{:?}", totals);
}
{"about": 1, "home": 3, "shop": 1}
{"about": 1, "home": 3, "shop": 1}
.and_modify(|n| *n += 1)runs the closure on the value only if the key already exists;.or_insert(1)handles the new key..or_default()inserts the type's default value:0for numbers, an emptyStringorVec. It's handy for building lists per key, which you'll do in the last step.- The entry API works the same on
BTreeMap; here it's used so the map can be printed directly.
Common mistakes
Holding two entries at once.
or_insertreturns a mutable reference into the map, so the map stays mutably borrowed while you use it:use std::collections::HashMap; fn main() { let mut counts: HashMap<&str, u32> = HashMap::new(); let a = counts.entry("a").or_insert(0); let b = counts.entry("b").or_insert(0); *a += 1; *b += 1; }error[E0499]: cannot borrow `counts` as mutable more than once at a timeInserting
"b"could make the map grow and move its entries, which would leaveapointing at freed memory. Finish with one entry before starting the next:*counts.entry("a").or_insert(0) += 1;on one line, then the same for"b".Keys need to be owned values of the key type. With
Stringkeys,entryneeds aString:counts.entry(word.to_string()). That allocates even when the word is already in the map, which is fine for most programs.
Your task
Write fn word_counts(text: &str) -> HashMap<String, u32> that counts each word (split on whitespace), ignoring case: "Rust" and "rust" are the same word, stored in lowercase.
Challenge 2
Fill in the blanks to count how often each letter appears, using a BTreeMap so the letters print in order.
Challenge 3
Write a sales report. Each input line is region amount (whole numbers). Total the amounts per region with the entry API, then print every region as region: total, highest total first, with equal totals in alphabetical order.
Previous: Iteration order and BTreeMap Next: HashSet: have I seen this before?