Books / Ruby for Beginners / Chapter 29

dig method

Look at the following nested data structure:

users = [
    { first: 'John', last: 'Smith', address: { city: 'San Francisco', country: 'US' } },
    { first: 'Pat', last: 'Roberts', address: { country: 'US' } },
    { first: 'Sam', last: 'Schwartzman' }
]

The structure above has its own data scheme. The format is the same for every record (there are 3 total records in this array), but two last records are missing something. For example, the second one is missing the “city”. Third record doesn’t have “address”. And what we want is to print all the cities from all the records (we might have more than three).

First thing that comes to mind is iteration over the array of elements and using standard hash access:

users.each do |user|
  puts user[:address][:city]
end

Why that wouldn’t work? Let’s give it a try:

San Francisco

-:8:in `block in <main>': undefined method `[]' for nil:NilClass (NoMethodError).

Oops, it produces error. But why? Well, let’s try to access every element manually:

$ pry
> users[0][:address][:city]
=> "San Francisco"
> users[1][:address][:city]
=> nil
> users[2][:address][:city]
NoMethodError: undefined method `[]' for nil:NilClass

Here we go. It worked for the first element. There was also no any error for the second element, result is just “nil”. However, for the third user (user with index 2) expression “users[2][:address]” gives “nil”, because there is no “address” field for Sam. And then we basically execute “nil[:city]” which always produces error, because you can’t access nil like that, there is nothing inside nils.

So how do we fix this program? For example, by using if-statement:

users.each do |user|
  if user[:address]
    puts user[:address][:city]
  end
end

It works now and there is no error, we did a great job! But what if we add one more nested object to “address”?

street: { line1: '...', line2: '...' }

So that we always have two lines of street address for “address” node. Here is how it looks:

users = [
    {
        first: 'John',
        last: 'Smith',
        address: {
            city: 'San Francisco',
            country: 'US',
            street: {
                line1: '555 Market Street',
                line2: 'apt 123'
            }
        }
    },
    { first: 'Pat', last: 'Roberts', address: { country: 'US' } },
    { first: 'Sam', last: 'Schwartzman' }
]

Now we want to print all “line1” addresses for all the records. Can you do that? Here is the first thing we might want to do - improve already existing program by just adding “[:line1]” navigation:

users.each do |user|
  if user[:address]
    puts user[:address][:street][:line1]
  end
end

However, the the code above will choke on the second record, because for the second record “user[:address][:street]” is nil. If it is not clear, don’t hesitate to try it yourself in your pry/irb console.

What we can do is to add another check for nil:

users.each do |user|
  if user[:address] && user[:address][:street]
    puts user[:address][:street][:line1]
  end
end

It works great with the second condition. Here we check if “address” is not nil and if the following “street” is not nil:

if user[:address] && user[:address][:street]

In other words, for every level of nesting we will need to add one more check, so our program won’t raise any errors on nils. It wasn’t very convenient and programmer-friendly, so Ruby starting from 2.3.0 (you can check your version by running “ruby -v”) has “dig” method:

users.each do |user|
  puts user.dig(:address, :street, :line1)
end

Method accepts any number of parameters and you can use it to access deeply nested structure. If one or more keys in the provided chain wasn’t found, the method returns nil.

Side note: keep in mind that Rails has similar (but slightly different) “try” method, and latest version of Ruby also implements “safe navigation operator”, which can be useful for nil checks while working with chains of objects and methods.


Licenses and Attributions


Speak Your Mind

-->