12 Rustlings HashMaps Solution

Hashmaps

From the Rustlings README

A hash map allows you to associate a value with a particular key. You may also know this by the names unordered map in C++, dictionary in Python or an associative array in other languages.

This is the other data structure that we've been talking about before, when talking about Vecs.

Further information

hashmaps1.rs

// hashmaps1.rs
// A basket of fruits in the form of a hash map needs to be defined.
// The key represents the name of the fruit and the value represents
// how many of that particular fruit is in the basket. You have to put
// at least three different types of fruits (e.g apple, banana, mango)
// in the basket and the total count of all the fruits should be at
// least five.
//
// Make me compile and pass the tests!
//
// Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

use std::collections::HashMap;

fn fruit_basket() -> HashMap<String, u32> {
    let mut basket = // TODO: declare your hash map here.

    // Two bananas are already given for you :)
    basket.insert(String::from("banana"), 2);

    // TODO: Put more fruits in your basket here.

    basket
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn at_least_three_types_of_fruits() {
        let basket = fruit_basket();
        assert!(basket.len() >= 3);
    }

    #[test]
    fn at_least_five_fruits() {
        let basket = fruit_basket();
        assert!(basket.values().sum::<u32>() >= 5);
    }
}

Our instructions are to put more fruit into our HashMap basket, we need at least 3 different kinds of fruits and the total count of all fruits should be at least five. In the comments we see more instructions via TODO's which are:

  • to declare a hash map
  • put more fruits in our basket.

hashmaps1.rs errors

A quick glance of our errors shows nothing unexpected:

⚠️  Compiling of exercises/hashmaps/hashmaps1.rs failed! Please try again. Here's the output:
error[E0425]: cannot find value `basket` in this scope
  --> exercises/hashmaps/hashmaps1.rs:21:5
   |
21 |     basket.insert(String::from("banana"), 2);
   |     ^^^^^^ not found in this scope

error: aborting due to previous error

For more information about this error, try `rustc --explain E0425`.

hashmaps1 solution

So now let's move on to the solution. We first declare our HashMap by completing line 5 with let mut basket = HashMap::new(); That let's us create our hashmap so it's usable in the next lines, meaning we can add some more fruit to it. We already have our first line defined as basket.insert(String::from("banana"), 2); so if we use this same pattern but add different fruits, keeping in mind our quantity requirements, we should be good.

use std::collections::HashMap;

fn fruit_basket() -> HashMap<String, u32> {
    // TODO: declare your hash map here.
    let mut basket = HashMap::new();

    // Two bananas are already given for you :)
    basket.insert(String::from("banana"), 2);
    basket.insert(String::from("apple"), 2);
    basket.insert(String::from("mango"), 1);
    // TODO: Put more fruits in your basket here.

    basket
}

Easy enough, we declare a new HashMap and add some fruit. Let's move on to the next one!

hashmap2.rs

// hashmaps2.rs

// A basket of fruits in the form of a hash map is given. The key
// represents the name of the fruit and the value represents how many
// of that particular fruit is in the basket. You have to put *MORE
// THAN 11* fruits in the basket. Three types of fruits - Apple (4),
// Mango (2) and Lychee (5) are already given in the basket. You are
// not allowed to insert any more of these fruits!
//
// Make me pass the tests!
//
// Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

use std::collections::HashMap;

#[derive(Hash, PartialEq, Eq)]
enum Fruit {
    Apple,
    Banana,
    Mango,
    Lychee,
    Pineapple,
}

