Skip to content

Instantly share code, notes, and snippets.

@rpearce
Last active May 10, 2022 02:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rpearce/5885173261ca8d22697e to your computer and use it in GitHub Desktop.
Save rpearce/5885173261ca8d22697e to your computer and use it in GitHub Desktop.
Ruby Fundamentals - Working With Collections

Working With Collections

Once we have a collection of information, whether in the form of an Array, Hash, or even an Array of Hashes, 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.

while

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.

Counting to 100

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."

Accessing Array Members

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".

each

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.

With Arrays

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.

With Hashes

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}

With an Array of Hashes

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.

map

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]

With Arrays

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!

With Hashes

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".

With an Array of Hashes

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"]

select

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!

With Arrays

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.

With Hashes

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}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment