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

匿名 (未验证) 提交于 2019-12-03 02:11:02

问题:

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?

回答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? 


回答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) 


回答3:

Another alternative:

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


回答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/.



回答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 


回答6:

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.



回答7:

dino_has_children = !dino.fetch(person, {})[:children].nil? 

Note that in rails you can also do:

dino_has_children = !dino[person].try(:[], :children).nil?   #  


回答8:

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 


回答9:

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.



回答10:

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 {} 


回答11:

Given

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

you could check x and y like this:

(x[:a] || {})[:b] # 'c' (y[:a] || {})[:b] # nil 


回答12:

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 does not exists :)



回答13:

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 


回答14:

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"}} 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!