rails - Ruby Style: cómo verificar si existe un elemento hash anidado
ruby rubocop (14)
Aquí hay una manera de hacer un chequeo profundo de cualquier valor falso en el hash y cualquier hash anidado sin parche de monos en la clase Ruby Hash (POR FAVOR, no pongas parches en las clases de Ruby, es algo que no debes hacer, NUNCA) .
(Asumiendo Rails, aunque podrías modificar esto fácilmente para trabajar fuera 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
Considere una "persona" almacenada en un hash. Dos ejemplos son:
fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
Si la "persona" no tiene hijos, el elemento "secundarios" no está presente. Entonces, para el Sr. Slate, podemos verificar si tiene padres:
slate_has_children = !slate[:person][:children].nil?
Entonces, ¿qué pasa si no sabemos que "pizarra" es una "persona" hash? Considerar:
dino = {:pet => {:name => "Dino"}}
No podemos verificar fácilmente para los niños más:
dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]'' for nil:NilClass
Entonces, ¿cómo verificaría la estructura de un hash, especialmente si está anidado profundamente (incluso más profundo que los ejemplos proporcionados aquí)? Tal vez una mejor pregunta es: ¿cuál es la "forma de Ruby" para hacer esto?
Con Ruby 2.3, tendremos soporte para el operador de navegación segura: ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released
has_children
ahora podría escribirse como:
has_children = slate[:person]&.[](:children)
dig
se está agregando también:
has_children = slate.dig(:person, :children)
Dado
x = {:a => {:b => ''c''}}
y = {}
puedes verificar xey de esta manera:
(x[:a] || {})[:b] # ''c''
(y[:a] || {})[:b] # nil
La forma más obvia de hacerlo es simplemente verificar cada paso del camino:
has_children = slate[:person] && slate[:person][:children]
Uso de .nil? en realidad solo se requiere cuando usa falso como valor de marcador de posición, y en la práctica esto es raro. En general, puedes simplemente probar que existe.
Actualización : si está usando Ruby 2.3 o posterior, hay un método de
dig
integrado que hace lo que se describe en esta respuesta.
De lo contrario, también puede definir su propio método Hash "dig" que puede simplificar esto de manera sustancial:
class Hash
def dig(*path)
path.inject(self) do |location, key|
location.respond_to?(:keys) ? location[key] : nil
end
end
end
Este método comprobará cada paso del camino y evitará tropezar con llamadas a cero. Para las estructuras poco profundas, la utilidad es algo limitada, pero para las estructuras profundamente anidadas, me parece invaluable:
has_children = slate.dig(:person, :children)
También puede hacer esto más robusto, por ejemplo, probando si la entrada: children está realmente poblada:
children = slate.dig(:person, :children)
has_children = children && !children.empty?
Otra alternativa:
dino.fetch(:person, {})[:children]
Puedes intentar jugar con
dino.default = {}
O por ejemplo:
empty_hash = {}
empty_hash.default = empty_hash
dino.default = empty_hash
De esa forma puedes llamar
empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
Puedes usar el andand
gem:
require ''andand''
fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true
Puede encontrar más explicaciones en http://andand.rubyforge.org/ .
Simplificando las respuestas anteriores aquí:
Cree un método Recursive Hash cuyo valor no puede ser nil, como el siguiente.
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 no te importa crear hashes vacíos si no existe :)
También puede definir un módulo para aliar los métodos de paréntesis y usar la sintaxis de Ruby para leer / escribir elementos anidados.
ACTUALIZACIÓN: en lugar de anular los accesos al soporte, solicite la instancia de Hash para extender el módulo.
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
Entonces puedes hacer:
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"}}
Thks @tadman por la respuesta.
Para aquellos que quieren perfs (y están atrapados con ruby <2.3), este método es 2.5 veces más rápido:
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
y si usas RubyInline , este método es 16 veces más rápido:
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
Tradicionalmente, realmente tenías que hacer algo como esto:
structure[:a] && structure[:a][:b]
Sin embargo, Ruby 2.3 agregó una característica que hace de esta manera más elegante:
structure.dig :a, :b # nil if it misses anywhere along the way
Hay una gema llamada ruby_dig
que hará un parche de respaldo para usted.
Uno podría usar hash con el valor predeterminado de {} - hash vacío. Por ejemplo,
dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false
Eso también funciona con Hash ya creado:
dino = {:pet=>{:name=>"Dino"}}
dino.default = {}
dino_has_children = !dino[:person][:children].nil? #=> false
O puede definir el método [] para la clase nula
class NilClass
def [](* args)
nil
end
end
nil[:a] #=> nil
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
Esto aplanará todos los hashes en uno y luego en alguno? devuelve verdadero si existe alguna clave que coincida con la subcadena "hijos". Esto también podría ayudar.
dino_has_children = !dino.fetch(person, {})[:children].nil?
Tenga en cuenta que en los rieles también puede hacer:
dino_has_children = !dino[person].try(:[], :children).nil? #