web-dev-qa-db-fra.com

Ruby Style: Comment vérifier si un élément de hachage imbriqué existe

Considérons une "personne" stockée dans un hachage. Deux exemples sont:

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

Si la "personne" n'a pas d'enfants, l'élément "enfants" n'est pas présent. Donc, pour M. Slate, on peut vérifier s’il a des parents:

slate_has_children = !slate[:person][:children].nil?

Alors, que se passe-t-il si nous ne savons pas que "l'ardoise" est un hachage "de personne"? Considérer:

dino = {:pet => {:name => "Dino"}}

Nous ne pouvons plus facilement vérifier s'il y a des enfants:

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

Alors, comment vérifier la structure d'un hachage, surtout s'il est imbriqué profondément (même plus profond que les exemples fournis ici)? Peut-être une meilleure question est: quelle est la "manière Ruby" de faire cela?

53
Todd R

La manière la plus évidente de le faire est simplement de vérifier chaque étape:

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

Utilisation de .nil? n’est vraiment nécessaire que lorsque vous utilisez false comme valeur d’espace réservé, ce qui est rare en pratique. Généralement, vous pouvez simplement tester son existence.

Update: Si vous utilisez Ruby 2.3 ou une version ultérieure, une méthode Dig intégrée fait ce qui est décrit dans cette réponse.

Sinon, vous pouvez également définir votre propre méthode de «hachage» de hachage, ce qui peut considérablement simplifier cette opération:

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

Cette méthode permet de vérifier chaque étape du processus et d’éviter de déclencher un appel nul. Pour les structures peu profondes, l'utilité est quelque peu limitée, mais pour les structures profondément imbriquées, je la trouve inestimable:

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

Vous pouvez également rendre cela plus robuste, par exemple, en testant si l'entrée: children est réellement remplie:

children = slate.Dig(:person, :children)
has_children = children && !children.empty?
76
tadman

Avec Ruby 2.3, l’opérateur de navigation sécurisée sera pris en charge: https://www.Ruby-lang.org/fr/news/2015/11/11/Ruby-2-3-0-preview1 -released/

has_children pourrait maintenant être écrit comme suit: 

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

Dig est également ajouté:

has_children = slate.Dig(:person, :children)
20
Mario Pérez

Une autre alternative:

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

Vous pouvez utiliser la gemme andand:

require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true

Vous pouvez trouver plus d'explications sur http://andand.rubyforge.org/ .

4
paradigmatic

Traditionnellement, il fallait vraiment faire quelque chose comme ça:

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

Cependant, Ruby 2.3 a ajouté une fonctionnalité qui le rend plus gracieux:

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

Il existe une gemme appelée Ruby_Dig qui corrigera cela pour vous.

2
DigitalRoss

On pourrait utiliser un hash avec la valeur par défaut {} - hash vide. Par exemple,

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

Cela fonctionne aussi avec Hash déjà créé:

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

Ou vous pouvez définir la méthode [] pour la classe nil

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

nil[:a] #=> nil
2
kirushik
dino_has_children = !dino.fetch(person, {})[:children].nil?

Notez que dans Rails, vous pouvez également faire:

dino_has_children = !dino[person].try(:[], :children).nil?   # 
1
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

Cela va aplatir tous les hachages en un seul et ensuite? renvoie true si une clé correspondant à la sous-chaîne "enfants" existe ..__ Cela pourrait également aider.

1
bharath

Voici un moyen de vérifier en profondeur toutes les valeurs de fausseté dans le hachage et les hachages imbriqués sans que singe ne corrige la classe Ruby Hash (VEUILLEZ ne pas appliquer de correctif pour singe sur les classes Ruby; .

(En supposant que Rails, vous pouvez facilement le modifier pour qu’il fonctionne en dehors de 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
josiah

Simplifier les réponses ci-dessus ici: 

Créez une méthode de hachage récursif dont la valeur ne peut pas être nulle, comme suit.

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

Si cela ne vous dérange pas de créer des hachages vides si la valeur n'existe pas pour la clé :)

1
Abhi

Thks @tadman pour la réponse.

Pour ceux qui veulent des perfs (et sont bloqués avec Ruby <2.3), cette méthode est 2,5 fois plus rapide

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

et si vous utilisez RubyInline , cette méthode est 16 fois plus rapide:

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
gtournie

Vous pouvez utiliser une combinaison de & et key? c'est O(1) par rapport à Dig qui est O(n) et ceci s'assurera que la personne est accédée sans NoMethodError: undefined method `[]' for nil:NilClass

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

Vous pouvez également définir un module pour aliaser les méthodes entre crochets et utiliser la syntaxe Ruby pour lire/écrire des éléments imbriqués. 

UPDATE: Au lieu de remplacer les accesseurs de support, demandez à l'instance Hash d'étendre le 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

Ensuite, vous pouvez faire:

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
Juan Matias

Je ne sais pas comment "Ruby" c'est (!), Mais le KeyDial gem que j'ai écrit vous permet de le faire sans changer la syntaxe d'origine:

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

devient:

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

Cela utilise quelques astuces pour intermédiaires les appels d'accès clé. À call, il tentera de Dig les clés précédentes sur dino et, si elle rencontre une erreur (comme il le fera), renvoie nil. nil? retourne bien sûr true.

0
Convincible

Vous pouvez essayer de jouer avec

dino.default = {}

Ou par exemple:

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

De cette façon, vous pouvez appeler

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
0
MBO

Donné

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

vous pouvez vérifier x et y comme ceci:

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