Hashes

A Hash is ruby’s basic key-value store: a place where you can store values, and look them up by a key. The Hash and the Array are ruby’s two common ways of storing a collection of objects. The key differences between ‘Hash’ and ‘Array’ are:

  • In an Array objects are stored in order. You can think of an Array as a list.
  • In an Array we reference objects by a numerical index.
  • In a Hash there is no order on the objects. You can think of a Hash as a general collection.
  • In a Hash objects are referenced by a key. The key can be a number or a word (or several other things).

Here is an example of a simple hash:

character = {'name'=>'Bart', 'surname'=>'Simpson', 'age'=> 10, 'catchphrase' => 'Eat my shorts'}

character['name'] #=> 'Bart'
character['catchphrase'] #=> 'Eat my shorts'

In particular:

  • You write a Hash using the curly brackets { }.
  • Each element of the hash has a key e.g. 'name' and a value e.g. 'Bart'.
  • When you write the hash you put the keys and values using the => symbol: key => value.
  • To pull values out of a hash you put the key inside [ ].
Task:
  1. With your partner work through the expressions below. See if you can guess what each expression will do, then test by pasting into irb.
  2. If you finish early, have a look at the 'Arrays and back' extension tab.
h = {'one' => 1, 'two' => 2, 'three' => 3}

# Accessing elements
# ------------------

h['one']
h['two']
h['four']

# Setting elements
# ----------------

h['five'] = 5
h['one'] = 1.0
h.delete('one')  # What does this return? Does it change the hash?

# Some methods
# ------------

h.length
h.empty?
h.keys
h.values
h.has_key?('one')
h.has_value?(7)

# Iterating
# ---------

h.each do |key, value|
  puts "#{key}: #{value}"
end


# Combining
# ---------

h1 = {'a'=>1, 'b'=>2}
h2 = {'b'=>3, 'c'=>4}

h1.merge(h2)
h1    # what has this done to h1?
h1.merge!(h2)
h1    # what is h1 now?


# Using a hash for counting (extension)
# -------------------------

# start with an empty hash
h = {}

['a', 'b', 'a', 'a'].each do |letter|
    if h.has_key?(letter)
        # if the letter is already in the hash, increase
        # its count by 1
        h[letter] += 1
    else # letter isn't in the hash
        # so put it in and set the count to 1
        h[letter] = 1
    end
end

h   # what is h now?

# A slicker way to count (extension)
# ----------------------------------

# A slightly slicker way of doing this is
# using a hash with a default value:

h = Hash.new(0) # 0 is the default value that is 
                # returned when the key is missing

['a', 'b', 'a', 'a'].each do |letter|
    h[letter] += 1
end

Exercise summary

In the last exercise you will have learnt a number of things about hashes. Here is a summary of the important points:

Accessing and setting elements

h = {'one' => 1, 'two' => 2, 'three' => 3}

h['one'] #=> 1
h['four'] #=> nil

h['five'] = 5
h['one'] = 1.0

h #=> {'one' => 1.0, 'two' => 2, 'three' => 3, 'five' => 5}
  • You access an element via its key: h['one']
  • If the key isn’t in the hash this will give nil
  • You add new elements by setting their key
  • If the key already exists its value will be updated

Methods

Hashes have a number of methods, which behave as you would expect e.g.

  • length
  • empty?
  • keys, values
  • has_key?, has_value?

Iterating

Just like an Array, you can iterate over a Hash using the each method. Unlike array iteration, in hash iteration the block accepts two parameters: the key and the value:

h = {'one' => 1, 'two' => 2, 'three' => 3}

h.each do |key, value|
	puts "#{key} = #{value}"
end

Combining hashes

Combining hashes is done by using the merge method. The values from the second hash are added to the first, replacing them if they already exist. You will often see this used in rails for specifying default options to a function:

# note how the first version of b gets overwritten
{'a' => 1, 'b' => 2}.merge({'b'=>3, 'c'=>4}) #=> {'a'=>1, 'b'=>3, 'c'=>4}

# real life example
def print_names(opts)
	default_opts = {'fancy_format' => true, 'max_length' => 20}

	# replace the defaults with options supplied
	my_opts = default_opts.merge(opts) 

	if my_opts['fancy_format']
		# ....
	end

	#...
end

The options example above is a common use of a hash in real life code.

Using a hash for counting

One special use of a hash is for counting things. The following code is an example of how to do this.

h = {}

['a', 'b', 'a', 'a'].each do |letter|
    if h.has_key?(letter)
        # if the letter is already in the hash, increase
        # its count by 1
        h[letter] += 1
    else # letter isn't in the hash
        # so put it in and set the count to 1
        h[letter] = 1
    end
end

h #=> {'a' => 3, 'b' => 1}

You can make this example a bit shorter by using a hash with a default value: normally a hash will return nil if the key isn’t there, but we can set it up to return something else. In particular we’ll set it up to return 0:

# set up a hash with default value 0
h = Hash.new(0)

['a', 'b', 'a', 'a'].each do |letter|
    h[letter] = h[letter] + 1
end

h #=> {'a' => 3, 'b' => 1}

If the letter isn’t in the hash yet, the default value of 0 will be returned. Otherwise the current count will be returned. Either way, we just need to increase the value by 1.

