1

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

4 Answers 4

2

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.

Sign up to request clarification or add additional context in comments.

3 Comments

It is possible also to set the values? burrow(h, ['2223','1113'], 5) would set the value '5'
Tonja, let me know if you don't want to alter the hash in place; that is, if you want a new hash returned that reflects the update without altering the original hash.
Just in case you might have an idea of how to make it work: followup question
1

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.

2 Comments

and for ruby < 2.3.0 ?
Create a dig-like method? Imho the method was already somewhat overdue when it was introduced in 2.3.0. Ofc you can access nested elements with just my_hash['2223']['1113'], but I don't know of a programaticly access nested elements from an array without dig.
0

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"}}

2 Comments

so, with just simple my_hash.dig(*my_array) ='5' my_hash will not get updated, right? because I want to update only the values of my_hash['2223']['1113']
Hash#dig can be used to retrieve values - it cannot be used to set the values
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.