Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 21 of 26

Medium Primitives

Make hashing agree with equality

Fix the hashing implementation so words that differ only in ASCII letter case share a count.

pub fn count_case_insensitive(words: Vec<String>) -> Vec<(String, u64)>

Keep the supplied CiWord(String) wrapper, its ASCII-case-insensitive PartialEq and Eq, and the HashMap<CiWord, u64>. Replace the derived Hash with a manual implementation that agrees with that equality rule.

Required result

Treat each input string as one word. Change only ASCII A through Z to lowercase when grouping words and producing output keys. Preserve all other characters, including whitespace and punctuation. Do not split or trim strings.

Return one (normalized_word, count) pair per group, sorted by the word using Rust's String ordering. An empty input returns an empty vector. Empty strings are valid words and must be counted.

Input Output
["Hello", "HELLO"] [("hello", 2)]
["Zoo", "apple", "APPLE"] [("apple", 2), ("zoo", 1)]
["", ""] [("", 2)]
["Straße", "STRASSE"] [("strasse", 1), ("straße", 1)]

The last pair stays separate because ASCII normalization leaves ß unchanged. Likewise, "É" and "é" remain distinct. This task does not ask for Unicode case folding or Unicode normalization.

Why the starter is wrong

Equal keys must produce equal hashes when hashed with the same algorithm and initial state:

a == b  implies  hash(a) == hash(b)

The starter compares "Hello" and "HELLO" as equal, but its derived Hash passes their original, differently cased strings to the hasher. The map relies on hashing to locate candidates before checking equality, so that mismatch can prevent equivalent words from sharing an entry.

Breaking this contract is a logic error with unspecified results, not undefined behavior. Do not rely on a particular incorrect count to diagnose it. Unequal keys may have the same hash: the map handles those collisions using equality. See the Hash contract.

Hash a consistent representation

Add this implementation to the supplied type and remove #[derive(Hash)]:

use std::hash::{Hash, Hasher};

impl Hash for CiWord {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.to_ascii_lowercase().hash(state);
    }
}

This delegates to the normalized string's Hash implementation. The collection supplies the hasher; your type supplies the data. Every pair accepted by this PartialEq produces the same normalized string, so the two implementations agree. Keep eq_ignore_ascii_case for equality: it compares without allocating a lowercase copy of each string.

What the checks tell you

Clippy's derived_hash_with_manual_eq lint flags the combination used in the starter. It identifies a risky implementation pattern; it does not prove that arbitrary handwritten equality and hashing code agree. The exercise requires passing compilation, test cases and Clippy with warnings denied.

The existing output code normalizes the stored keys and sorts the vector. Keep that sort: HashMap iteration order is arbitrary, while this function's result must be sorted. There is no guarantee that two map iterations or runs will produce different orders.