Menu
Alfredo Motta
  • Home
  • Newsletter
  • Articles
  • About me
  • Events
  • Books
  • Research
  • Learn to code in London
Alfredo Motta

Introducing Hash#dig_and_collect, a useful extension to the Ruby Hash#dig method

Posted on November 4, 2016May 18, 2022

In this blog post I will introduce Hash#dig_and_collect , a simple utility method that is built on top of Hash#dig to help you navigate nested hashes mixed up with arrays.

Why Hash#dig is great

The introduction of the  Hash#dig1 method in Ruby 2.3 completely changed the way I navigate deeply nested hashes. For example, given this Hash :

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
client = {
  details: {
    first_name: "Florentino",
    last_name: "Perez"
  },
  addresses: {
    postcode: "SE1 9SG",
    street: London Bridge St,
    number: 32,
    city: "London",
    location: {
      latitude: 51.504382,
      longitude: -0.086279
    }
  }
}

to get the latitude and longitude you need to do the following:

Ruby
1
client[:addresses] && client[:addresses][:location] && client[:addresses][:location][:latitude]

which is not only weird, but also verbose. Since Ruby 2.3 you can finally write:

Ruby
1
client.dig(:addresses, :location, :latitude)

which is, needless to say, awesome. However, I started wondering if it was possible to stretch the idea of Hash#dig even further when you have to deal with Hashes that also have Arrays within them.

When Hash#dig is not enough?

In the previous example the addresses key contains a Hash, but this is often a soft guarantee. This means that when multiple addresses are present you may actually find an  Array instead. For example:

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
client = {
  details: {
    first_name: "Florentino",
    last_name: "Perez"
  },
  addresses: [
    {
      type: "home",
      postcode: "SE1 9SG",
      street: "London Bridge St",
      number: 32,
      city: "London",
      location: {
        latitude: 51.504382,
        longitude: -0.086279
      }
    },
    {
      type: "office",
      postcode: "SW1A 1AA",
      street: "Buckingham Palace Road",
      number: nil,
      city: "London",
      location: {
        latitude: 51.5013673,
        longitude: -0.1440787
      }
    }
  ]
}

What’s the problem here? Now not only addresses could be  nil or not, but depending if the user has one address or multiple addresses the value of the :addresses key could be a Hash  or an Array . This is often the reality of many real-world hashes, and even though we could argue that the data structure design is wrong, somehow we have to deal with it.

A simple solution

Let’s assume that we want to collect all the latitude values of our client  addresses. Hash#dig will not work in this case simply because it doesn’t know what to do as soon as an Array  is found:

Ruby
1
2
puts client.dig(:addresses, :location, :latitude)
# `dig': no implicit conversion of Symbol into Integer (TypeError)

The code will have to fetch the :addresses  key. Then if it’s a Hash use dig  to get the :latitude  from the location key. If it is an Array  it should iterate over all the addresses and collect the :latitude  from the various locations. Here it is:

Ruby
1
2
3
4
5
6
7
8
9
10
def get_offices_latitudes_for(client)
  addresses = client[:addresses]
  return [] if addresses.nil?
 
  if addresses.is_a? Hash
    [ addresses.dig(:location, :latitude) ]
  else
    addresses.map { |address| address.dig(:location, :latitude) }
  end
end

And these are three simple tests that prove that it works:

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
client_with_no_addresses = {
  details: {
    first_name: "Florentino",
    last_name: "Perez"
  },
  addresses: nil
}
 
client_with_one_address = {
  details: {
    first_name: "Florentino",
    last_name: "Perez"
  },
  addresses:  {
    type: "home",
    postcode: "SE1 9SG",
    street: "London Bridge St",
    number: 32,
    city: "London",
    location: {
      latitude: 51.504382,
      longitude: -0.086279
    }
  }
}
 
client_with_many_addresses = {
  details: {
    first_name: "Florentino",
    last_name: "Perez"
  },
  addresses: [
    {
      type: "home",
      postcode: "SE1 9SG",
      street: "London Bridge St",
      number: 32,
      city: "London",
      location: {
        latitude: 51.504382,
        longitude: -0.086279
      }
    },
    {
      type: "office",
      postcode: "SW1A 1AA",
      street: "Buckingham Palace Road",
      number: nil,
      city: "London",
      location: {
        latitude: 51.5013673,
        longitude: -0.1440787
      }
    }
  ]
}
 
puts get_offices_latitudes_for(client_with_no_addresses).join(", ")
# empty string
 
puts get_offices_latitudes_for(client_with_one_address).join(", ")
# 51.504382
 
puts get_offices_latitudes_for(client_with_many_addresses).join(", ")
# 51.504382, 51.5013673

Introducing Hash#dig_and_collect

I don’t know about you, but I hate the previous code. How can we make it better? Being inspired by Hash#dig what we need here is a good default for navigating deeper our Hash  when we find an Array . I strongly feel a good solution is to collect all the values, exactly like we did in our naïve solution.

Going back to our example, when a client does not have any addresse the code should return an empty array, there was nothing to collect.

Ruby
1
2
client_with_no_addresses.dig_and_collect(:addresses, :location, :latitude)
# []

The scenarios in which a client has just one address (  addresses  key is a Hash ) or multiple addresses (  addresses  key is an Array ) should now be straightforward:

1
2
3
4
5
client_with_one_address.dig_and_collect(:addresses, :location, :latitude)
# [51.504382]
 
client_with_many_addresses.dig_and_collect(:addresses, :location, :latitude)
# [51.504382, 51.5013673]

The implementation is fairly simple and you can read it on Github. We are monkey patching the Hash object, but I am sure it could be done better. Suggestions that involves Ruby refinements are more than welcome.

The solution recursively check what type of key you are trying to fetch and depending on the type recursively call dig_and_collect  either on a Hash  directly, or on all the elements in the Array  that you found along your path.

The rest of the code with the specs is available in this Github repo and I would really love to hear your feedback.

Conclusion

In this blog I presented Hash#dig_and_collect , a simple utility method that is built on top of Hash#dig to help you navigate complex nested hashes. I found it really handy when dealing with badly designed Hashes out in the wild but I feel it could be helpful in other scenarios. Looking forward to hearing your thoughts.

If you enjoyed this blog post you can also follow me on twitter.

References

  1. Ruby Hash#dig documentation

2 thoughts on “Introducing Hash#dig_and_collect, a useful extension to the Ruby Hash#dig method”

  1. Brian Katzung says:
    July 17, 2019 at 1:47 am

    There’s also Gem XKeys, which has allowed you to read and write nested arrays and hashes since before #dig. You can do things like:

    section = [:system, :users] # How about a (partial?) variable path?
    info[*section, 271, :accesses, :else => 0] += 1 # Default 0, not nil, to increment
    info[:system, :users, :[]] = new_user_info # Push (if [:users] is array)

    Reply
    1. mottalrd says:
      July 17, 2019 at 6:07 am

      Thank you for the great tip Brian!

      Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Answer the question * Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Tweets by mottalrd

Recent Comments

  • mottalrd on Not So Random Software #21 – Technical debt
  • mottalrd on Not So Random Software #24 – Climate Change Tech
  • mottalrd on A/B Testing, from scratch
  • mottalrd on A/B Testing, from scratch
  • Karim on A/B Testing, from scratch
©2023 Alfredo Motta | Powered by SuperbThemes & WordPress