Styl Ruby: jak sprawdzić, czy zagnieżdżony element hash istnieje

Rozważmy "osobę" przechowywaną w hashu. Dwa przykłady to:

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

Jeśli "osoba" nie ma dzieci, element "dzieci" nie jest obecny. Więc dla Pana Slate ' a możemy sprawdzić, czy ma rodziców:

slate_has_children = !slate[:person][:children].nil?
Co jeśli nie wiemy, że "slate" to skrót od "person"? Consider:
dino = {:pet => {:name => "Dino"}}

Nie możemy już łatwo sprawdzić dzieci:

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

Więc jak sprawdzić strukturę hasha, zwłaszcza jeśli jest zagnieżdżony głęboko (nawet głębiej niż przykłady podane tutaj)? Może lepszym pytaniem jest: jaki jest "rubinowy sposób", aby to zrobić?

Author: Gaurav Sharma, 2009-11-30

14 answers

Najbardziej oczywistym sposobem, aby to zrobić, jest po prostu sprawdzić każdy krok drogi:

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

Użycie .zero? jest wymagane tylko wtedy, gdy używasz false jako wartości zastępczej, a w praktyce jest to rzadkie. Ogólnie można po prostu sprawdzić, że istnieje.

Update : Jeśli używasz Rubiego 2.3 lub nowszego, jest wbudowany dig metoda, która robi to, co jest opisane w tej odpowiedzi.

Jeśli nie, możesz również zdefiniować własną metodę Hash "dig", która może uprość to znacznie:

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

Ta metoda będzie sprawdzać każdy krok drogi i uniknąć potknięcia się na połączenia do zera. Dla płytkich struktur użyteczność jest nieco ograniczona, ale dla głęboko zagnieżdżonych struktur uważam, że jest nieoceniona: {]}

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

Możesz również uczynić to bardziej solidnym, na przykład, testując, czy wpis: children jest rzeczywiście wypełniony:

children = slate.dig(:person, :children)
has_children = children && !children.empty?
 74
Author: tadman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-24 19:31:25

W Ruby 2.3 będziemy mieli wsparcie dla operatora bezpiecznej nawigacji: https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/

has_children teraz Można zapisać jako:

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

dig dodaje się również:

has_children = slate.dig(:person, :children)
 20
Author: Mario Pérez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-13 10:59:01

Inna alternatywa:

dino.fetch(:person, {})[:children]
 13
Author: Cameron Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-13 21:23:38

Możesz użyć andand gem:

require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true
Dalsze wyjaśnienia można znaleźć na stronie http://andand.rubyforge.org/.
 4
Author: paradigmatic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-30 16:47:14

Można użyć hash z domyślną wartością {} - empty hash. Na przykład,

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

To działa również z już utworzonym Hashem:

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

Lub można zdefiniować metodę [] dla klasy nil

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

nil[:a] #=> nil
 2
Author: kirushik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-30 15:33:42

Tradycyjnie, naprawdę trzeba było zrobić coś takiego:

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

Jednak Ruby 2.3 dodał funkcję, która czyni ten sposób bardziej wdzięcznym:

structure.dig :a, :b # nil if it misses anywhere along the way
Jest klejnot o nazwie ruby_dig, który naprawi to za Ciebie.
 2
Author: DigitalRoss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-06 20:56:37
dino_has_children = !dino.fetch(person, {})[:children].nil?

Zauważ, że w rails możesz również zrobić:

dino_has_children = !dino[person].try(:[], :children).nil?   # 
 1
Author: Marc-André Lafortune,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-30 15:59:17

Oto sposób, w jaki możesz zrobić głębokie sprawdzenie wszelkich fałszywych wartości w hash i zagnieżdżonych hashach bez Małpiego łatania klasy Ruby Hash (proszę nie małpuj łatania klas Ruby, czegoś takiego nigdy nie powinieneś robić).

(zakładając Rails, chociaż można łatwo zmodyfikować to tak, aby działało poza 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
 1
Author: josiah,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-04 18:45:58
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
To spłaszczy wszystkie hasze w jeden, a potem każdy? zwraca true, jeśli istnieje jakikolwiek klucz pasujący do podłańcucha "dzieci". To również może pomóc.
 1
Author: bharath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-15 21:22:14

Upraszczając powyższe odpowiedzi tutaj:

Utwórz rekurencyjną metodę Hash, której wartość nie może być zerowa, jak poniżej.

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

Jeśli nie masz nic przeciwko stworzeniu pustych hashów, jeśli wartość nie istnieje dla klucza:)

 1
Author: Abhi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-03 14:47:45

Możesz spróbować grać z

dino.default = {}

Lub na przykład:

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

W ten sposób możesz zadzwonić

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
 0
Author: MBO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-30 15:47:45

Given

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

Możesz sprawdzić x i y TAK:

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
 0
Author: wedesoft,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-15 11:01:50

Thks @ tadman za odpowiedź.

Dla tych, którzy chcą perfs (i utknęli z Rubim

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

A jeśli używasz RubyInline , ta metoda jest 16X szybsza:

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
 0
Author: gtournie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-10 23:42:42

Możesz również zdefiniować moduł do aliasów metod nawiasów i użyć składni Ruby do odczytu/zapisu zagnieżdżonych elementów.

UPDATE: zamiast nadpisywać Accesory wsporników, Zażądaj wystąpienia skrótu, aby rozszerzyć moduł.

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

Wtedy możesz zrobić:

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"}}
 0
Author: Juan Matias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-29 08:29:10