fn fruit_basket(basket: &mut HashMap<Fruit, u32>) {
    let fruit_kinds = vec![
        Fruit::Apple,
        Fruit::Banana,
        Fruit::Mango,
        Fruit::Lychee,
        Fruit::Pineapple,
    ];

    for fruit in fruit_kinds {
        // TODO: Put new fruits if not already present. Note that you
        // are not allowed to put any type of fruit that's already
        // present!
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn get_fruit_basket() -> HashMap<Fruit, u32> {
        let mut basket = HashMap::<Fruit, u32>::new();
        basket.insert(Fruit::Apple, 4);
        basket.insert(Fruit::Mango, 2);
        basket.insert(Fruit::Lychee, 5);

        basket
    }

    #[test]
    fn test_given_fruits_are_not_modified() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        assert_eq!(*basket.get(&Fruit::Apple).unwrap(), 4);
        assert_eq!(*basket.get(&Fruit::Mango).unwrap(), 2);
        assert_eq!(*basket.get(&Fruit::Lychee).unwrap(), 5);
    }

    #[test]
    fn at_least_five_types_of_fruits() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        let count_fruit_kinds = basket.len();
        assert!(count_fruit_kinds >= 5);
    }

    #[test]
    fn greater_than_eleven_fruits() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        let count = basket.values().sum::<u32>();
        assert!(count > 11);
    }
}

In the given code, the goal is to modify the fruit_basket function to add more than 11 fruits of various kinds to a hash map (HashMap) called basket. The hash map represents a basket of fruits, where the keys are different types of fruits (represented by the Fruit enum) and the values are the number of each fruit in the basket (u32).

The fruit_basket function should insert additional fruit types into the basket hash map without modifying the quantities of the fruits that are already present (Apple, Mango, and Lychee) since they are already given in the initial basket.

The provided test cases ensure that the given fruits are not modified, that there are at least five types of fruits in the basket after the function execution, and that there are more than eleven fruits in total in the basket.

hashmap2.rs solution

use std::collections::HashMap;

#[derive(Hash, PartialEq, Eq)]
enum Fruit {
    Apple,
    Banana,
    Mango,
    Lychee,
    Pineapple,
    Pear,
    Kiwi,
    Strawberries,
    Blueberries,
    Cherries,
    Lemons,
    Grapefruit,
}

fn fruit_basket(basket: &mut HashMap<Fruit, u32>) {
    let fruit_kinds = vec![
        Fruit::Apple,
        Fruit::Banana,
        Fruit::Mango,
        Fruit::Lychee,
        Fruit::Pineapple,
        Fruit::Pear,
        Fruit::Kiwi,
        Fruit::Strawberries,
        Fruit::Blueberries,
        Fruit::Cherries,
        Fruit::Lemons,
        Fruit::Grapefruit,
    ];

    for fruit in fruit_kinds {
        basket.insert(Fruit::Pear, 2);
        basket.insert(Fruit::Kiwi, 10);
        basket.insert(Fruit::Strawberries, 5);
        basket.insert(Fruit::Lemons, 6);
        basket.insert(Fruit::Strawberries, 6);
        basket.insert(Fruit::Blueberries, 6);
        basket.insert(Fruit::Grapefruit, 6);
        basket.insert(Fruit::Cherries, 6);
    }
}

This solution involves adding more fruit types to the fruit_kinds vector, and then iterating through this vector to insert the new fruits into the basket hash map using the insert method. This includes adding multiple instances of new fruit types to meet the requirement of having more than eleven fruits in the basket.

In our code, we add several new fruit types (e.g., Pear, Kiwi, Strawberries, Blueberries, Cherries, Lemons, and Grapefruit), and multiple instances of each fruit are inserted into the basket hash map.

This satisfies the requirements, passing all the given test cases, and ensuring that the function can add additional fruit types to the basket while preserving the initial quantities of the given fruits.

hashmaps3.rs

// hashmaps3.rs

// A list of scores (one per line) of a soccer match is given. Each line
// is of the form :
// <team_1_name>,<team_2_name>,<team_1_goals>,<team_2_goals>
// Example: England,France,4,2 (England scored 4 goals, France 2).

// You have to build a scores table containing the name of the team, goals
// the team scored, and goals the team conceded. One approach to build
// the scores table is to use a Hashmap. The solution is partially
// written to use a Hashmap, complete it to pass the test.

// Make me pass the tests!

// Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

use std::collections::HashMap;

// A structure to store team name and its goal details.
struct Team {
    name: String,
    goals_scored: u8,
    goals_conceded: u8,
}

