In this lesson, we'll be taking a closer look at multidimensional, or nested, hashes, iteration and higher level hash methods. We'll go through a few challenges together before you move on to the next lab.
Let's say we have the following nested hash:
contacts = {
"Jon Snow" => {
name: "Jon",
email: "[email protected]",
favorite_icecream_flavors: ["chocolate", "vanilla"],
knows: nil
},
"Freddy Mercury" => {
name: "Freddy",
email: "[email protected]",
favorite_icecream_flavors: ["strawberry", "cookie dough", "mint chip"]
}
}
Your good buddy Jon Snow has just tried mint chip ice cream for the first time. He loved it a lot and now you need to add "mint chip" to his list of favorite ice creams. We already know how to access the array of ice cream flavors that constitute the value of the :favorite_icecream_flavors
key:
contacts["Jon Snow"][:favorite_icecream_flavors]
# => ["chocolate", "vanilla"]
How can we add a flavor to the list? Well, :favorite_icecream_flavors
is an array, so we can use the same syntax as above to access that array along with our old friend <<
(the shovel method) to add an item to the array. Let's take a look:
contacts["Jon Snow"][:favorite_icecream_flavors] << "mint chip"
puts contacts
# => {
"Jon Snow" => {
name: "Jon",
email: "[email protected]",
favorite_icecream_flavors: ["chocolate", "vanilla", "mint chip"],
knows: nil
},
"Freddy Mercury" => {
name: "Freddy",
email: "[email protected]",
favorite_icecream_flavors: ["strawberry", "cookie dough", "mint chip"]
}
}
Now let's say that you want to add a whole new key/value pair to your Jon Snow contact—his address. In a simpler hash with just one level of key value pairs, we've already learned how to add new key/value pairs. Here's a reminder:
hash = {first: "first value!", second: "second value!"}
hash[:third] = "third value!"
puts hash
# => {first: "first value!", second: "second value!", third: "third value!"}
In a nested hash, we can add new key/value pairs in a similar way. We need to first access the level of the hash to which we want to add a key value pair, and then we can create a new key value on the second level using the same notation. Since we want the "Jon Snow"
key to include a new key/value pair of his address, the implementation should look like this:
contacts["Jon Snow"][:address] = "The Lord Commander's Rooms, The Wall, Westeros"
puts contacts
# =>
{
"Jon Snow" => {
:name=>"Jon", :email=>"[email protected]",
:favorite_icecream_flavors=>["chocolate", "vanilla", "mint chip"],
:knows=>nil,
:address=>"The Lord Commander's Rooms, The Wall, Westeros"},
"Freddy Mercury"=> {
:name=>"Freddy",
:email=>"[email protected]",
:favorite_icecream_flavors=> ["cookie dough", "mint chip"]
}
}
So far, we've only iterated over hashes that had one level—a series of key/value pairs on a single tier. What happens when we want to iterate over a multidimensional hash like the one above? Let's iterate over our nested hash one level at a time; iterating over the first level of our hash would look like this:
contacts = {
"Jon Snow" => {
name: "Jon",
email: "[email protected]",
favorite_icecream_flavors: ["chocolate", "vanilla", "mint chip"],
knows: nil
},
"Freddy Mercury" => {
name: "Freddy",
email: "[email protected]",
favorite_icecream_flavors: ["strawberry", "cookie dough", "mint chip"]
}
}
contacts.each do |person, data|
puts "#{person}: #{data}"
end
This should return:
Jon Snow
{ :name=>"Jon",
:email=>"[email protected]",
:favorite_icecream_flavors=>["chocolate", "vanilla", "mint chip"],
:knows=>nil
}
Freddy:
{ :name=>"Freddy",
:email=>"[email protected]",
:favorite_icecream_flavors=>["strawberry", "cookie dough", "mint chip"]
}
On the first level, the keys are our contacts' names, "Jon Snow" and "Freddy", and our values are the hashes that contain a series of key/value pairs describing them.
Let's iterate over the second level of our contacts
hash. In order to access the key/value pairs of the second tier (i.e. the name, email, and other data about each contact), we need to iterate down into that level. So, we pick up where we left off with the previous iteration and we keep going:
contacts.each do |person, data|
#at this level, "person" is Jon Snow or Freddy and "data" is a hash of key/value pairs
#to iterate over the "data" hash, we can use the following line:
data.each do |attribute, value|
puts "#{attribute}: #{value}"
end
end
That should output the following:
name: Jon
email: jon_snow@thewall.we
favorite_icecream_flavors: ["chocolate", "vanilla", "mint chip"]
knows: nil
name: Freddy
email: freddy@mercury.com
favorite_icecream_flavors: ["strawberry", "cookie dough", "mint chip"]
Let's take is one step further and print out just the favorite ice cream flavors. Once again, we'll need to iterate down into that level of the hash, then we can access the favorite ice cream array and print out the flavors:
contacts.each do |person, data|
#at this level, "person" is Jon Snow or Freddy and "data" is a hash of key/value pairs
#to iterate over the "data" hash, we can use the following line:
data.each do |attribute, value|
#at this level, "attribute" is describes the key of :name, :email, :favorite_icecream_flavors, or :knows
#we need to first check and see if the key is :favorite_icecream_flavors,
#if it is, that means the VALUE is an array that we can iterate over to print out each element
if attribute == :favorite_icecream_flavors
value.each do |flavor|
# here, each index element in an ice cream flavor string
puts "#{flavor}"
end
end
end
end
This should output:
chocolate
vanilla
mint chip
strawberry
cookie dough
mint chip
Now it's your turn! You're going to iterate through the levels of this hash to operate on one of the ice cream flavor arrays.
Reminder: Iterating through nested hashes is hard, and (I'm pretty sure) you are not psychic. Meaning, you can't necessarily predict with perfect clarity what the key/value pair is at a certain level of the hash. Use binding.pry
when you are iterating in upcoming labs to make sure you understand what the key/value pair is that you are iterating over.
Fork and clone this lab. You'll be coding your solution to this first challenge in lib/first_challenge.rb
.
Your good buddy Freddy Mercury has recently developed a strawberry allergy! You need to delete "strawberry"
from his list of favorite ice cream flavors. In the first_challenge
method, we've given you the hash from our previous example.
- Iterate over the
contacts
hash and when you reach the key of:favorite_icecream_flavors
, remove"strawberry"
from the array of Freddy's favorite ice cream flavors. There are at least two ways you can accomplish this:- You can iterate through the hash and, when you reach the appropriate level, check to see if the key
==
("is equal to"):favorite_icecream_flavors
. If it does, check to see if that array contains"strawberry"
. If it does, then delete it from the array. - OR you can directly iterate over the hash that is the value of the
"Freddy Mercury"
key by calling an enumerator method incontacts["Freddy Mercury"]
.
Hint: Use the.delete_if
method to eliminate strawberry from the appropriate array.
Hint: Remember that thefirst_challenge
method needs to return the newly alteredcontacts
hash.
- You can iterate through the hash and, when you reach the appropriate level, check to see if the key
In a previous lab, you were asked to iterate over a hash and collect the key that pointed to the lowest value. We asked you not to use some of the higher level hash methods there. Now, we're going to learn a few tricks that can make a task like that much easier.
You can collect all of the values in a hash with the .values
method:
family_members = {mom: "Victoria", dad: "Richard", sister: "Zoe"}
family_members.values
# => ["Victoria", "Richard", "Zoe"]
We can see that the .values
method returns an array of the values of the keys in the hash.
This method, not surprisingly, returns an array containing all of the keys in the hash that .keys
has been called on:
family_members = {mom: "Victoria", dad: "Richard", sister: "Zoe"}
family_members.keys
# => [:mom, :dad, :sister]
You can use the .min
method on a hash to return the key/value pair that contains that lowest value:
food_items = {apples: 45, pears: 12}
food_items.min
# => [:apples, 45]
These are only a few of the many helpful methods out there. Be there to check out the Ruby Docs on Hashes to learn more.
Let's practice before you move on to the next challenge:
You'll be coding your solution to this challenge in lib/second_challenge.rb
. In the second_challenge
method we have a nested hash of grocery items.
- Use the
.values
method to collect all of the values of the grocery type keys (:dairy
,:vegetables
,:meat
,:grains
). The method should return a one-dimensional (or "flat") array that only includes only the values (groceries such as "milk" and "carrots") without their keys.
Hint: What happens when you call .values
on a nested hash? What is the return value? How can you flatten an array of arrays? Make sure to use binding.pry
to help you solve this one.