5

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
Florin Lei
  • 521
  • 8
  • 16
  • In Ruby 2.4.5 I do not get the same results. `response.dig(:results)[0].dig(:value).dig(:destination)` gives "Rome", and `response.dig(:results)[0].dig(:value).fetch('destination')` raises `KeyError: key not found: "destination"` – David Aldridge Mar 30 '19 at 09:59
  • I forgot to mention that I'm using ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux] and Rails 5.2.1.1 – Florin Lei Mar 30 '19 at 10:03
  • @DavidAldridge I've added a picture, hope now that the issue is more explanatory – Florin Lei Mar 30 '19 at 10:16
  • Your question is straightforward, but I think it's muddled by I lot of extraneous information, which I believe accounts for the downvotes and vote to close. To highlight code in text, bracket it with backticks. To highlight a block of code indent it 4 spaces or select it and click the `{}` button. When giving an example assign a variable to all inputs (so readers can refer to those variables in answers and comments without having to define them) and always state the desired or expected return value (`"Rome"`). (cont...) – Cary Swoveland Mar 30 '19 at 17:57
  • ... Lastly, in most SO questions you should say what you want to achieve (return `"Rome"`) without any reference to how it should be done. Then show what you have tried, which is to use `dig` and `fetch`. Some may deem your question as the statement of an [XY problem](https://en.wikipedia.org/wiki/XY_problem). (There are exceptions, such as when you want to use a regular expression to extract information from a string, though you might invite other solutions that do not employ a regex.) If you don't like my edit please change it however you like. It's your question! – Cary Swoveland Mar 30 '19 at 18:13

2 Answers2

14

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
2

Hash#dig

dig(key, ...) → object

Extracts the nested value specified by the sequence of key objects by calling dig at each step, returning nil if any intermediate step is nil.

Hash#fetch

fetch(key [, default] ) → obj

fetch(key) {| key | block } → obj

Returns a value from the hash for the given key. If the key can't be found, there are several options: With no other arguments, it will raise a KeyError exception; if default is given, then that will be returned; if the optional code block is specified, then that will be run and its result returned.

That how difference looks at your example:

response = {
  "results": [
    {
      "type": 'product_group',
      "value": {
        "destination": 'Rome'
      }
    },
    {
      "type": 'product_group',
      "value": {
        "destination": 'Paris'
      }
    },
    {
      "type": 'product_group',
      "value": {
        "destination": 'Madrid'
      }
    }
  ]
}

response[:results].first.dig(:value, :destination) #=> "Rome"
response[:results].first.fetch(:value).fetch(:destination) #=> "Rome"

Community
  • 1
  • 1
mechnicov
  • 12,025
  • 4
  • 33
  • 56