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

Viewed 57223

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?

16 Answers

You can also define a module to alias the brackets methods and use the Ruby syntax to read/write nested elements.

UPDATE: Instead of overriding the bracket accessors, request Hash instance to extend the module.

module Nesty
  def []=(*keys,value)
    key = keys.pop
    if keys.empty? 
      super(key, value) 
    else
      if self[*keys].is_a? Hash
        self[*keys][key] = value
      else
        self[*keys] = { key => value}
      end
    end
  end

  def [](*keys)
    self.dig(*keys)
  end
end

class Hash
  def nesty
    self.extend Nesty
    self
  end
end

Then you can do:

irb> a = {}.nesty
=> {}
irb> a[:a, :b, :c] = "value"
=> "value"
irb> a
=> {:a=>{:b=>{:c=>"value"}}}
irb> a[:a,:b,:c]
=> "value"
irb> a[:a,:b]
=> {:c=>"value"}
irb> a[:a,:d] = "another value"
=> "another value"
irb> a
=> {:a=>{:b=>{:c=>"value"}, :d=>"another value"}}

I don't know how "Ruby" it is(!), but the KeyDial gem which I wrote lets you do this basically without changing your original syntax:

has_kids = !dino[:person][:children].nil?

becomes:

has_kids = !dino.dial[:person][:children].call.nil?

This uses some trickery to intermediate the key access calls. At call, it will try to dig the previous keys on dino, and if it hits an error (as it will), returns nil. nil? then of course returns true.

You can use a combination of & and key? it is O(1) compared to dig which is O(n) and this will make sure person is accessed without NoMethodError: undefined method `[]' for nil:NilClass

fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)
Related