#---Hash---
Hashes, like arrays are a powerful and commonly used data structure in Ruby. In some languages they’re known as dictionaries or maps.
With array, you’re only given indices to the items, but with a hash you can name the items:
arr = ['Bob', 23]
p "#{arr[0]}'s age is #{arr[1]}"
## => "Bob's age is 23"
hash = {name: 'Bob', age: 23}
p "#{hash[:name]}'s age is #{hash[:age]}"
Use arrays to store a list of items. But use hashes to store key-value pairs that you want to look up later by the key.
The simplest way to create a hash is with the literal syntax:
hash = {name: 'Bob', age: 23}
p hash # => {name: "Bob", age: 23}
Here, Bob
and 23
are the values, and
:name
and :age
are the corresponding keys for those
values.
Note that the keys are symbols, although written weirdly with the colon after the word. That’s because prior to Ruby 1.9, you had to declare the same hash like this:
## This is a bit verbose, and so has gone out of style.
## This is the 'hash-rocket' style:
hash = { :name => 'Bob', :age => 23 }
You can also create a hash with Hash.new
or Hash[]
methods:
hash = Hash.new
p hash # => {}
hash = Hash[:name, 'Bob', :age, 23]
p hash # => {name: "Bob", age: 23}
Hash keys can be any object, including numbers, although it’s common to use strings or symbols. The values can also be any object, including other hashes.
h = {1 => 'one',
'two' => 2,
three: 3,
'four' => {4 => 'four'}}
p h
## => {1 => "one", "two" => 2, three: 3, "four" =>
## {4 => "four"}}
Note: You still have to use the =>
operator for
keys that are not symbols.
Since Ruby 3.1, you can create a hash even more tersely if the value variables are named the same as the keys:
a, b = 1, 2
h = {a: a, b: b} # pre-3.1
h = {a:, b:}
p h # => {a: 1, b: 2}
It’s as simple as this:
p hash[:name] # => "Bob"
p hash[:age] # => 23
But there’s a catch:
If you try to access a key that doesn’t exist, you’ll
get nil
:
p hash[:height] # => nil
You might expect to get an error, but instead you
silently got a nil
.
To avoid this, use fetch
. It’ll blow up if the key
doesn’t exist:
p hash[:height] # => nil
p hash.fetch(:height) rescue nil
## 💥 without `rescue nil`, blows up with KeyError
If you don’t want it to blow up, but just return a default value if the key doesn’t exist, you can do so like this:
p hash.fetch(:height, 123) # => 123
If you want to set a default value for all keys that don’t exist, you can do so like this:
h = Hash.new('Default Joe')
p h[:name] # => "Default Joe"
Use the block version to set default value based on the key:
h = Hash.new { |hash, key| hash[key] = key.to_s.upcase }
p h[:name] # => "NAME"
p h[:age] # => "AGE"
Just like in arrays, you can use dig
to
access items from nested hashes. This is a great way to
avoid errors:
dogs = {
pongo: {age: 7, owner: "Roger" },
perdita: {age: 5, owner: "Anita" },
}
p dogs[:pongo][:age] # => 7
p dogs[:perdita][:age] # => 5
p dogs[:colonel][:age] rescue nil
## 💥 without `rescue nil`, blows up with NoMethodError
Instead, use dig
:
p dogs.dig(:colonel, :age) # => nil
It’s most useful in conditionals:
Prior to dig
, you had to do this:
if dogs[:pongo] && dogs[:pongo][:age]
end
With dig
, you can do this:
if dogs.dig(:pongo, :age)
end
Note: dig
also works with hashes containing arrays,
that in turn contain hashes, and so on:
h = {a: [{b: 11}, {c: [22, 33]}]}
p h.dig(:a, 1, :c, 0) # => 22
Also note: An even better way is with pattern matching:
dogs => {perdita: {age: age_of_perdita}}
p age_of_perdita # => 5
Use []=
to add a new item or modify an existing one:
h = {}
h[:name] = 'Bob'
p h[:name] # => "Bob"
h[:name] = 'Bobby'
p h[:name] # => "Bobby"
You can also use merge
to add or modify multiple
items at once. Here we’re modifying Bob’s name as well
as adding a new key height
:
bob = {name: 'Bob', age: 23}
p bob.merge(name: 'Bobby', height: 180)
## => {name: "Bobby", age: 23, height: 180}
Note: merge
takes a hash as an argument, but you can
see that the {}
braces are missing. That’s because
the braces are optional when the hash is the
last argument in the method call.
Also note: merge
doesn’t modify the original hash.
It just returns a new hash:
p bob # => {name: "Bob", age: 23}
Use merge!
to modify the original hash:
bob.merge!(name: 'Bobby', height: 180)
p bob # => {name: "Bobby", age: 23, height: 180}
Use delete
to remove an item from a hash:
h = {bob: 23, alice: 25, charlie: 30}
p h.delete(:bob) # => 23
p h # => {alice: 25, charlie: 30}
Don’t assign nil
to a key to delete it. This will just
set the value to nil
.
Use delete_if
to delete items conditionally:
## Delete all older folks:
h.delete_if { |k, v| v > 27 }
p h # => {alice: 25}
size
gives you the count of hash items:h = {bob: 23, alice: 25, charlie: 30}
p h.size # => 3
empty?
returns true if hash has no items:p h.empty?, {}.empty? # => [false, true]
keys
returns an array of the keys. And values
returns an array of the values:p h.keys # => [:bob, :alice, :charlie]
p h.values # => [23, 25, 30]
has_key?
returns true if the hash has the key.
has_value?
returns true if the hash has the value:p h.has_key?(:bob), h.has_key?(:david) # => [true, false]
p h.has_value?(23), h.has_value?(100) # => [true, false]
compact
removes all items with nil
values:h = {bob: 23, alice: nil, charlie: 30}
p h.compact # => {bob: 23, charlie: 30}
transform_keys
is often used to convert string keys
to symbols:p ({'a' => 'aa', 'b' => 'bb'}).transform_keys(&:to_sym)
## => {:a => "aa", :b => "bb"}
each
is the simplest way to iterate over a hash. The
block is called with each key-value pair:h.each { |k, v| puts "#{k} is #{v} years old." }
## => bob is 23 years old.
## => alice is 25 years old.
## => charlie is 30 years old.
map
returns a new array with the results of calling
the block on each item:p h.map { |k, v| "#{k} is #{v}" }
## => ["bob is 23", "alice is 25", "charlie is 30"]
Like Array, the Hash class too gets a lot of its
superpowers from the Enumerable
module it includes.
Some useful enumerable methods:
map
, discussed above.select
returns a new hash with only the items
that make the block return a truthy value.
reject
does the opposite:h = {bob: 23, alice: 25, charlie: 30}
p h.select { |k, v| v.even? } # => {charlie: 30}
p h.reject { |_, v| v.even? } # => {bob: 23, alice: 25}
group_by
groups the items in the hash by the
block’s return value:h = {bob: 23, alice: 25, charlie: 30}
p h.group_by { |_, v| v.even? }
## => {true => [[:charlie, 30]],
## false => [[:bob, 23], [:alice, 25]]}
include?
returns true if the hash contains the key:p h.include?(:bob) # => true
to_a
converts a hash to an array of arrays:p h.to_a # => [[:bob, 23], [:alice, 25], [:charlie, 30]]
to_json
converts a hash to a JSON string. But you
need to require json
first in some older versions of
Ruby:require 'json'
puts h.to_json # => {"bob":23,"alice":25,"charlie":30}