rubyisforfun/manuscript/072.txt
2019-10-17 17:36:59 -07:00

121 lines
4.1 KiB
Text

## "dig" method
Look at the following nested data structure:
```ruby
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:
```ruby
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:
```ruby
$ 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:
```ruby
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`"?
```ruby
street: { line1: '...', line2: '...' }
```
So that we always have two lines of street address for "`address`" node. Here is how it looks:
```ruby
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:
```ruby
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:
```ruby
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:
```ruby
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:
```ruby
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](https://en.wikipedia.org/wiki/Safe_navigation_operator)", which can be useful for nil checks while working with chains of objects and methods.