Ruby Style: How to check whether a nested hash element exists

55,040

Solution 1

The most obvious way to do this is to simply check each step of the way:

has_children = slate[:person] && slate[:person][:children]

Use of .nil? is really only required when you use false as a placeholder value, and in practice this is rare. Generally you can simply test it exists.

Update: If you're using Ruby 2.3 or later there's a built-in dig method that does what's described in this answer.

If not, you can also define your own Hash "dig" method which can simplify this substantially:

class Hash
  def dig(*path)
    path.inject(self) do |location, key|
      location.respond_to?(:keys) ? location[key] : nil
    end
  end
end

This method will check each step of the way and avoid tripping up on calls to nil. For shallow structures the utility is somewhat limited, but for deeply nested structures I find it's invaluable:

has_children = slate.dig(:person, :children)

You might also make this more robust, for example, testing if the :children entry is actually populated:

children = slate.dig(:person, :children)
has_children = children && !children.empty?

Solution 2

With Ruby 2.3 we'll have support for the safe navigation operator: https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/

has_children now could be written as:

has_children = slate[:person]&.[](:children)

dig is being added as well:

has_children = slate.dig(:person, :children)

Solution 3

Another alternative:

dino.fetch(:person, {})[:children]

Solution 4

You can use the andand gem:

require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true

You can find further explanations at http://andand.rubyforge.org/.

Solution 5

One could use hash with default value of {} - empty hash. For example,

dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false

That works with already created Hash as well:

dino = {:pet=>{:name=>"Dino"}}
dino.default = {}
dino_has_children = !dino[:person][:children].nil? #=> false

Or you can define [] method for nil class

class NilClass
  def [](* args)
     nil
   end
end

nil[:a] #=> nil
Share:
55,040
Todd R
Author by

Todd R

"If it's not tested, how do you know whether it works?"

Updated on May 23, 2020

Comments

  • Todd R
    Todd R about 4 years

    Consider a "person" stored in a hash. Two examples are:

    fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
    slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 
    

    If the "person" doesn't have any children, the "children" element is not present. So, for Mr. Slate, we can check whether he has parents:

    slate_has_children = !slate[:person][:children].nil?
    

    So, what if we don't know that "slate" is a "person" hash? Consider:

    dino = {:pet => {:name => "Dino"}}
    

    We can't easily check for children any longer:

    dino_has_children = !dino[:person][:children].nil?
    NoMethodError: undefined method `[]' for nil:NilClass
    

    So, how would you check the structure of a hash, especially if it is nested deeply (even deeper than the examples provided here)? Maybe a better question is: What's the "Ruby way" to do this?

  • zeeraw
    zeeraw almost 13 years
    How would you go about using the same method for setting values in the nested hash?
  • tadman
    tadman almost 13 years
    You'd have to write something that creates the intermediate hashes instead of simply testing if they're there. location[key] ||= { } would be sufficient if you're dealing with hashes only but you'd have to extract the last part final_key = path.pop and assign to it in the end.
  • zeeraw
    zeeraw almost 13 years
    Thanks for the quick answer. This is how I solved the problem, I used the same inject method on the path, but put this in it's block instead. location[key] = ( location[key].class == Hash ) ? location[key] : value
  • tadman
    tadman almost 13 years
    You can also use the is_a? method which is more concise: location[key].is_a?(Hash) but this would exclude hash-like objects that sometimes come into play.
  • zeeraw
    zeeraw almost 13 years
    is_a?(hash) - good idea. keeping it short concise and simple :p
  • boulder_ruby
    boulder_ruby almost 12 years
    Where is the pluralization (:keys) coming from? I'm only aware of the Rails Inflector module..
  • tadman
    tadman almost 12 years
    :keys is a method that Hash provides and is (usually) a reliable enough indicator of the object in question being a Hash or Hash equivalent.
  • Sam Figueroa
    Sam Figueroa almost 12 years
    You would have to ensure that .default is nested deep into every hash.
  • xyz
    xyz over 11 years
    Many thanks ! by removing splat operator (*) from method argument you can pass string as parameter ex.: dig "path/path/path/path".split('/')
  • tadman
    tadman over 11 years
    You can still do that without changing the method signature. Call it like dig(*("a/b/c".split('/'))) instead. An alternative would be to iterate using path.flatten.inject to handle array arguments.
  • josiah
    josiah almost 9 years
    is_a? Hash should be faster since respond_to? has to traverse the methods on the object. Also, -1000 for encouraging monkey patching. Wrap/extend the Hash class, or create a Hash utility class and pass the hash under test into the method instead of monkey patching onto the Ruby Hash class. Monkey Patching is the main reason Chef needed to write their own dependency installer.
  • tadman
    tadman almost 9 years
    @Josiah If you've got a problem with extending core classes, better steer clear of Rails completely, it's endemic there. Used sparingly this is can make your code a lot cleaner. Used aggressively leads to chaos.
  • josiah
    josiah almost 9 years
    @tadman this is one of my misgivings about Ruby. I love the language and the enthusiasm of the community, but the community's tendancy to monkey patch here and there creates headaches. I put up with it in Rails, though I don't agree with it.
  • Gabe Kopley
    Gabe Kopley over 8 years
    @tadman fyi through this SO answer, #dig is now in Ruby trunk: ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-release‌​d :D
  • tadman
    tadman over 8 years
    @GabeKopley Wow, that's great news. I've found this function to be really helpful and I'm glad it's going mainstream now.
  • Colin Kelley
    Colin Kelley over 8 years
    If you are using Ruby < 2.3, I just published a gem that adds the 2.3-compatible Hash#dig and Array#dig methods: rubygems.org/gems/ruby_dig
  • bharath
    bharath over 7 years
    I gave a thought on @zeeraw 's comment to use is_a? for a nested hash and came up with this.