- Iterate over a nested hash
In Ruby, it's possible to use enumerable methods to iterate over key-value pairs in hashes. For example:
jon_snow = {
name: "Jon",
email: "jon_snow@thewall.we"
}
jon_snow.each do |key, value|
puts "Key: #{key}"
puts "Value: #{value}"
end
# Key: name
# Value: Jon
# Key: email
# Value: jon_snow@thewall.we
What happens when we want to iterate over a nested hash like the one below? 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: "jon_snow@thewall.we",
favorite_ice_cream_flavors: ["chocolate", "vanilla", "mint chip"],
knows: nil
},
"Freddie Mercury" => {
name: "Freddie",
email: "freddie@mercury.com",
favorite_ice_cream_flavors: ["strawberry", "cookie dough", "mint chip"]
}
}
contacts.each do |person, data|
puts "#{person}: #{data}"
end
This should return:
Jon Snow: {
:name=>"Jon",
:email=>"jon_snow@thewall.we",
:favorite_ice_cream_flavors=>["chocolate", "vanilla", "mint chip"],
:knows=>nil
}
Freddie Mercury: {
:name=>"Freddie",
:email=>"freddie@mercury.com",
:favorite_ice_cream_flavors=>["strawberry", "cookie dough", "mint chip"]
}
On the first level, the keys are our contacts' names, "Jon Snow" and "Freddie Mercury", 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 Freddie Mercury 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_ice_cream_flavors: ["chocolate", "vanilla", "mint chip"]
knows:
name: Freddie
email: freddie@mercury.com
favorite_ice_cream_flavors: ["strawberry", "cookie dough", "mint chip"]
Let's take it 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 Freddie 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" describes the key of :name, :email,
#:favorite_ice_cream_flavors, or :knows we need to first check and see if
#the key is :favorite_ice_cream_flavors, if it is, that means the VALUE is
#an array that we can iterate over to print out each element
if attribute == :favorite_ice_cream_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
Being able to access data from a nested hash like this gives us a lot of ways to work with this complex data structure and derive insights. What if instead of printing out the favorite ice creams of our contacts, we wanted to collect their email addresses in an array? Well, we could do something like this:
emails = []
contacts.each do |person, data|
data.each do |attribute, value|
if attribute == :email
emails << value
end
end
end
emails
Fork and clone this exercise to code along!
Let's do an exercise to get some practice iterating through nested arrays.
You'll be coding your solution in contacts.rb
. You'll be manipulating the hash
that is returned by the #contacts
method:
def contacts
{
"Jon Snow" => {
name: "Jon",
email: "jon_snow@thewall.we",
favorite_ice_cream_flavors: ["chocolate", "vanilla"]
},
"Freddie Mercury" => {
name: "Freddie",
email: "freddie@mercury.com",
favorite_ice_cream_flavors: ["strawberry", "cookie dough", "mint chip"]
}
}
end
Your good buddy Freddie Mercury has recently developed a strawberry allergy! You
need to delete "strawberry"
from his list of favorite ice cream flavors in the
remove_strawberry
method.
Iterate over the contacts
hash and when you reach the key
:favorite_ice_cream_flavors
, remove "strawberry"
from the Array of Freddie's
favorite ice cream flavors.
There are at least two ways you can accomplish this, and for this code along, we'll work with the second way.
-
You can directly iterate over the hash that is the value of the
"Freddie Mercury"
key by calling an enumerator method incontacts["Freddie Mercury"]
. -
You can set a iterate through the hash and check for
Freddie Mercury
only; when you reach the appropriate level, check to see if the key==
("is equal to"):favorite_ice_cream_flavors
. If it is, check to see if the array of flavors contains"strawberry"
. If it does, then delete it from the array.
Inside the #remove_strawberry
method, let's take our first dive into the
contacts hash. Then we'll use binding.pry
to see where we are.
We are going to first iterate over the top level of the hash where the keys should be the person and the values should be a hash of details about the person.
Note on variable naming: This process will be remarkably easier if you name
your variables to accurately reflect the data they represent. For now, when the
value we're iterating over is another hash, we will explicitly add a _hash
to
the end of the variable name (like contact_details_hash
below).
Add this code inside the #remove_strawberry
method:
contacts.each do |person, contact_details_hash|
binding.pry
end
We can enter the Pry session in one of two ways: by running learn test
or by
running ruby contacts.rb
. We'll use learn test
.
Run learn test
in the terminal and, at the Pry prompt, check that our defined
variables (person
and contact_details_hash
) match our expectations.
person
# => "Jon Snow"
contact_details_hash
# => {:name=>"Jon", :email=>"jon_snow@thewall.we", :favorite_ice_cream_flavors=>["chocolate", "vanilla"]}
Excellent! They do!
Type exit
while in Pry to continue. The binding.pry
breakpoint should
trigger a second time because we have two contacts. You can verify that we're
in the second loop through our hash by checking the values of person
and
contact_details_hash
at the Pry prompt.
Typing exit
now will end the loop and exit Pry since we've finished
iterating through our contacts. It will also display the results of the test,
which we haven't passed just yet.
Update your code to match the following:
def remove_strawberry(contacts)
contacts.each do |person, contact_details_hash|
if person == "Freddie Mercury"
contact_details_hash.each do |attribute, data|
binding.pry
end
end
end
end
Again, let's jump into our binding.pry
using learn test
. We can verify that
we've found the record for Freddie Mercury by checking the values of our
variables:
attribute
# => :name
data
# => "Freddie"
Remember, if you get stuck and can't enter text in Pry, hit
q
to continue!
Before we move on, you will need to exit pry
again so you can see the results
of the new code we'll be writing in Step 3. We are now inside the loop through
the attributes. Because there are three of them, we will need to run exit
three times to finish the loop and exit pry
. Alternatively, you can run
exit-program
or !!!
at any time to exit out of pry
entirely.
Update your code to match the following:
def remove_strawberry(contacts)
contacts.each do |person, contact_details_hash|
if person == "Freddie Mercury"
contact_details_hash.each do |attribute, data|
if attribute == :favorite_ice_cream_flavors
binding.pry
end
end
end
end
end
This time we are still iterating through the attributes but we've added a
conditional so the pry
will only hit when the attribute is equal to
:favorite_ice_cream_flavors
. If we check the value of data
in our binding,
we should see the array containing Freddie's favorite flavors.
Lastly, we will use the #delete_if
array method to iterate through the ice
cream array and remove any element that matches "strawberry". Recall that data
is the array containing Freddie's favorite ice cream flavors. #delete_if
will
iterate through the array, check each element to see if it is equal to
"strawberry", and delete the element if the block returns true
. Learn
more about #delete_if
in the ruby docs..
The full method should now be:
def remove_strawberry(contacts)
contacts.each do |person, contact_details_hash|
if person == "Freddie Mercury"
contact_details_hash.each do |attribute, data|
if attribute == :favorite_ice_cream_flavors
data.delete_if {|ice_cream| ice_cream == "strawberry"}
end
end
end
end
end
Congrats! You made it. Test that your method works by running ruby contacts.rb
in the terminal. It should output the hash without strawberry ice cream. Also,
be sure to run the specs to make sure they pass.