fn build_scores_table(results: String) -> HashMap<String, Team> {
    // The name of the team is the key and its associated struct is the value.
    let mut scores: HashMap<String, Team> = HashMap::new();

    for r in results.lines() {
        let v: Vec<&str> = r.split(',').collect();
        let team_1_name = v[0].to_string();
        let team_1_score: u8 = v[2].parse().unwrap();
        let team_2_name = v[1].to_string();
        let team_2_score: u8 = v[3].parse().unwrap();
        // TODO: Populate the scores table with details extracted from the
        // current line. Keep in mind that goals scored by team_1
        // will be the number of goals conceded from team_2, and similarly
        // goals scored by team_2 will be the number of goals conceded by
        // team_1.
    }
    scores
}

#[cfg(test)]
mod tests {
    use super::*;

    fn get_results() -> String {
        let results = "".to_string()
            + "England,France,4,2\n"
            + "France,Italy,3,1\n"
            + "Poland,Spain,2,0\n"
            + "Germany,England,2,1\n";
        results
    }

    #[test]
    fn build_scores() {
        let scores = build_scores_table(get_results());

        let mut keys: Vec<&String> = scores.keys().collect();
        keys.sort();
        assert_eq!(
            keys,
            vec!["England", "France", "Germany", "Italy", "Poland", "Spain"]
        );
    }

    #[test]
    fn validate_team_score_1() {
        let scores = build_scores_table(get_results());
        let team = scores.get("England").unwrap();
        assert_eq!(team.goals_scored, 5);
        assert_eq!(team.goals_conceded, 4);
    }

    #[test]
    fn validate_team_score_2() {
        let scores = build_scores_table(get_results());
        let team = scores.get("Spain").unwrap();
        assert_eq!(team.goals_scored, 0);
        assert_eq!(team.goals_conceded, 2);
    }
}

Our task in this exercise is to build a scores table for soccer matches using a HashMap. We are given a list of match scores in the form of <team_1_name>,<team_2_name>,<team_1_goals>,<team_2_goals>. For example, "England,France,4,2" means England scored 4 goals, and France scored 2 goals in a match.

To build the scores table, we need to use a HashMap to store each team's name, the goals they scored, and the goals they conceded in the matches.

hashmaps3.rs errors

⚠️  Testing of exercises/hashmaps/hashmaps3.rs failed! Please try again. Here's the output:

running 3 tests
test tests::build_scores ... FAILED
test tests::validate_team_score_1 ... FAILED
test tests::validate_team_score_2 ... FAILED

successes:

successes:

failures:

---- tests::build_scores stdout ----
thread 'tests::build_scores' panicked at 'assertion failed: `(left == right)`
  left: `[]`,
 right: `["England", "France", "Germany", "Italy", "Poland", "Spain"]`', exercises/hashmaps/hashmaps3.rs:66:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- tests::validate_team_score_1 stdout ----
thread 'tests::validate_team_score_1' panicked at 'called `Option::unwrap()` on a `None` value', exercises/hashmaps/hashmaps3.rs:75:42

---- tests::validate_team_score_2 stdout ----
thread 'tests::validate_team_score_2' panicked at 'called `Option::unwrap()` on a `None` value', exercises/hashmaps/hashmaps3.rs:83:40


failures:
    tests::build_scores
    tests::validate_team_score_1
    tests::validate_team_score_2

test result: FAILED. 0 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

hasmap3.rs solution


use std::collections::HashMap;

// A structure to store team name and its goal details.
struct Team {
    name: String,
    goals_scored: u8,
    goals_conceded: u8,
}

