70

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?

Gaurav Sharma
  • 477
  • 9
  • 24
Todd R
  • 18,236
  • 8
  • 31
  • 39
  • 1
    Any reason why you haven't implemented an object model for this or at least decorated some structs with validation methods. I think you'll drive yourself nuts trying to add semantics onto a hash. – Chris McCauley Nov 30 '09 at 16:46
  • 1
    Even if you have an object model, you need sometimes to extract data from a hash to populate your model. For example if you get data from a JSON stream. – paradigmatic Nov 30 '09 at 17:23

16 Answers16

101

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?
tadman
  • 208,517
  • 23
  • 234
  • 262
  • 1
    How would you go about using the same method for setting values in the nested hash? – zeeraw Aug 04 '11 at 14:03
  • 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. – tadman Aug 04 '11 at 14:25
  • 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` – zeeraw Aug 04 '11 at 14:49
  • 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. – tadman Aug 05 '11 at 14:47
  • 2
    is_a?(hash) - good idea. keeping it short concise and simple :p – zeeraw Aug 10 '11 at 11:51
  • Where is the pluralization (:keys) coming from? I'm only aware of the Rails Inflector module.. – boulder_ruby Aug 03 '12 at 21:20
  • `: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. – tadman Aug 07 '12 at 14:46
  • Many thanks ! by removing splat operator (*) from method argument you can pass string as parameter ex.: `dig "path/path/path/path".split('/')` – xyz Feb 28 '13 at 21:04
  • 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. – tadman Feb 28 '13 at 21:10
  • `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. – josiah Aug 04 '15 at 18:27
  • 1
    @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. – tadman Aug 05 '15 at 16:49
  • @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. – josiah Aug 05 '15 at 17:38
  • 3
    @tadman fyi through this SO answer, #dig is now in Ruby trunk: https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/ :D – Gabe Kopley Nov 12 '15 at 18:38
  • @GabeKopley Wow, that's great news. I've found this function to be really helpful and I'm glad it's going mainstream now. – tadman Nov 12 '15 at 21:30
25

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)
Mario Pérez Alarcón
  • 3,468
  • 2
  • 27
  • 38
  • 6
    If you are using Ruby < 2.3, I just published a gem that adds the 2.3-compatible Hash#dig and Array#dig methods: https://rubygems.org/gems/ruby_dig – Colin Kelley Nov 29 '15 at 23:58
13

Another alternative:

dino.fetch(:person, {})[:children]
Cameron Martin
  • 5,952
  • 2
  • 40
  • 53
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/.

paradigmatic
  • 40,153
  • 18
  • 88
  • 147
2

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
kirushik
  • 1,296
  • 1
  • 10
  • 20
2

Traditionally, you really had to do something like this:

structure[:a] && structure[:a][:b]

However, Ruby 2.3 added a feature that makes this way more graceful:

structure.dig :a, :b # nil if it misses anywhere along the way

There is a gem called ruby_dig that will back-patch this for you.

DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
2
def flatten_hash(hash)
  hash.each_with_object({}) do |(k, v), h|
    if v.is_a? Hash
      flatten_hash(v).map do |h_k, h_v|
        h["#{k}_#{h_k}"] = h_v
      end
    else
      h[k] = v
    end
  end
end

irb(main):012:0> fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
=> {:person=>{:name=>"Fred", :spouse=>"Wilma", :children=>{:child=>{:name=>"Pebbles"}}}}

irb(main):013:0> slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}

irb(main):014:0> flatten_hash(fred).keys.any? { |k| k.include?("children") }
=> true

irb(main):015:0> flatten_hash(slate).keys.any? { |k| k.include?("children") }
=> false

This will flatten all the hashes into one and then any? returns true if any key matching the substring "children" exist. This might also help.

bharath
  • 481
  • 4
  • 10
1
dino_has_children = !dino.fetch(person, {})[:children].nil?

Note that in rails you can also do:

dino_has_children = !dino[person].try(:[], :children).nil?   # 
Marc-André Lafortune
  • 78,216
  • 16
  • 166
  • 166
1

Here is a way you can do a deep check for any falsy values in the hash and any nested hashes without monkey patching the Ruby Hash class (PLEASE don't monkey patch on the Ruby classes, such is something you should not do, EVER).

(Assuming Rails, although you could easily modify this to work outside of Rails)

def deep_all_present?(hash)
  fail ArgumentError, 'deep_all_present? only accepts Hashes' unless hash.is_a? Hash

  hash.each do |key, value|
    return false if key.blank? || value.blank?
    return deep_all_present?(value) if value.is_a? Hash
  end

  true
end
josiah
  • 1,314
  • 1
  • 13
  • 33
1

Simplifying the above answers here:

Create a Recursive Hash method whose value cannot be nil, like as follows.

def recursive_hash
  Hash.new {|key, value| key[value] = recursive_hash}
end

> slate = recursive_hash 
> slate[:person][:name] = "Mr. Slate"
> slate[:person][:spouse] = "Mrs. Slate"

> slate
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}
slate[:person][:state][:city]
=> {}

If you don't mind creating empty hashes if the value does not exists for the key :)

Abhi
  • 3,361
  • 2
  • 33
  • 38
0

Given

x = {:a => {:b => 'c'}}
y = {}

you could check x and y like this:

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
wedesoft
  • 2,781
  • 28
  • 25
0

You can try to play with

dino.default = {}

Or for example:

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

That way you can call

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
MBO
  • 30,379
  • 5
  • 50
  • 52
0

Thks @tadman for the answer.

For those who want perfs (and are stuck with ruby < 2.3), this method is 2.5x faster:

unless Hash.method_defined? :dig
  class Hash
    def dig(*path)
      val, index, len = self, 0, path.length
      index += 1 while(index < len && val = val[path[index]])
      val
    end
  end
end

and if you use RubyInline, this method is 16x faster:

unless Hash.method_defined? :dig
  require 'inline'

  class Hash
    inline do |builder|
      builder.c_raw '
      VALUE dig(int argc, VALUE *argv, VALUE self) {
        rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
        self = rb_hash_aref(self, *argv);
        if (NIL_P(self) || !--argc) return self;
        ++argv;
        return dig(argc, argv, self);
      }'
    end
  end
end
gtournie
  • 4,143
  • 1
  • 21
  • 22
0

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"}}
Matias
  • 575
  • 5
  • 17
0

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.

Convincible
  • 314
  • 2
  • 10
0

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)
aabiro
  • 3,842
  • 2
  • 23
  • 36