Ruby Language

Hashes

Introduction#

A Hash is a dictionary-like collection of unique keys and their values. Also called associative arrays, they are similar to Arrays, but where an Array uses integers as its index, a Hash allows you to use any object type. You retrieve or create a new entry in a Hash by referring to its key.

Syntax#

  • { first_name: “Noel”, second_name: “Edmonds” }

  • { :first_name => “Noel”, :second_name => “Edmonds” }

  • { “First Name” => “Noel”, “Second Name” => “Edmonds” }

  • { first_key => first_value, second_key => second_value }

Remarks#

Hashes in Ruby map keys to values using a hash table.

Any hashable object can be used as keys. However, it’s very common to use a Symbol as it is generally more efficient in several Ruby versions, due to the reduced object allocation.

{ key1: "foo", key2: "baz"  }

Creating a hash

A hash in Ruby is an object that implements a hash table, mapping keys to values. Ruby supports a specific literal syntax for defining hashes using {}:

my_hash = {}  # an empty hash
grades = { 'Mark' => 15, 'Jimmy' => 10, 'Jack' => 10 }

A hash can also be created using the standard new method:

my_hash = Hash.new  # any empty hash
my_hash = {}        # any empty hash

Hashes can have values of any type, including complex types like arrays, objects and other hashes:

mapping = { 'Mark' => 15, 'Jimmy' => [3,4], 'Nika' => {'a' => 3, 'b' => 5} }
mapping['Mark']   # => 15
mapping['Jimmy']  # => [3, 4]
mapping['Nika']   # => {"a"=>3, "b"=>5}

Also keys can be of any type, including complex ones:

mapping = { 'Mark' => 15, 5 => 10, [1, 2] => 9 }
mapping['Mark']  # => 15
mapping[[1, 2]]  # => 9

Symbols are commonly used as hash keys, and Ruby 1.9 introduced a new syntax specifically to shorten this process. The following hashes are equivalent:

# Valid on all Ruby versions
grades = { :Mark => 15, :Jimmy => 10, :Jack => 10 }
# Valid in Ruby version 1.9+
grades = { Mark: 15, Jimmy: 10, Jack: 10 }

The following hash (valid in all Ruby versions) is different, because all keys are strings:

grades = { "Mark" => 15, "Jimmy" => 10, "Jack" => 10 }

While both syntax versions can be mixed, the following is discouraged.

mapping = { :length => 45, width: 10 }

With Ruby 2.2+, there is an alternative syntax for creating a hash with symbol keys (most useful if the symbol contains spaces):

grades = { "Jimmy Choo": 10, :"Jack Sparrow": 10 }
# => { :"Jimmy Choo" => 10, :"Jack Sparrow" => 10}

Accessing Values

Individual values of a hash are read and written using the [] and []= methods:

my_hash = { length: 4, width: 5 }

my_hash[:length] #=> => 4

my_hash[:height] = 9

my_hash #=> {:length => 4, :width => 5, :height => 9 }

By default, accessing a key which has not been added to the hash returns nil, meaning it is always safe to attempt to look up a key’s value:

my_hash = {}

my_hash[:age] # => nil

Hashes can also contain keys in strings. If you try to access them normally it will just return a nil, instead you access them by their string keys:

my_hash = { "name" => "user" }

my_hash[:name]    # => nil
my_hash["name"]   # => user

For situations where keys are expected or required to exist, hashes have a fetch method which will raise an exception when accessing a key that does not exist:

my_hash = {}

my_hash.fetch(:age) #=> KeyError: key not found: :age

fetch accepts a default value as its second argument, which is returned if the key has not been previously set:

my_hash =  {}
my_hash.fetch(:age, 45) #=> => 45

fetch can also accept a block which is returned if the key has not been previously set:

my_hash = {}
my_hash.fetch(:age) { 21 } #=> 21

my_hash.fetch(:age) do |k|
  puts "Could not find #{k}"
end

#=> Could not find age

Hashes also support a store method as an alias for []=:

my_hash = {}

my_hash.store(:age, 45)

my_hash #=> { :age => 45 }

You can also get all values of a hash using the values method:

my_hash = { length: 4, width: 5 }

my_hash.values #=> [4, 5]

Note: This is only for Ruby 2.3+ #dig is handy for nested Hashs. Extracts the nested value specified by the sequence of idx objects by calling dig at each step, returning nil if any intermediate step is nil.

h = { foo: {bar: {baz: 1}}}

h.dig(:foo, :bar, :baz)   # => 1
h.dig(:foo, :zot, :xyz)   # => nil

g = { foo: [10, 11, 12] }
g.dig(:foo, 1)            # => 11

Setting Default Values

By default, attempting to lookup the value for a key which does not exist will return nil. You can optionally specify some other value to return (or an action to take) when the hash is accessed with a non-existent key. Although this is referred to as “the default value”, it need not be a single value; it could, for example, be a computed value such as the length of the key.

The default value of a hash can be passed to its constructor:

h = Hash.new(0)