fn build_scores_table(results: String) -> HashMap<String, Team> {
    // The name of the team is the key and its associated struct is the value.
    let mut scores: HashMap<String, Team> = HashMap::new();

    for r in results.lines() {
        let v: Vec<&str> = r.split(',').collect();
        let team_1_name = v[0].to_string();
        let team_1_score: u8 = v[2].parse().unwrap();
        let team_2_name = v[1].to_string();
        let team_2_score: u8 = v[3].parse().unwrap();

        // Try to get a mutable reference to the first team's Team struct in the scores HashMap
        scores
            .entry(team_1_name.clone())
            // If the Team struct exists, modify it by incrementing the goals_scored and goals_conceded fields
            .and_modify(|team| {
                team.goals_scored += team_1_score;
                team.goals_conceded += team_2_score;
            })
            // If the Team struct does not exist, insert a new one with the initial scores
            .or_insert(Team {
                name: team_1_name,
                goals_scored: team_1_score,
                goals_conceded: team_2_score,
            });

        // Repeat the same process for the second team
        scores
            .entry(team_2_name.clone())
            .and_modify(|team| {
                team.goals_scored += team_2_score;
                team.goals_conceded += team_1_score;
            })
            .or_insert(Team {
                name: team_2_name,
                goals_scored: team_2_score,
                goals_conceded: team_1_score,
            });
    }
    scores
}

The code already provides a Team struct with fields for the team's name, goals scored, and goals conceded. Our task is to complete the build_scores_table function to create and populate the HashMap with the match results.

Here's our plan to complete the function:

  1. We'll start by creating an empty HashMap called scores to store the team details.

  2. Next, we'll iterate through each line of the results string using results.lines().

  3. For each line, we'll split it into components using .split(',') to extract the team names and goals.

  4. We'll convert the necessary fields to their correct types (e.g., team names as strings, goals as u8).

  5. Then, we'll populate the scores HashMap with the team details. We need to be careful because goals scored by one team are the goals conceded by the other team.

  6. Finally, we'll return the scores HashMap from the function.

Once we complete the function, the provided test cases will validate that the scores table is correctly built, and each team's goals scored and conceded are accurate.

This would print "England,France,4,2\nFrance,Italy,3,1\n"

Each line of the results string represents one match, with the teams and their respective scores separated by commas. This loop goes through each line one at a time, parsing the teams and scores and updating the scores HashMap accordingly.

The entry() method tries to get a mutable reference to the Team struct associated with a team's name. If the Team struct exists, the and_modify() method modifies it by adding the new scores to the goals_scored and goals_conceded fields. If the Team struct doesn't exist, the or_insert() method inserts a new Team struct with the initial scores. This is done for both teams in each match. After all the matches have been processed, the scores HashMap is returned, with each team's total goals scored and conceded updated to reflect all the matches.

Alternate Solution using If / Let

fn build_scores_table(results: String) -> HashMap<String, Team> {
    // The name of the team is the key and its associated struct is the value.
    let mut scores: HashMap<String, Team> = HashMap::new();

    for r in results.lines() {
        let v: Vec<&str> = r.split(',').collect();
        let team_1_name = v[0].to_string();
        let team_1_score: u8 = v[2].parse().unwrap();
        let team_2_name = v[1].to_string();
        let team_2_score: u8 = v[3].parse().unwrap();

        // Check if the first team exists in the scores HashMap
        if let Some(team) = scores.get_mut(&team_1_name) {
            // If the team exists, increment the goals_scored and goals_conceded fields
            team.goals_scored += team_1_score;
            team.goals_conceded += team_2_score;
        } else {
            // If the team does not exist, insert a new one with the initial scores
            scores.insert(
                team_1_name.clone(),
                Team {
                    name: team_1_name.clone(),
                    goals_scored: team_1_score,
                    goals_conceded: team_2_score,
                },
            );
        }

        // Repeat the same process for the second team
        if let Some(team) = scores.get_mut(&team_2_name) {
            team.goals_scored += team_2_score;
            team.goals_conceded += team_1_score;
        } else {
            scores.insert(
                team_2_name.clone(),
                Team {
                    name: team_2_name.clone(),
                    goals_scored: team_2_score,
                    goals_conceded: team_1_score,
                },
            );
        }
    }
    scores
}

In this version, if let Some(team) = scores.get_mut(&team_1_name) tries to get a mutable reference to the team in the scores HashMap. If the team exists, Some(team) is returned and the team's scores are updated. If the team does not exist, None is returned and a new Team struct is inserted into the scores HashMap with the scores.insert() method. This is repeated for both teams.

