Hashes
Table of Contents
- Initialization
- Accessing keys and values
- Looping
- Modifying elements
- Filtering
- Transforming keys and values
- Mutable keys
- Miscellaneous
Initialization
- as comma separated key-value pairs inside
{}
- both key and value can be of any data type and be result of an expression
- but having mutable data type as key requires careful usage
- as a special case, string keys are frozen
>> marks = {'Foo' => 86, 'Bar' => 92, 'Baz' => 75}
=> {"Foo"=>86, "Bar"=>92, "Baz"=>75}
>> s = 'a'
=> "a"
>> h = {s+'g' => 5, 'c' => Math.sqrt(45)}
=> {"ag"=>5, "c"=>6.708203932499369}
- where possible, symbols are used instead of strings
>> marks = {:Foo => 86, :Bar => 92, :Baz => 75}
=> {:Foo=>86, :Bar=>92, :Baz=>75}
# simpler alternate syntax
>> marks = {Foo: 86, Bar: 92, Baz: 75}
=> {:Foo=>86, :Bar=>92, :Baz=>75}
- hashes can also be created using
new
method of Hash class- helpful to declare a default value for keys
- See ruby-doc: Hash for more details
>> h = Hash.new
=> {}
>> h[2] = 'foo'
=> "foo"
>> h
=> {2=>"foo"}
# 0 will be default value for keys instead of nil
>> hist = Hash.new(0)
=> {}
# to avoid same mutable object as default, use the block form
>> greetings = Hash.new('good day')
=> {}
>> greetings[:foo].equal?(greetings[:baz])
=> true
>> greetings = Hash.new { 'good day' }
=> {}
>> greetings[:foo].equal?(greetings[:baz])
=> false
- array-hash conversion
>> {1=>2, "foo"=>"baz"}.to_a
=> [[1, 2], ["foo", "baz"]]
>> [[1, 2], ['foo', 'baz']].to_h
=> {1=>2, "foo"=>"baz"}
>> [[:Foo, 86], [:Bar, 92], [:Baz, 75]].to_h
=> {:Foo=>86, :Bar=>92, :Baz=>75}
Accessing keys and values
- accessing based on key and getting all keys/values
>> marks = {Foo: 86, Bar: 92, Baz: 75}
=> {:Foo=>86, :Bar=>92, :Baz=>75}
>> marks[:Foo]
=> 86
>> marks['Foo'.to_sym]
=> 86
# key-value pair
>> marks.assoc(:Bar)
=> [:Bar, 92]
>> marks.keys
=> [:Foo, :Bar, :Baz]
>> marks.values
=> [86, 92, 75]
- if key doesn't exist
>> marks = {Foo: 86, Bar: 92, Baz: 75}
=> {:Foo=>86, :Bar=>92, :Baz=>75}
# nil by default, or the value set as default using new/default= methods
>> marks[:xyz]
=> nil
# this will raise an exception irrespective of default value being set
>> marks.fetch(:xyz)
KeyError (key not found: :xyz)
# default value can be specified as 2nd argument/block
>> marks.fetch(:xyz, 60)
=> 60
- reverse look-up, getting the first key or key-value pair based on given value
>> marks = {Foo: 86, Bar: 75, Baz: 75}
=> {:Foo=>86, :Bar=>75, :Baz=>75}
>> marks.key(75)
=> :Bar
>> marks.rassoc(75)
=> [:Bar, 75]
>> marks.key(100)
=> nil
- Hash slice
>> marks = {Foo: 86, Bar: 92, Baz: 75, Lo: 73}
=> {:Foo=>86, :Bar=>92, :Baz=>75, :Lo=>73}
>> marks.slice(:Baz)
=> {:Baz=>75}
>> marks.slice(:Bar, :Lo)
=> {:Bar=>92, :Lo=>73}
# if key doesn't match
>> marks.slice(:Bar, :Low)
=> {:Bar=>92}
>> marks.slice(:Low)
=> {}
- getting multiple values
>> marks = {Foo: 86, Bar: 92, Baz: 75, Lo: 73}
=> {:Foo=>86, :Bar=>92, :Baz=>75, :Lo=>73}
>> marks.values_at(:Bar, :Lo)
=> [92, 73]
# if key is not present, gives nil or the default value if set
>> marks.values_at(:Baz, :xyz)
=> [75, nil]
>> marks.fetch_values(:Baz, :Lo)
=> [75, 73]
>> marks.fetch_values(:Baz, :xyz)
KeyError (key not found: :xyz)
Looping
- for loop and each method to iterate over key-value pairs
- Note that Hashes maintain the same order in which key-value pairs were added
>> fav_books = {
?> 'fantasy' => ['Harry Potter', 'Stormlight Archive', 'Kingkiller Chronicle'],
?> 'sci-fi' => ['Enders Game', 'Martian', 'Red Rising'],
?> 'classic' => ['Count of Monte Cristo', 'Jane Eyre', 'Scarlet Pimpernel']
>> }
>> for genre, books in fav_books
>> puts "#{genre}: #{books.join(', ')}"
>> end
fantasy: Harry Potter, Stormlight Archive, Kingkiller Chronicle
sci-fi: Enders Game, Martian, Red Rising
classic: Count of Monte Cristo, Jane Eyre, Scarlet Pimpernel
>> fav_books.each { |genre, books| puts "#{genre}: #{books.join(', ')}" }
fantasy: Harry Potter, Stormlight Archive, Kingkiller Chronicle
sci-fi: Enders Game, Martian, Red Rising
classic: Count of Monte Cristo, Jane Eyre, Scarlet Pimpernel
- to iterate over all the keys or values
>> fav_books.each_key { |k| puts k }
fantasy
sci-fi
classic
>> fav_books.each_value { |v| puts v.inspect }
["Harry Potter", "Stormlight Archive", "Kingkiller Chronicle"]
["Enders Game", "Martian", "Red Rising"]
["Count of Monte Cristo", "Jane Eyre", "Scarlet Pimpernel"]
Modifying elements
- adding/modifying single/multiple elements
>> marks = {foo: 86, baz: 75, lo: 73}
=> {:foo=>86, :baz=>75, :lo=>73}
>> marks[:foo] = 95
=> 95
>> marks[:baz], marks[:kek] = [80, 62]
=> [80, 62]
>> marks
=> {:foo=>95, :baz=>80, :lo=>73, :kek=>62}
- merging two hashes
>> marks = {foo: 86, baz: 75}
=> {:foo=>86, :baz=>75}
>> new_marks = {foo: 95, baz: 80, lo: 73}
=> {:foo=>95, :baz=>80, :lo=>73}
# use merge! for in-place modification
>> marks.merge(new_marks)
=> {:foo=>95, :baz=>80, :lo=>73}
# block form
>> marks.merge(new_marks) { |k, v1, v2| v1 + v2/10 }
=> {:foo=>95, :baz=>83, :lo=>73}
- deleting elements
# based on key
>> marks = {foo: 86, baz: 75, lo: 73}
=> {:foo=>86, :baz=>75, :lo=>73}
>> marks.delete(:foo)
=> 86
>> marks
=> {:baz=>75, :lo=>73}
# based on a condition
>> marks = {foo: 86, baz: 75, lo: 73}
=> {:foo=>86, :baz=>75, :lo=>73}
>> marks.delete_if { |k, v| v < 80 }
=> {:foo=>86}
>> marks
=> {:foo=>86}
# delete all elements
>> marks.clear
=> {}
Filtering
- check if a key/value is present
>> fruits = {'apple' => 5, 'mango' => 10, 'guava' => 6}
=> {"apple"=>5, "mango"=>10, "guava"=>6}
# can also use the alias include?
>> fruits.key?('apple')
=> true
>> fruits.key?('orange')
=> false
>> fruits.value?(10)
=> true
>> fruits.value?(2)
=> false
- slicing based on a condition
>> fruits = {'apple' => 5, 'mango' => 10, 'guava' => 6, 'orange' => 12}
=> {"apple"=>5, "mango"=>10, "guava"=>6, "orange"=>12}
# use select! for in-place modification
>> fruits.select { |k, v| v > 5 }
=> {"mango"=>10, "guava"=>6, "orange"=>12}
# apply keys/values methods when needed
>> fruits.select { |k, v| v > 5 }.keys
=> ["mango", "guava", "orange"]
>> fruits.select { |k, v| v > 5 }.values
=> [10, 6, 12]
>> fruits.select { |k, v| k.length > 5 }
=> {"orange"=>12}
>> fruits.select { |k, v| k[0] < 'o' && v > 5 }
=> {"mango"=>10, "guava"=>6}
- Like arrays, one can use ruby-doc: Enumerable methods for hashes as well
- use
partition
to get both matching and non-matching elements
>> fruits = {'apple' => 5, 'mango' => 10, 'guava' => 6, 'orange' => 12}
=> {"apple"=>5, "mango"=>10, "guava"=>6, "orange"=>12}
# returns array of arrays
>> fruits.partition { |k, v| k.length > 5 }
=> [[["orange", 12]], [["apple", 5], ["mango", 10], ["guava", 6]]]
# convert to hash if needed
>> m_h, nm_h = fruits.partition { |k, v| k.length > 5 }.map(&:to_h)
=> [{"orange"=>12}, {"apple"=>5, "mango"=>10, "guava"=>6}]
>> m_h
=> {"orange"=>12}
>> nm_h
=> {"apple"=>5, "mango"=>10, "guava"=>6}
Transforming keys and values
- changing keys
>> fruits = {'apple' => 5, 'mango' => 10, 'guava' => 6, 'orange' => 12}
=> {"apple"=>5, "mango"=>10, "guava"=>6, "orange"=>12}
>> fruits.transform_keys { |k| k.capitalize }
=> {"Apple"=>5, "Mango"=>10, "Guava"=>6, "Orange"=>12}
# in-place modification
>> fruits.transform_keys! { |k| k.capitalize }
=> {"Apple"=>5, "Mango"=>10, "Guava"=>6, "Orange"=>12}
>> fruits.transform_keys(&:to_sym)
=> {:Apple=>5, :Mango=>10, :Guava=>6, :Orange=>12}
- changing values
>> marks = {"foo" => 90, "baz" => 80, "lo" => 73, "kek" => 62}
=> {"foo"=>90, "baz"=>80, "lo"=>73, "kek"=>62}
>> marks.transform_values { |v| v + 5 }
=> {"foo"=>95, "baz"=>85, "lo"=>78, "kek"=>67}
# in-place modification
>> marks.transform_values! { |v| v - 5 }
=> {"foo"=>85, "baz"=>75, "lo"=>68, "kek"=>57}
- use
map
to work with keys and values together
>> marks = {"foo" => 90, "baz" => 80, "lo" => 73, "kek" => 62}
=> {"foo"=>90, "baz"=>80, "lo"=>73, "kek"=>62}
>> marks.map { |k, v| marks[k] = v + 5 if k[0] > 'c' }
=> [95, nil, 78, 67]
>> marks
=> {"foo"=>95, "baz"=>80, "lo"=>78, "kek"=>67}
Mutable keys
- strings used as hash key are frozen and cannot be modified - they act like immutable keys much like integers
>> s = 'foo'
=> "foo"
>> h = { s => 42 }
=> {"foo"=>42}
>> s.object_id
=> 47309460553140
>> h.keys[0].object_id
=> 47309460712980
>> s.upcase!
=> "FOO"
>> h
=> {"foo"=>42}
>> h.keys[0].capitalize!
FrozenError (can't modify frozen String)
- using mutable types like array and hash as key requires careful usage
- changing the mutable key will leave the hash with old hash value unless recalculated using
rehash
method- until rehashing, it will also be possible to add another key with same value as the modified mutable key
- See also softwareengineering.stackexchange: why use mutable keys?
>> a = [3.14, 42]
=> [3.14, 42]
>> h = { a => 'foo' }
=> {[3.14, 42]=>"foo"}
>> a.equal?(h.keys[0])
=> true
>> a[1] = 100
=> 100
>> h
=> {[3.14, 100]=>"foo"}
>> h[a]
=> nil
>> h[[3.14, 100]]
=> nil
>> h.rehash
=> {[3.14, 100]=>"foo"}
>> h[a]
=> "foo"
>> h[[3.14, 100]]
=> "foo"
Miscellaneous
- use Enumerable methods
sort
andsort_by
for sorting needs - the output is an array of arrays, so convert back to hash when needed
>> marks = {"foo" => 90, "baz" => 80, "lo" => 73, "kek" => 62}
=> {"foo"=>90, "baz"=>80, "lo"=>73, "kek"=>62}
# by default, keys get sorted in ascending order
>> marks.sort
=> [["baz", 80], ["foo", 90], ["kek", 62], ["lo", 73]]
# block form example
>> marks.sort { |a, b| b[0] <=> a[0] }.to_h
=> {"lo"=>73, "kek"=>62, "foo"=>90, "baz"=>80}
>> marks.sort { |a, b| a[1] <=> b[1] }.to_h
=> {"kek"=>62, "lo"=>73, "baz"=>80, "foo"=>90}
# sort_by example
>> marks.sort_by { |k, v| -v }.to_h
=> {"foo"=>90, "baz"=>80, "lo"=>73, "kek"=>62}
flatten
method will convert hash to array with each key-value pair forming two elements- optional argument allows to specify depth of flattening
compact
method will remove all keys withnil
value- keys with
nil
value within nested hash won't be removed
- keys with
>> marks = {"foo" => 90, "baz" => 80, "lo" => 73}
=> {"foo"=>90, "baz"=>80, "lo"=>73}
>> marks.flatten
=> ["foo", 90, "baz", 80, "lo", 73]
>> h = { 'a' => [1, 2, 4], 'b' => %w[foo baz] }
=> {"a"=>[1, 2, 4], "b"=>["foo", "baz"]}
>> h.flatten
=> ["a", [1, 2, 4], "b", ["foo", "baz"]]
>> h.flatten(2)
=> ["a", 1, 2, 4, "b", "foo", "baz"]
>> h = { 'a' => 42, 'b' => 'foo', 'c' => nil }
=> {"a"=>42, "b"=>"foo", "c"=>nil}
# use compact! for in-place modification
>> h.compact
=> {"a"=>42, "b"=>"foo"}
invert
will swap key-value pairs- in case of multiple keys with same value, the last key-value pair will win
>> h = { 'a' => 42, 'b' => 'foo', 'c' => 3, 'd' => 42 }
=> {"a"=>42, "b"=>"foo", "c"=>3, "d"=>42}
>> h.invert
=> {42=>"d", "foo"=>"b", 3=>"c"}
>> h.length == h.invert.length
=> false
>> h = { foo: 90, baz: 80 }
=> {:foo=>90, :baz=>80}
>> h.invert
=> {90=>:foo, 80=>:baz}
>> h.length == h.invert.length
=> true
- getting user input
- manually convert based on agreed upon delimiter or use
eval
if input can be trusted
>> usr_ip = gets.chomp
foo:baz:123:good
=> "foo:baz:123:good"
>> h = usr_ip.split(':').each_slice(2).to_h
=> {"foo"=>"baz", "123"=>"good"}
>> usr_ip = gets.chomp
{ a: 42, b: 78, c: 99 }
=> "{ a: 42, b: 78, c: 99 }"
>> h = eval(usr_ip)
=> {:a=>42, :b=>78, :c=>99}