Reputation: 2261
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
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
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
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
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