GSwR IV: Going Loopy Over Arrays and Hashes

This entry is part 4 of 6 in the series Getting Started with Ruby

Getting Started with Ruby

In the last post we covered numbers and their methods. In this post we’re going to look at some of Ruby’s collections such as Arrays, Hashes and Ranges. We’ll look at what they are, what they’re used for, and what they can do. We’ll also learn how to iterate over a collection and how to create loops. To finish off, we’ll create a program that mimics shuffling a deck of cards and then develop this into an online game of Play Your Cards Right using Sinatra.

Arrays

An array is an ordered list of objects. You can create an array literal by placing the objects, separtated by commas inside square brackets:

primes = [2, 3, 5, 7, 11]

You can place any type of object in an array, such as strings:

fruits = ["apples" "pears" "bananas"]

You don’t even have to use the same type of objects:

mixture = [1, "two", 3.0, "IV" ]

You can even have an array of arrays, known as a multi-dimensional array. This could be used to create a coordinate systyem, like so:

coordinates = [[1,3],[4,2]]

Array Methods

Arrays are a powerful tool in the Ruby toolkit and have some very useful methods. To demonstrate some of them, I will use an array of the Stark children:

starks = %w(Rob Sansa Arya Bran Rickon)
=> ["Rob","Sansa","Arya","Bran","Rickon"]

This uses a slightly different syntax to create the array. If the array you are making is made of one-word strings, then you can use the %w flag at the start to indicate this and then omit the quote marks on the strings inside the array as well as the commas separating them.

To access a specific value in an array, we write the position it is in the array (known as the index) in square brackets

starks[1]
=> "Sansa"

Notice that this isn’t the first term in the array – that’s because the numbering begins at zero. The numbering system also using negative values to start counting from the back, so starks[-1] will return the last value in the array:

starks[-1]
=> "Rickon"

There are also methods for specifically finding the first and last item in an array:

starks[0]
=> "Rob"
starks.first
=> "Rob"
starks[-1]
=> "Rickon"
starks.last
=> "Rickon"

We can also return a subset of the array by specifying a second parameter, which indicates the length of the sub-array:

starks[2,3]
=> ["Arya","Bran","Rickon"]

Alternatively, we can supply a Range object (see more about ranges later) that will produce the subset from one index to another:

starks[2..4]
=> ["Arya","Bran","Rickon"]

If we want to know how many items in an array, then we can use the length method, which thankfully doesn’t start counting from zero:

starks.length
=> 5

To find out if an array contains an object by using the include? method:

starks.include?("Arya")
=> true
starks.include?("Jon")
=> false

To add a new value to the end of an array, use the push method:

starks.push("Jon")
=> ["Rob","Sansa","Arya","Bran","Rickon","Jon"]

There is a common shorthand operator for doing this:

starks << "Jon"
=> ["Rob","Sansa","Arya","Bran","Rickon","Jon"]

We can also remove the last object in an array using the pop method:

nightswatchman = starks.pop
=> "Jon"

We can sort the array into order (alphabetical order by default for String objects):

starks.sort
=> ["Arya","Bran","Rickon","Rob","Sansa"]

This has not actually changed the order of the array though:

starks
=> ["Rob","Sansa","Arya","Bran","Rickon"]

To do that, we need to use the ‘bang’ method of sortsort!:

starks.sort!
=> ["Arya","Bran","Rickon","Rob","Sansa"]
starks
=> ["Arya","Bran","Rickon","Rob","Sansa"]

Now the order of the array has changed for good.

The reverse method (which does what it says on the tin) has a similar bang method:

starks.reverse
=> ["Sansa", "Rob", "Rickon", "Bran", "Arya"]
starks
=> ["Arya", "Bran", "Rickon", "Rob", "Sansa"]
starks.reverse!
=> ["Sansa", "Rob", "Rickon", "Bran", "Arya"]
starks
=> ["Sansa", "Rob", "Rickon", "Bran", "Arya"]

We can join all the elements of an array together in a string using the join method. This takes an argument to indicate what you want to be used as a separator:

starks.join(",")
=> "Sansa,Rob,Rickon,Bran,Arya"

Hashes

Hashes are a list of key and value pairs. We can create a hash literal by placing it inside curly braces

stark = { :name => "Eddard Stark" }

It is a common practice to use symbols for the keys, since they use memory more efficiently. There is a shorthand that can be used from Ruby 1.9 onwards:

stark = {   name: "Eddard Stark",
            lady: "Catelyn Stark",
            sigil: "Direwolf",
            motto: "Winter is Coming",
            residence: "Winterfell",
            children: ["Rob","Sansa","Arya","Bran","Rickon"]
        }

