Once we have a collection of information, whether in the form of an Array
, Hash
, or even an Array
of Hash
es, we often need to perform operations on and ask questions of this data. Writing out the code for every single member of a collection is tedious and does not scale to handle dynamic collection sizes. Instead, we can write our code once and apply it to every member of a collection and keep things DRY1 (don't repeat yourself!).
In this lesson, we will be introduced to loops and understand the power they give us when working with collections.
Before we can apply loops to collections, we first need to know how one works! Here is what a very basic while
loop looks like:
while 1 < 2
# code to be run goes here
end
A while
loop will continue to execute the code before its end
(typically known as a block) over and over so long as the condition, which in the above example is 1 < 2
, evaluates to be truthy. After the code within the block is executed, Ruby goes back to this condition, asks if it's still truthy, and if it is, will enter the block and run the code again. Since this condition will never be false (1
is always less than 2
!), the code above creates something called an infinite loop. Let's use our knowledge of variables to leverage loops for more practical examples.
We need to puts
to the Terminal all of the numbers between 1
and 100
. Writing puts
100 times is not practical, and what would we do if we needed to count to 1000
? One way we can solve this is to increment a variable that we start with the value of 1
and then stop our loop once the variable reaches the value 100
.
i = 1
while i <= 100
puts i
i += 1
end
Since i
is a variable whose value can be mutated (changed), the i += 1
line is what increments the value of i
. Once this incrementing happens, there's nothing else for the while loop to evaluate, so it looks at its condition (i <= 100
) and asks, "Is the current value of i
less than or equal to 100? If so, run the loop code again; if not, then ignore the loop code."
Members of an array all have a unique index, and the first member index always starts at 0
. The other members are incrementally indexed, meaning the index of the next item in the array is one more than the current index. If we combine our knowledge of incrementing a variable's value using a while
loop with our knowledge of array indexing, we can access each member of an array from within our loop!
films = ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"]
i = 0
while i < films.length
puts films[i]
i += 1
end
# A New Hope
# The Empire Strikes Back
# Return of the Jedi
When the value of i
is 0
, then films[i]
is really evaluated as films[0]
, which is "A New Hope"
. Likewise, when i
is 2
, then films[i]
is really evaluated as films[2]
, which is "Return of the Jedi"
.
We can be freed from maintaining the counter and conditional aspect of looping over an array or hash with the each
command. In this section, we will use it with arrays and hashes. One thing to keep in mind is that the each
command always returns the array's or hash's value when you start the loop, regardless of what happens inside the loop.
While while
loops are incredibly useful and can help solve any array looping operations you require, it does have the added overhead of having to manage the actual incrementing and array accessing yourself. It would be great if we could simply get access to each item in an array without the additional cognitive overhead! The each
command on an array lets us do just that.
films = ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"]
films.each do |film|
puts film
end
# A New Hope
# The Empire Strikes Back
# Return of the Jedi
# => ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"]
The do
and end
, which can both be substituted with {
and }
, respectively, create something called a block (a bit of enclosed code that typically starts with a do
and stops with an end
) which then gives you access to one individual array item at a time to which you can assign a variable name. We call ours film
, but between the |
and |
, you can give this temporary local variable any name you wish. The each
command tells the array to loop over itself and do whatever is in the block.
A hash is essentially a collection of key and value pairs: { name: "Vader", age: 45 }
. When it comes to looping, instead of having a singular value like we do with an array item, we can consider each key and value pair (name
and "Vader"
) to be a single item. Here is how we can use each
to iterate over each key and value pair in a hash:
character = { name: "Vader", age: 45 }
character.each do |key, value|
puts "\"#{key}\" has a value of \"#{value}\""
end
# "name" has a value of "Vader"
# "age" has a value of "45"
# => {:name=>"Vader", :age=>45}
If we have many different hashes, they can be grouped in to an array of hashes, so that we can iterate over them and extract data.
characters = [{ name: "Leia", age: 22 }, { name: "Han", age: 30 }, { name: "Vader", age: 45 }]
characters.each do |character|
character.each do |key, value|
puts "\"#{key}\" has a value of \"#{value}\""
end
end
Here, we combine the tools from the previous two sections to create a nested each
where we loop over the characters
array and then loop over each character
's hash, giving us access to each key
and value
for each character
.
On the surface, map
might sound and look a bit like each
, but where each
always returns the original array or hash, map
returns a new array whose values are determined by what is returned from its block. Here is a quick demonstration of map
vs. each
:
[1, 2, 3].each do |number|
number * 2
end
# => [1, 2, 3]
[1, 2, 3].map do |number|
number * 2
end
# => [2, 4, 6]
Imagine we have been tasked with seeing what a list of films
looks like in uppercase but without altering the original array. We can use map
to create a new array from the original one to do just that and store the upcased values in to a variable.
films = ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"]
upcased_films = films.map do |film|
film.upcase
end
films # => ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"]
upcased_films # => ["A NEW HOPE", "THE EMPIRE STRIKES BACK", "RETURN OF THE JEDI"]
As you can see, films
retained its original value, and we now have an uppercased copy called upcased_films
.
If you want to shorten the map
assignment and block to one line, you could write
upcased_films = films.map { |film| film.upcase }
and if you find yourself doing nothing more than executing a single command on each item, can even shorten it to
upcased_films = films.map(&:upcase)
This last bit is more advanced than is relevant for the scope of this lesson, but it's good to know about!
Given we have a character
hash, we need a way to get a list of all of its keys for table headers in some part of our application. We can do that easily with map
on our hash:
character = { name: "Vader", age: 45 }
character.map { |key, value| key }
# => [:name, :age]
Now we need a list of all the values but with "-factCheck"
appended to each value! No problem; map
's got us covered:
character = { name: "Vader", age: 45 }
character.map { |key, value| "#{value}-factCheck" }
# => ["Vader-factCheck", "45-factCheck"]
Remember: whatever is returned from map
's block is what will be set as that item's value in the newly created array. In the last case, it is "#{value}-factCheck"
.
Consider that our boss has come back and told us we need to get a list of all the characters
' names. Fear not! We can map
over the array and ask to extract the value for each one's :name
property:
characters = [{ name: "Leia", age: 22 }, { name: "Han", age: 30 }, { name: "Vader", age: 45 }]
characters.map { |character| character[:name] }
# => ["Leia", "Han", "Vader"]
We know that map
is awesome, but there's a problem: map
always returns the same number of array items as are provided, and it gives us back an array most of the time. What if we need to search for certain values and only get those back? That's where select
comes in!
Let's say we need to return all of the films
that contain the word the
(case insensitive). We can use select
and a regular expression2 to solve this:
films = ["A New Hope", "The Empire Strikes Back", "Return of the Jedi"]
films.select { |film| film =~ /the/i }
# => ["The Empire Strikes Back", "Return of the Jedi"]
This works by looping over all of the films, and if a film
string has the
in it — regardless of case — then the current value of film
will be added to a new array that will be returned.
When select
is used on a hash, it will return a new hash with only the keys and values that you asked for. If we have a hash containing planets and their populations, we can select ones that meet certain criteria with select
.
populations = {
coruscant: 1_000_000_000_000,
tatooine: 120_000,
endor: 30_000_000,
bespin: 3_000_000
}
# Which have a population greater than 1,000,000?
populations.select { |planet, population| population > 1_000_000 }
# => {:coruscant=>1000000000000, :endor=>30000000, :bespin=>3000000}
# Which have a population less than 500,000?
populations.select { |planet, population| population < 500_000 }
# => {:tatooine=>120000}