The logic is the same as in the previous version that uses closures, but the control flow is more explicit here. However, this version is slightly more verbose and arguably less idiomatic in Rust. Closures and method chaining using and_modify and or_insert on HashMap::entry are common patterns in Rust code.

3rd Possible Solution

fn build_scores_table(results: String) -> HashMap<String, Team> {
    let mut scores: HashMap<String, Team> = HashMap::new();

    for r in results.lines() {
        let v: Vec<&str> = r.split(',').collect();
        let team_1_name = v[0].to_string();
        let team_1_score: u8 = v[2].parse().unwrap();
        let team_2_name = v[1].to_string();
        let team_2_score: u8 = v[3].parse().unwrap();

        // Handle team 1
        let team_1 = scores.remove(&team_1_name);
        if let Some(mut t) = team_1 {
            t.goals_scored += team_1_score;
            t.goals_conceded += team_2_score;
            scores.insert(team_1_name, t);
        } else {
            scores.insert(team_1_name, Team { name: team_1_name.clone(), goals_scored: team_1_score, goals_conceded: team_2_score });
        }

        // Handle team 2
        let team_2 = scores.remove(&team_2_name);
        if let Some(mut t) = team_2 {
            t.goals_scored += team_2_score;
            t.goals_conceded += team_1_score;
            scores.insert(team_2_name, t);
        } else {
            scores.insert(team_2_name, Team { name: team_2_name.clone(), goals_scored: team_2_score, goals_conceded: team_1_score });
        }
    }
    scores
}

In this version, scores.remove(&team_1_name) attempts to remove the Team struct associated with team_1_name from the HashMap. If the team exists, the Team struct is returned and removed from the HashMap, the team's scores are updated, and the updated Team struct is re-inserted into the HashMap. If the team does not exist, a new Team struct is inserted into the HashMap. This process is repeated for both teams.

Although this version is simpler and doesn't use closures, it's also less efficient because it requires additional lookups and insertions into the HashMap. The previous two versions that use HashMap::entry are generally more efficient and idiomatic in Rust.

Conclusion

In this post, we explored the concept of HashMaps in Rust and how they allow us to associate values with specific keys. HashMaps are similar to unordered maps in C++, dictionaries in Python, or associative arrays in other languages. We learned how to use the HashMap data structure to build collections with key-value pairs.

We worked through three exercises that involved using HashMaps to store and manipulate data:

  1. In hashmaps1.rs, we had to create a basket of fruits using a HashMap. We needed to ensure that the basket contained at least three different types of fruits and a total of at least five fruits. By declaring a new HashMap and adding the required fruits with their quantities, we successfully completed this task.

  2. In hashmaps2.rs, we were given a pre-defined basket of fruits and had to add more than eleven fruits of various kinds to it. However, we were not allowed to modify the quantities of the given fruits (Apple, Mango, and Lychee). By iterating through a list of new fruit types and inserting them into the HashMap, we successfully met the requirements of this exercise.

  3. In the exercise hashmaps3.rs, we were tasked with building a scores table for soccer matches using a HashMap. Three possible solutions were provided to achieve this goal:

Solution 1: Using Closures and entry() Method Chaining This solution utilized closures and method chaining with entry() to efficiently handle the teams' scores and update the HashMap accordingly.

Solution 2: Using if let Statements for Handling Entries In this solution, if let statements were used to handle entries in the HashMap, removing and updating teams' scores as needed.

Solution 3: Using if let and match for Handling Entries This solution combined if let with match to handle the entries in the HashMap, similar to Solution 2, but with a slight variation in the syntax.

All three solutions achieved the goal of building the scores table and were valid approaches to solving the problem. The choice between these solutions may depend on personal coding style and preferences.

Key Takeaways:

  • HashMaps in Rust provide fast lookup and insertion times.
  • They enable efficient data retrieval and manipulation based on unique keys.
  • HashMaps are valuable tools for various scenarios, such as building scores tables or maintaining associations between data elements.

By mastering the usage of HashMaps, we expand our ability to manage and organize data efficiently in Rust. These skills are valuable for building robust and performant applications in a wide range of domains.