To access a value in a hash, just reference the key:

stark[:motto]
=> "Winter is Coming"

You can even have nested hashes:

houses = { :stark => { sigil: "Direwolf", residence: "Winterfell" },
       :lannister => {sigil: "Lion", residence: "Casterly Rock" }
     }

The values in a nested hash can be accessed by referencing each key in order:

houses[:lanister][:sigil]
=> "Lion"

Ranges

A range can be used to represent a sequence of values. You can create a range by separating the start and end of the range by either 2 dots (inclusive) or 3 dots (exclusive). For example 1 .. 10 is the range of integers from 1 to 10, including 10 and 1 ... 10 is the range of integers from 1 to 9 (10 is not included).

We can also create a range of letters:

alphabet = "a".."z"
=> "a".."z"

Unfortunately, you can’t access the individual values of a Range, like you can with arrays and hashes:

alphabet[2]
NoMethodError: undefined method `[]' for "a".."z":Range
  from (irb):26
  from /home/daz/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'

But there is a handy method called to_a (short for ‘to array’) that will convert our Range into an Array:

alphabet.to_a
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

Ranges are most often used with integers and strings, but can actually be used with any object that is comparable (that is, it has a way of determining order, see here for more information).

Other

There are other Ruby collections, one being Set. This is the same as an array, but can only contain unique values. In fact, any Ruby class can be made to act like a collection by including the Enumerable module, which is actually what gives arrays, hashes and ranges most of the methods we’ve just looked at.

Loops

A loop is a control structure that will repeat a block of code until a certain condition is met. A block of code starts with the command do and finishes with end. Here’s an example of a loop using the until condition:

bottles = 10
until bottles == 0 do
  puts "There were #{bottles} beer bottles hanging on the wall ... "
  bottles -= 1
  puts "And if one beer bottle should accidently fall ..."
  puts "There'd be #{bottles} beer bottles hanging on the wall." 
end

The condition here is that the number of bottles equals zero. The block of code between do and end will be run until that happens. Notice that the value in the bottles variable is reduced each time the block is run, so it will eventually end the loop. It’s important that you make sure the loop will finish, otherwise you will get stuck in an infinite loop.

The code above could be written using the while condition instead:

bottles = 10
while bottles > 0 do
  puts "There were #{bottles} beer bottles hanging on the wall ... "
  bottles -= 1
  puts "And if one beer bottle should accidently fall ..."
  puts "There'd be #{bottles} beer bottles hanging on the wall." 
end

The while and until conditions do pretty much the same thing, but come at it from different angles. Ruby offers both of them so that you can write more expressive code that reads more like English.

Iterators

Iterator methods allow us to loop over an array, hash or range (or indeed any class that includes the Enumerable module). The most common iterator method is the each method. This will go through each object in the collection and then run a block of code. For example, the following piece of code prints out the first five square numbers:

numbers = [1,2,3,4,5]
numbers.each do |number|
  puts number * number
end

Notice that the block takes a parameter that is placed within |pipes| and comes immediately after the do statement, in this case it was |number|. This represents the value in the array at each stage of the loop. This loop will set number to be 1, then print the result of 1 multiplied by 1. In the next iteration, number will be 2 and the process is repeated until every element of the array has been iterated over.

Another way of creating a block is to use curly braces to denote the start and finsih instead of do and end. So the code above could be written more succinctly as:

numbers = 1 .. 5
numbers.each { |number| puts number * number }

I also used a range instead of an array, just to show that the each method works for ranges just the same as arrays.

The general consensus in the Ruby community is to use a { .. } block for single line blocks and use a do .. end block for multi-line blocks.

When iterating over a hash using each, you can enter two parameters to represent the key and value pairs. Heres and example using the stark hash from earlier:

stark = {   
    name: "Eddard Stark",
    lady: "Catelyn Stark",
    sigil: "Direwolf",
    motto: "Winter is Coming",
    residence: "Winterfell",
    children: ["Rob","Sansa","Arya","Bran","Rickon"]
}
stark.each {|key,value| puts "#{key}: #{value}" }

This produces the following output:

name: Eddard Stark
lady: Catelyn Stark
sigil: Direwolf
motto: Winter is Coming
residence: Winterfell
children: ["Rob", "Sansa", "Arya", "Bran", "Rickon"]

Another useful iterator method is the map method (note that the collect method does exactly the same thing). This replaces each value in the array with the return value of the block. So we could create an array of square numbers using the following code:

numbers = [1,2,3,4,5]
=> [1, 2, 3, 4, 5]
squares = numbers.map { |number| number * number }
=> [1, 4, 9, 16, 25]

Note that the value of numbers has not been changed by the map method, it is still [1,2,3,4,5]. A new array called squares holds the result of the map method. If you want to actually change the original array, then use the map! method instead:

numbers = [1,2,3,4,5]
numbers.map! { |number| number * number }
=> [1, 4, 9, 16, 25]

Now the values in the numbers variable will have changed.

Create and Shuffle a Pack of Cards

Now we’re going to use some of the methods we learned above to write a Ruby program that will create and shuffle a pack of cards. Open up your favourite text editor and save the following as ‘cards.rb’:

deck = []
suits = %w[ Hearts Diamonds Clubs Spades ]
values = %w[ Ace 2 3 4 5 6 7 8 9 10 Jack Queen King ]
suits.each do |suit|
  values.each do |value|
  deck << "#{value} of #{suit}"
  end
end
deck.shuffle!
p deck

What we do here is create an empty array called deck to hold the deck of cards. We then create an array of all the suits and an array of the 13 card values (from Ace to King). We then iterate over each suit and then for each suit, we iterate over each value, creating a string to name each card (“Ace of Spades” for example) and then pushing this string into the deck array using the shorthand < < operator.

If you run the following code by typing ruby cards.rb into a terminal, you should see an array similar to the one below, but in a different order (if the cards are being shuffled correctly).

["6 of Spades", "9 of Diamonds", "6 of Hearts", "4 of Spades", "Queen of Spades", "Queen of Hearts", "Ace of Hearts", "8 of Diamonds", "King of Hearts", "2 of Clubs", "3 of Spades", "8 of Clubs", "4 of Diamonds", "4 of Clubs", "10 of Hearts", "5 of Spades", "2 of Spades", "Queen of Clubs", "5 of Diamonds", "9 of Hearts", "10 of Spades", "6 of Diamonds", "7 of Diamonds", "9 of Spades", "7 of Hearts", "2 of Hearts", "8 of Spades", "10 of Diamonds", "7 of Clubs", "8 of Hearts", "King of Clubs", "Jack of Hearts", "5 of Clubs", "3 of Clubs", "King of Spades", "Ace of Clubs", "Ace of Diamonds", "10 of Clubs", "6 of Clubs", "Jack of Spades", "3 of Hearts", "4 of Hearts", "Jack of Clubs", "3 of Diamonds", "Queen of Diamonds", "King of Diamonds", "Ace of Spades", "7 of Spades", "5 of Hearts", "Jack of Diamonds", "9 of Clubs", "2 of Diamonds"]daz@batfink:~/Dropbox/rubysource/drafts/jones$ ruby cards.rb 
["8 of Diamonds", "5 of Clubs", "6 of Clubs", "2 of Hearts", "4 of Hearts", "8 of Hearts", "8 of Spades", "Jack of Hearts", "King of Diamonds", "Queen of Spades", "Queen of Diamonds", "9 of Spades", "6 of Diamonds", "6 of Spades", "9 of Clubs", "3 of Spades", "Ace of Spades", "9 of Hearts", "10 of Diamonds", "7 of Spades", "King of Clubs", "7 of Hearts", "5 of Spades", "7 of Clubs", "Jack of Diamonds", "3 of Hearts", "4 of Diamonds", "9 of Diamonds", "Ace of Hearts", "8 of Clubs", "2 of Diamonds", "2 of Spades", "3 of Clubs", "Queen of Hearts", "Jack of Spades", "10 of Hearts", "6 of Hearts", "Jack of Clubs", "5 of Hearts", "Ace of Diamonds", "2 of Clubs", "King of Spades", "3 of Diamonds", "4 of Clubs", "5 of Diamonds", "4 of Spades", "10 of Clubs", "Ace of Clubs", "King of Hearts", "10 of Spades", "Queen of Clubs", "7 of Diamonds"]

This array can now be used to create all sorts of wonderful card games. Speaking of which …

Play Your Cards Right on the Web

I used to love watching the Play Your Cards Right gameshow when I was younger, so I thought I’d do a web game based on this using the code for the deck of cards that we created above.

Create a file called ‘playyourcards_right.rb’ and add the following code:

require 'sinatra'
enable :sessions

configure do
  set :deck, []
  suits = %w[ Hearts Diamonds Clubs Spades ]
  values = %w[ Ace 2 3 4 5 6 7 8 9 10 Jack Queen King ]
  suits.each do |suit|
    values.each do |value|
      settings.deck << "#{value} of #{suit}"
    end
  end
end

This starts off by requiring the sinatra gem and enabling sessions (we’ll need those to keep track of the cards as we play).

Next we need a route handler to get us started and do some setting up. We’ll put this in the root URL (‘/’):

get '/' do
  session[:deck] = settings.deck.shuffle
  session[:guesses] = -1
  redirect to('/play')
end

This uses the settings.deck array that we set up in our configure block, shuffles it, and puts it in the session hash using a key of ‘:deck’. We also set up another session variable to keep track of how many guesses the player has made. After this is done, we redirect to the /play route, which is where the main game happens.

Let’s set up a route handler for that now:

get '/:guess' do
  card = session[:deck].pop

  value = case card[0]
    when "J" then 11
    when "Q" then 12
    when "K" then 13
    else card.to_i
  end

  if (value < session[:value] and params[:guess] == 'higher') or (value > session[:value] and params[:guess] == 'lower')
    "Game Over! The card was the #{ card }. You managed to make #{session[:guesses]} correct guess#{'es' unless session[:guesses] == 1}. <a href='/'>Play Again</a>"
  else
    session[:value] = value
    session[:guesses]  += 1
    "The card is the #{ card }. Do you think the next card wil be <a href='/higher'>Higher</a> or <a href='/lower'>Lower</a>?"
  end
end

First of all, you might be thinking that this route handler doesn’t actually correspond to /play. But it does! The ‘:guess’ part of the route is a named parameter. The string that comes after / in the route will be stored in the params hash with a key of :guess. It is used in the game to check whether the player has guessed ‘higher’ or ‘lower’. Any word can be entered into this route. In this case, the word ‘play’ was used as a dummy, since the player hasn’t actually made a guess yet.

The first thing we do in this handler is use the pop method to remove the last card from the array and store it in a variable called card. This is the equivalent of dealing a card from the top of the deck.

The next piece of code works out the numerical value of the card. This uses a case statement to check the first letter in the card string (this is obtained using card[0], like arrays, strings start counting at zero). It then checks to see if it’s a Jack, Queen or King and assigns a value of 11, 12 and 13, respectively. For all the other cards it is enough to use the to_i method as this takes the first integer value found in a string (BONUS – can you figure out why this still works for Aces?).

Then comes a big if statement that checks to see if the player has guessed incorrectly. First, a check to see if the value held in the session hash with a key of :value is less or more than the value of the card. Then, comparing this to the player’s guess which is stored in the params hash as params[:guess]. The value held in session[:card] is the value of the previous card. If the player guessed incorrectly then a ‘Game Over’ Message is displayed, telling the player what the card was and how many correct guesses they have made.

The else condition covers if the player guesses correctly – or more accurately if the guess is not wrong. This allows the code to run if params[:guess] is set to ‘play’ as it is at the start of the game. The first thing that happens is the current card’s value is stored in session[:value] so it can be compared with the next card and then the number of correct guesses (stored in session[:guesses]) is increased by 1. Then a message is shown, telling the player what the value of the card is and offering links to choose if the next one will be higher or lower.

Start a server running by typing ruby play_your_cards_right.rb into a terminal then navigate to http://localhost:4567 in your browser and have some fun playing!

That’s All Folks

That brings us to the end of this installment of the Getting Started with Ruby series. Arrays are a very powerful part of any programming toolkit and there aren’t many programs where that won’t include some sort of iterator. Hashes also appear frequently in many Ruby programs (they are often used to provide options to methods in Rails, for example). Hopefully, it is now a bit clearer how the session and params hashes work in Sinatra.

If you want to find out even more about arrays, make sure you have a look at this excellent post by Robert Qualls which is full of even more useful stuff about arrays.

We’ve now covered strings, numbers and collections and their methods. Things start to get interetsting in the next part of the series when we’ll be looking at how to write you own methods.

Getting Started with Ruby

<< Getting Started with Ruby, III: Numbers and LogicGSwR V: Methods to the Madness >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Will

    if (value < session[:value] and params[:guess] == 'higher') or (value > session[:value] and params[:guess] == ‘lower’) yields “comparison of Fixnum with nil failed”. I’ve copied and pasted to ensure I didn’t mistype your example. Any suggestions to rectify this?

  • Anonymous

    Hi Will2,

    The if statement should be the other way round.

    It should be:
    if (params[:guess] == ‘higher’ and value < session[:value]) or (params[:guess] == 'lower' and value > session[:value])

    Sorry about that.

    DAZ

  • Will

    Thanks for these tutorials!