Iteration Nation
Using arrays and hashes in Ruby
Posted Oct. 4, 2015
I began my discussion of collections in Ruby—specifically, arrays and hashes—in my last post, Taking up collections. In this post, I'll look at how we can put arrays and hashes to work.
One of the main reasons we use collections like arrays and hashes to store related data is so that we can work with the entire collection...collectively. We often want to search through a collection for specific bits of information, or perform the same action on every single item of the set. Because we've organized our collections into arrays and hashes, Ruby lets us accomplish these things easily, using iteration.
Thrown for a loop
When we iterate over an array or hash, we systematically apply the same bit of code to each item in the collection, exactly one time each. Because arrays are accessed by numeric indexes, we have more flexibility in how we iterate over them than we do with hashes. For starters, we can use simple loops, limited by counters based on the array.size
, to do something to every item in an array. (Did you notice I slipped a new method, size
, in there? It returns an integer value equal to the number of items in the array.) Here, we'll use an until
loop to print out all the members of a new array, beatles
:
- $ beatles = [ "John", "Paul", "George", "Ringo" ]
- => ["John", "Paul", "George", "Ringo"]
- $ counter = 0
- => 0
- $ until counter >= beatles.size do
- $ puts beatles[counter]
- $ counter += 1
- $ end
- John
- Paul
- George
- Ringo
- => nil
In this example, our counter started at zero, and went up by 1 with each iteration of the loop. By the time the program had printed "Ringo," the counter had counted up to four. Since the counter was no longer less than the size of the array, the "until" condition was fulfilled, and the loop terminated, having printed the name of each member of the Beatles to the terminal. Note also that when it finishes its work, until
returns nil.
To each, his own
Because we don't use numeric indexes to access the values in hashes, we can't use counters to loop through them with simple control expressions like while
and until
. Fortunately, we have the each
method, an iterator that does much the same thing, and doesn't need us to provide a counter for it to work. Let's convert our Beatles into a hash, so we can store more informatiom about them. Then we'll use each
to print it back out.
- $ beatles = {
- $ :lead_vocals => "John",
- $ :bass => "Paul",
- $ :lead_guitar => "George",
- $ :drums => "Ringo"
- $ }
- => {:lead_vocals=>"John", :bass=>"Paul", :lead_guitar=>"George", :drums=>"Ringo"}
- $ beatles.each do |key, value|
- $ puts value + " played " + key.to_s + "."
- $ end
- John played lead_vocals.
- Paul played bass.
- George played lead_guitar.
- Ringo played drums.
- => {:lead_vocals=>"John", :bass=>"Paul", :lead_guitar=>"George", :drums=>"Ringo"}
Sure, the sentences are clunky, but you get the idea. Conveniently, arrays have an each
method as well. Though what's going on under the hood for the array version of each
is a bit different than it is for hash version, the syntax is just about the same. Since Ruby lets us put our each
block on a single line if it's simple enough, we'll try that here:
- $ beatles = [ "John", "Paul", "George", "Ringo" ]
- => ["John", "Paul", "George", "Ringo"]
- $ beatles.each { |member| puts beatle + " was a Beatle." }
- John was a Beatle.
- Paul was a Beatle.
- George was a Beatle.
- Ringo was a Beatle.
- => ["John", "Paul", "George", "Ringo"]
Unlike simple control expressions like until,
methods like each
usually return a value which may be of use to us in a more sophisticated program than these simplistic examples. In the case of each,
the value returned is the original array or hash. If you need to capture the value produced by each
method, you'll need to create another array or hash to capture the new values:
- $ numbers = [ 3, 5, 8 ]
- => [3, 5, 8]
- $ bigger_numbers = []
- => []
- $ numbers.each do |number|
- $ bigger_numbers.push number*5
- $ end
- => [3, 5, 8]
- $ print "The numbers are " + numbers.to_s + ".\n"
- The numbers are [3, 5, 8].
- =>nil
- $ print "The bigger numbers are " + bigger_numbers.to_s + ".\n"
- The bigger numbers are [15, 25, 40].
- =>nil
If you want to learn more about Ruby loops and iterators, read Alan Skorkin's short-but-sweet introduction to the topic, A Wealth of Ruby Loops and Interators.
The right tool for the job
You've got two different kinds of collections in Ruby&mspace;arrays and hashes&mspace;each with its own strengths. So how do you choose which one to use in any given situation? You have to think about what kind of data you're planning to store, and what you plan to do with it.
If you've got a list made up of lots of examples of the same kind of thing, such as moves on a chessboard, flavors of ice cream, or your weight recorded every morning for a month, you'll probably want an array. Storing this sort of data in an array makes it easy to put in alphanumeric order, use in statistical calculations (such as finding the min, max, median, average, etc.), eliminate duplications, and add more values to. If the order of the items in your collection matters, and especially if you anticipate reorganizing that order, then an array is definitely your best bet.
On the other hand, if your data includes many different kinds of information about a single topic, you probably want a hash. A hash is well-suited to storing customer data collected from a form, for example, which might include strings (first and last name, address), customer number and order number (integers), and products ordered (array). Storing this information in a hash will allow you to attach a label, in the form of a hash key, to each piece of information, such as :first_name
, :last_name
, :street_address
, etc. You can then use the hash keys to retrieve the exact value you need, such as the customer's zip code, instead of having to iterate through the collection searching for it, as you would have to do if your customer information were stored in an array.
Arrays and hashes become even more powerful, however, when you remember that they can both hold arrays and hashes as values, along with other types of objects. For example, you might store the information about each customer
in a hash, and then store all of those hashes in an array of customers
! Working together in this way, there's almost no collection of data collection that you can't store with Ruby's arrays and hashes.