Alina
Alina

Reputation: 2261

How to get values from hash for only specific keys stored in array

I want to be able to get only the elements from hash by using keys that are stored in the array.

I have a hash:

my_hash = { "2222"=> {"1111"=> "1"}, "2223"=>{"1113"=> "2"}, "12342"=> {"22343"=> "3"}}

or

my_hash2 = { "2222"=>"1", "1111"=> "2", "12342"=> "3"}

And an array:

my_array = ['2223','1113']
my_array2 = ['12342']

my_array represents the chained keys in my hash. The level of my_hash can vary from 1 to ..., therefore the length of my_array will also vary. So, I need a general solution (not only for two level-hash).

My idea is to do something like this but it is wrong.

my_hash[my_array] = '2'
my_hash2[my_array2] = '3'

In fact, I want to be able to set the values. my_hash[my_array] = '5' would set the value of my_hash["2223"]["2223"] to 5

Upvotes: 1

Views: 3354

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110755

Hash#dig made its debut quite recently, in Ruby v2.3. If you need to support earlier versions of Ruby you can use Enumerable#reduce (aka inject).

def burrow(h, a)
  a.reduce(h) { |g,k| g && g[k] }
end

h = {"2222"=>{"1111"=>"1"}, "2223"=>{"1113"=>"2"}, "12342"=>{"22343"=>"3"}}

burrow(h, ['2223','1113']) #=> "2"
burrow(h, ['2223'])        #=> {"1113"=>"2"}
burrow(h, ['2223','cat'])  #=> nil
burrow(h, ['cat','1113'])  #=> nil

This works because if, for some element k in a, the hash given by the block variable g (the "memo") does not have a key k, g[k] #=> nil, so nil becomes the value of the memo g and will remain nil for all subsequent values of a that are passed to the block. This is how digging was normally done when I was a kid.

To change a value in place we can do the following.

def burrow_and_update(h, a, v)
  *arr, last = a
  f = arr.reduce(h) { |g,k| g && g[k] }
  return nil unless f.is_a?(Hash) && f.key?(last)
  f[last] = v
end

burrow_and_update(h, ['2223','1113'], :cat) #=> :cat 
h #=> {"2222"=>{"1111"=>"1"}, "2223"=>{"1113"=>:cat}, "12342"=>{"22343"=>"3"}} 

h = {"2222"=>{"1111"=>"1"}, "2223"=>{"1113"=>"2"}, "12342"=>{"22343"=>"3"}} # reset h
burrow_and_update(h, ['2223', :dog], :cat)
  #=> nil

In the second case nil is returned because {"1113"=>"2"} does not have a key :dog.

Upvotes: 2

the Tin Man
the Tin Man

Reputation: 160631

If I knew the code was going to run on a Ruby that had dig available I'd use dig, but to fall back I'd use something like this:

class Hash
  def deep_get(*keys)
    o = self
    keys.each { |k| o = o[k] }
    o
  end

  def deep_set(*keys, v)
    o = self
    keys[0..-2].each { |k| o = o[k] }
    o[keys.last] = v
  end
end

my_hash = { "2222"=> {"1111"=> "1"}, "2223"=>{"1113"=> "2"}, "12342"=> {"22343"=> "3"}}
my_array = ['2223','1113']

my_hash.deep_get(*my_array)  # => "2"

Assigning to the hash based on my_array:

my_hash.deep_set(*my_array, '4')

my_hash.deep_get(*my_array) # => "4"
my_hash  # => {"2222"=>{"1111"=>"1"}, "2223"=>{"1113"=>"4"}, "12342"=>{"22343"=>"3"}}

Of course, patching Hash isn't recommended these days. You should use Refinements instead but if those aren't available then you'd have to patch it.

This code doesn't try to handle errors, such as if the array of keys doesn't match the keys in the hash. How to handle that and what to return is left for you to figure out.

Upvotes: 0

Glyoko
Glyoko

Reputation: 2080

You can use the Hash#dig method.

my_hash = { "2222"=> {"1111"=> "1"}, "2223"=>{"1113"=> "2"}, "12342"=> {"22343"=> "3"}}
my_hash.dig("2222", "1111")
# => 1

my_array = ["2222", "1111"]
my_hash.dig(*my_array) # with the splat operator
# => 1

Please note that Hash#dig only exists in Ruby 2.3+. If you're using an older version, this won't work.

Upvotes: 1

Wand Maker
Wand Maker

Reputation: 18772

To retrieve the value, you can use Hash#dig as suggested in other answer.

If you wish to update the hash, then, you will need to do bit of more work - here is one way to accomplish that:

my_hash = { "2222"=> {"1111"=> "1"}, "2223"=>{"1113"=> "2"}, "12342"=> {"22343"=> "3"}}
my_array = ['2223','1113']

target_hash = my_array.length > 1 ? 
                 my_hash.dig(*my_array[0, my_array.length - 1]) : 
                 my_hash

target_hash[my_array.last] = "5"

p my_hash
#=> {"2222"=>{"1111"=>"1"}, "2223"=>"5", "12342"=>{"22343"=>"3"}}

Upvotes: 0

Related Questions