h[:hi] = 1 
puts h[:hi]  # => 1 
puts h[:bye] # => 0 returns default value instead of nil

A default can also be specified on an already constructed Hash:

my_hash = { human: 2, animal: 1 }
my_hash.default = 0
my_hash[:plant] # => 0

It is important to note that the default value is not copied each time a new key is accessed, which can lead to surprising results when the default value is a reference type:

# Use an empty array as the default value
authors = Hash.new([])

# Append a book title 
authors[:homer] << 'The Odyssey'

# All new keys map to a reference to the same array:
authors[:plato] # => ['The Odyssey']

To circumvent this problem, the Hash constructor accepts a block which is executed each time a new key is accessed, and the returned value is used as the default:

authors = Hash.new { [] }

# Note that we're using += instead of <<, see below
authors[:homer] += ['The Odyssey']
authors[:plato] # => []

authors # => {:homer=>["The Odyssey"]}

Note that above we had to use += instead of << because the default value is not automatically assigned to the hash; using << would have added to the array, but authors[:homer] would have remained undefined:

authors[:homer] << 'The Odyssey' # ['The Odyssey']
authors[:homer] # => []
authors # => {}

In order to be able to assign default values on access, as well as to compute more sophisticated defaults, the default block is passed both the hash and the key:

authors = Hash.new { |hash, key| hash[key] = [] }

authors[:homer] << 'The Odyssey'
authors[:plato] # => []

authors # => {:homer=>["The Odyssey"], :plato=>[]}

You can also use a default block to take an action and/or return a value dependent on the key (or some other data):

chars = Hash.new { |hash,key| key.length }

chars[:test] # => 4

You can even create more complex hashes:

page_views = Hash.new { |hash, key| hash[key] = { count: 0, url: key } }
page_views["https://example.com"][:count] += 1
page_views # => {"https://example.com"=>{:count=>1, :url=>"https://example.com"}}

In order to set the default to a Proc on an already-existing hash, use default_proc=:

authors = {}
authors.default_proc = proc { [] }

authors[:homer] += ['The Odyssey']
authors[:plato] # => []

authors # {:homer=>["The Odyssey"]}

Automatically creating a Deep Hash

Hash has a default value for keys that are requested but don’t exist (nil):

a = {}
p a[ :b ] # => nil 

When creating a new Hash, one can specify the default:

b = Hash.new 'puppy'
p b[ :b ]            # => 'puppy'

Hash.new also takes a block, which allows you to automatically create nested hashes, such as Perl’s autovivification behavior or mkdir -p:

# h is the hash you're creating, and k the key.
#
hash = Hash.new { |h, k| h[k] = Hash.new &h.default_proc }
hash[ :a ][ :b ][ :c ] = 3

p hash # => { a: { b: { c: 3 } } }

Modifying keys and values

You can create a new hash with the keys or values modified, indeed you can also add or delete keys, using inject (AKA, reduce). For example to produce a hash with stringified keys and upper case values:

fruit = { name: 'apple', color: 'green', shape: 'round' }
# => {:name=>"apple", :color=>"green", :shape=>"round"}

new_fruit = fruit.inject({}) { |memo, (k,v)| memo[k.to_s] = v.upcase; memo }

# => new_fruit is {"name"=>"APPLE", "color"=>"GREEN", "shape"=>"ROUND"}

Hash is an enumerable, in essence a collection of key/value pairs. Therefore is has methods such as each, map and inject.

For every key/value pair in the hash the given block is evaluated, the value of memo on the first run is the seed value passed to inject, in our case an empty hash, {}. The value of memo for subsequent evaluations is the returned value of the previous blocks evaluation, this is why we modify memo by setting a key with a value and then return memo at the end. The return value of the final blocks evaluation is the return value of inject, in our case memo.

To avoid the having to provide the final value, you could use each_with_object instead:

new_fruit = fruit.each_with_object({}) { |(k,v), memo| memo[k.to_s] = v.upcase }

Or even map:

<!-- if version [gte 1.8] -->
new_fruit = Hash[fruit.map{ |k,v| [k.to_s, v.upcase] }]
<!-- end version if -->

(See this answer for more details, including how to manipulate hashes in place.)

Iterating Over a Hash

A Hash includes the Enumerable module, which provides several iteration methods, such as: Enumerable#each, Enumerable#each_pair, Enumerable#each_key, and Enumerable#each_value.

.each and .each_pair iterate over each key-value pair:

h = { "first_name" => "John", "last_name" => "Doe" }
h.each do |key, value|
    puts "#{key} = #{value}"
end

# => first_name = John
#    last_name = Doe

.each_key iterates over the keys only:

h = { "first_name" => "John", "last_name" => "Doe" }
h.each_key do |key|
  puts key
end

# => first_name
#    last_name

.each_value iterates over the values only:

h = { "first_name" => "John", "last_name" => "Doe" }
h.each_value do |value|
    puts value
end

# => John
#    Doe

.each_with_index iterates over the elements and provides the index of the iteration:

h = { "first_name" => "John", "last_name" => "Doe" }
h.each_with_index do |(key, value), index|
    puts "index: #{index} | key: #{key} | value: #{value}"
end

# => index: 0 | key: first_name | value: John
#    index: 1 | key: last_name | value: Doe

Conversion to and from Arrays

Hashes can be freely converted to and from arrays. Converting a hash of key/value pairs into an array will produce an array containing nested arrays for pair:

{ :a => 1, :b => 2 }.to_a # => [[:a, 1], [:b, 2]]

In the opposite direction a Hash can be created from an array of the same format:

[[:x, 3], [:y, 4]].to_h # => { :x => 3, :y => 4 }

Similarly, Hashes can be initialized using Hash[] and a list of alternating keys and values:

Hash[:a, 1, :b, 2] # => { :a => 1, :b => 2 }

Or from an array of arrays with two values each:

Hash[ [[:x, 3], [:y, 4]] ] # => { :x => 3, :y => 4 }

Hashes can be converted back to an Array of alternating keys and values using flatten():

{ :a => 1, :b => 2 }.flatten # => [:a, 1, :b, 2]

The easy conversion to and from an array allows Hash to work well with many Enumerable methods such as collect and zip:

Hash[('a'..'z').collect{ |c| [c, c.upcase] }] # => { 'a' => 'A', 'b' => 'B', ... }

people = ['Alice', 'Bob', 'Eve']
height = [5.7, 6.0, 4.9]
Hash[people.zip(height)] # => { 'Alice' => 5.7, 'Bob' => '6.0', 'Eve' => 4.9 }

Getting all keys or values of hash

{foo: 'bar', biz: 'baz'}.keys   # => [:foo, :biz]
{foo: 'bar', biz: 'baz'}.values # => ["bar", "baz"]
{foo: 'bar', biz: 'baz'}.to_a   # => [[:foo, "bar"], [:biz, "baz"]]
{foo: 'bar', biz: 'baz'}.each   #<Enumerator: {:foo=>"bar", :biz=>"baz"}:each>

Overriding hash function

Ruby hashes use the methods hash and eql? to perform the hash operation and assign objects stored in the hash to internal hash bins. The default implementation of hash in Ruby is the murmur hash function over all member fields of the hashed object. To override this behavior it is possible to override hash and eql? methods.

As with other hash implementations, two objects a and b, will be hashed to the same bucket if a.hash == b.hash and will be deemed identical if a.eql?(b). Thus, when reimplementing hash and eql? one should take care to ensure that if a and b are equal under eql? they must return the same hash value. Otherwise this might result in duplicate entries in a hash. Conversely, a poor choice in hash implementation might lead many objects to share the same hash bucket, effectively destroying the O(1) look-up time and causing O(n) for calling eql? on all objects.

In the example below only the instance of class A is stored as a key, as it was added first:

class A
  def initialize(hash_value)
    @hash_value = hash_value
  end
  def hash
    @hash_value # Return the value given externally
  end
  def eql?(b)
    self.hash == b.hash
  end
end

class B < A
end

a = A.new(1)
b = B.new(1)

h = {}
h[a] = 1
h[b] = 2

raise "error" unless h.size == 1
raise "error" unless h.include? b
raise "error" unless h.include? a

Filtering hashes

select returns a new hash with key-value pairs for which the block evaluates to true.

{ :a => 1, :b => 2, :c => 3 }.select { |k, v| k != :a && v.even? } # => { :b => 2 }

When you will not need the key or value in a filter block, the convention is to use an _ in that place:

{ :a => 1, :b => 2, :c => 3 }.select { |_, v| v.even? } # => { :b => 2 }
{ :a => 1, :b => 2, :c => 3 }.select { |k, _| k == :c } # => { :c => 3 }

reject returns a new hash with key-value pairs for which the block evaluates to false:

{ :a => 1, :b => 2, :c => 3 }.reject { |_, v| v.even? } # => { :a => 1, :c => 3 }
{ :a => 1, :b => 2, :c => 3 }.reject { |k, _| k == :b } # => { :a => 1, :c => 3 }

Set Operations on Hashes

  • Intersection of Hashes

    To get the intersection of two hashes, return the shared keys the values of which are equal:

    hash1 = { :a => 1, :b => 2 }
    hash2 = { :b => 2, :c => 3 }
    hash1.select { |k, v| (hash2.include?(k) && hash2[k] == v) } # => { :b => 2 }
  • Union (merge) of hashes:

    keys in a hash are unique, if a key occurs in both hashes which are to be merged, the one from the hash that merge is called on is overwritten:

    hash1 = { :a => 1, :b => 2 }
    hash2 = { :b => 4, :c => 3 }
    
    hash1.merge(hash2) # => { :a => 1, :b => 4, :c => 3 }
    hash2.merge(hash1) # => { :b => 2, :c => 3, :a => 1 }

This modified text is an extract of the original Stack Overflow Documentation created by the contributors and released under CC BY-SA 3.0 This website is not affiliated with Stack Overflow