Ruby Style: How to check whether a nested hash element exists
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
Comments
-
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 almost 13 yearsHow would you go about using the same method for setting values in the nested hash?
-
tadman almost 13 yearsYou'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 partfinal_key = path.pop
and assign to it in the end. -
zeeraw almost 13 yearsThanks 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 almost 13 yearsYou 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 almost 13 yearsis_a?(hash) - good idea. keeping it short concise and simple :p
-
boulder_ruby almost 12 yearsWhere is the pluralization (:keys) coming from? I'm only aware of the Rails Inflector module..
-
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 almost 12 yearsYou would have to ensure that .default is nested deep into every hash.
-
xyz over 11 yearsMany thanks ! by removing splat operator (*) from method argument you can pass string as parameter ex.:
dig "path/path/path/path".split('/')
-
tadman over 11 yearsYou can still do that without changing the method signature. Call it like
dig(*("a/b/c".split('/')))
instead. An alternative would be to iterate usingpath.flatten.inject
to handle array arguments. -
josiah almost 9 years
is_a? Hash
should be faster sincerespond_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 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 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 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-released :D
-
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 over 8 yearsIf 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 over 7 yearsI gave a thought on @zeeraw 's comment to use is_a? for a nested hash and came up with this.