I'm trying to obtain a nested value in a hash. I've tried using Hash#fetch
and Hash#dig
but I don't understand how they should be combined.
My hash is as follows.
response = {
"results":[
{
"type":"product_group",
"value":{
"destination":"Rome"
}
},
{
"type":"product_group",
"value":{
"destination":"Paris"
}
},
{
"type":"product_group",
"value":{
"destination":"Madrid"
}
}
]
}
I've tried the following
response.dig(:results)[0].dig(:value).dig(:destination) #=> nil
response.dig(:results)[0].dig(:value).fetch('destination') #=> Rome
The desired return value is "Rome"
. The second expression works but I would like to know if it can be simplified.
I'm using Ruby v2.5 and Rails v5.2.1.1.
Hash#fetch is not relevant here. That's because fetch
is the same as Hash#[] when, as here, fetch
has only a single argument. So let's concentrate on dig
.
A family of three dig
methods were introduced in Ruby v2.3: Hash#dig, Array#dig and OpenStruct#dig. An interesting thing about these methods is that they invoke each other (but that's not explained in the docs, not even in the examples). In your problem we can write:
response.dig(:results, 0, :value, :destination)
#=> "Rome"
response
is a hash so Hash#dig
evaluates response[:results]
. If it's value is nil
then the expression returns nil
. For example,
response.dig(:cat, 0, :value, :destination)
#=> nil
In fact, response[:results]
is an array:
arr = response[:results]
#=> [{:type=>"product_group", :value=>{:destination=>"Rome"}},
# {:type=>"product_group", :value=>{:destination=>"Paris"}},
# {:type=>"product_group", :value=>{:destination=>"Madrid"}}]
Hash#dig
therefore invokes Array#dig
on arr
, obtaining the hash
h = arr.dig(0)
#=> {:type=>"product_group", :value=>{:destination=>"Rome"}}
Array#dig
then invokes Hash#dig
on h
:
g = h.dig(:value)
#=> {:destination=>"Rome"}
Lastly, g
being a hash, Hash#dig
invokes Hash#dig
on g
:
g.dig(:destination)
#=> "Rome"
Caution needs to be exercised when using any of the dig
's. Suppose
arr = [[1,2], [3,[4,5]]]
and we wish to pull out the object that is now occupied by 4
. We could write either
arr[1][1][0]
#=> 4
or
arr.dig(1,1,0)
#=> 4
Now suppose arr
were changed as follows:
arr = [[1,2]]
Then
arr[1][1][0]
#=> NoMethodError: undefined method `[]' for nil:NilClass
and
arr.dig(1,1,0)
#=> nil
Both are indicative of there being an error in our code, so raising an exception would be preferable to nil
being returned, which may go unnoticed for some time.