Symbols

Symbols are a bit like strings that can’t be changed. They’re primarily used to save space and time:

  • Every time you write a string in ruby it has to store a new copy of it in case it is changed.
  • Every time you compare two strings it has to check every letter to see if they’re the same.
  • As symbols can’t be changed, it allows ruby to only store them once.
  • This makes it quick to compare, as you can just check to see if it’s in the same storage location.

So far we’ve used strings for our hash keys. As we write and compare hash keys a lot, it makes sense to use symbols instead.

"hello"
:hello
:hello.to_s
"hello".to_sym


"hello" << " world"
:hello << :" world"

"hello".object_id

:hello.object_id

Sending email

Today we’ll be looking at sending email from a ruby program. To do this we’ll be using a library called Pony. Pony is a light-weight, simple library (in comparison more fully fledged options like ActionMailer). After a small amount of configuration, it allows you to send emails with a single command.

We’ll be using a google account to send the email - you will need to input your username and password, and it will look like the email came from your gmail account. Using gmail is a good solution for a small test app like this. When you get bigger you will start hitting gmails sending limits, and you will want to investigate other solutions e.g. sendgrid or mandrill. For further options see the heroku addons page.

Before sending an email we need to set up some configuration. We do this by setting the Pony.options hash:

Pony.options = {
  :via => 'smtp',
  :via_options => {
    :address              => 'smtp.gmail.com',
    :port                 => '587',
    :enable_starttls_auto => true,
    :user_name            => 'yourusername',
    :password             => 'yourpassword',
    :authentication       => :plain, # :plain, :login, :cram_md5, no auth by default
    :domain               => "localhost.localdomain" # the HELO domain provided by the client to the server
  }
}

To send the email is then very simple:

Pony.mail(:to => 'example@example.com', :subject => "Wow - an email", :body=>"Hi. This is your program speaking. Bye.")

You can find out more about the different options you can use on the pony github page.

Task:
  1. Fork and clone the code for this session: https://github.com/code61/sinatra_c3s4

  2. Install the gems (including pony):

     bundle install
  3. Copy and paste the contents of development_pony_options.rb.sample into a new file. Save this file as development_pony_options.rb. Fill in your gmail (or university) account details.

  4. In irb type require &#39;pony&#39;, then copy and paste in your updated options.

  5. Send an email to yourself e.g.:

Pony.mail(:to => 'example@example.com', :subject => "Wow - an email", :body=>"Hi. This is your program speaking. Bye.")
  1. Try running the sinatra app: ruby app.rb.
  2. The form submission doesn't do anything at the moment. Can you work out what's wrong, and fix it?

Email templates

So far the body of our email has only been a single line. What if we want a proper multi-line email? You can use erb templates for this!

  Pony.mail( :to => @email,
             :subject => "Congratulations, you added a fruit!",
             :body => erb(:email, :layout => false)   )

The important bit is erb(:email, :layout => false), which tells sinatra to find views/email.erb and run it through erb (to replace any <%= %> tags). The :layout => false tells sintra to skip the normal layout file: we want to send the user the raw text, not an html page! The result is then used as the email’s body.

The template will look something like this:

Hello there!

<%= @name %> is a great fruit!

Best,

The FruitApp team
Task:
  1. Add the required line of code in the post &#39;/&#39; block, to send a welcome email to the new user.
  2. Modify views/email.erb so that it (at least) contains the name of the person who just signed up.
  3. (Optional extension) deploy your app to heroku. You will need to add the (free version) of the sendgrid addon to allow you to send emails.
  4. (Alternative extension) Clone down the project https://github.com/code61/mailmerge and have a play!

Homework

Task:
  1. Finish off any exercises from class.
  2. Do the Codecademy Ruby track Sections 7 and 8.

(Ext) Arrays and back

This page is more advanced. This stuff is sometimes useful, but don’t worry if you want to skip over it the first time through.

Converting to arrays and back

You can convert a hash to an array using the to_a method and use the Hash[ ] syntax to convert it back:

h = {'a' => 1, 'b' => 2}

h.to_a #=> [['a', 1], ['b', 2]]

h2 = Hash[['a', 1], ['b', 2]]

The Hash[ ] is also useful for creating a hash from arrays of keys and values (which is surprisingly useful).

keys = [1, 2, 3]
values = ['one', 'two', 'three']

# use Array#zip to get the arrays in the right format
zipped = keys.zip(values)  #=> [[1, 'one'], [2, 'two'], [3, 'three']]

h = Hash[zipped]  #=> {1 => 'one', 2 => 'two', 3 => 'three'}
Task:

(Optional)

  1. Work through the following examples, copying and pasting into irb.
  2. See if you can do the challenge at the end.
# A few new ways to create a hash
# -------------------------------

h1 = Hash['a', 1, 'b', 2, 'c', 3]

h2 = Hash[[['a',1],['b',2],['c',3]]


# Converting to arrays (and back?)
# -------------------------------

h = {'a'=>1, 'b'=>2, 'c'=>3}

a = h.to_a

# How could you convert a back into h?

h = {'one'=>1, 'two'=>2, 'three'=>3}

keys = h.keys
values = h.values

# How would you combine keys and values back into h?