Tengo un hash:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
¿Cuál es la mejor manera de extraer un sub-hash como este?
h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
Tengo un hash:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
¿Cuál es la mejor manera de extraer un sub-hash como este?
h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
Respuestas:
Si desea específicamente que el método devuelva los elementos extraídos pero h1 permanezca igual:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.select {|key, value| [:b, :d, :e, :f].include?(key) } # => {:b=>:B, :d=>:D}
h1 = Hash[h1.to_a - h2.to_a] # => {:a=>:A, :c=>:C}
Y si quieres parchear eso en la clase Hash:
class Hash
def extract_subhash(*extract)
h2 = self.select{|key, value| extract.include?(key) }
self.delete_if {|key, value| extract.include?(key) }
h2
end
end
Si solo desea eliminar los elementos especificados del hash, es mucho más fácil usar delete_if .
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h1.delete_if {|key, value| [:b, :d, :e, :f].include?(key) } # => {:a=>:A, :c=>:C}
h1 # => {:a=>:A, :c=>:C}
slice
o except
, según sus necesidades) es mucho más limpia
ActiveSupport
, Al menos desde 2.3.8, proporciona cuatro métodos convenientes: #slice
, #except
y sus homólogos destructivos: #slice!
y #except!
. Se mencionaron en otras respuestas, pero para resumirlas en un solo lugar:
x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}
x.slice(:a, :b)
# => {:a=>1, :b=>2}
x
# => {:a=>1, :b=>2, :c=>3, :d=>4}
x.except(:a, :b)
# => {:c=>3, :d=>4}
x
# => {:a=>1, :b=>2, :c=>3, :d=>4}
Tenga en cuenta los valores de retorno de los métodos bang. No solo adaptarán el hash existente, sino que también devolverán las entradas eliminadas (no guardadas). Se Hash#except!
adapta mejor al ejemplo que se da en la pregunta:
x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}
x.except!(:c, :d)
# => {:a=>1, :b=>2}
x
# => {:a=>1, :b=>2}
ActiveSupport
no requiere rieles completos, es bastante ligero. De hecho, muchas gemas que no son rieles dependen de él, por lo que probablemente ya lo tenga en Gemfile.lock. No es necesario extender la clase Hash por su cuenta.
x.except!(:c, :d)
(con bang) debería ser # => {:a=>1, :b=>2}
. Bien si puede editar su respuesta.
Si usa rieles , Hash # slice es el camino a seguir.
{:a => :A, :b => :B, :c => :C, :d => :D}.slice(:a, :c)
# => {:a => :A, :c => :C}
Si no usa rieles , Hash # values_at devolverá los valores en el mismo orden en que los solicitó para que pueda hacer esto:
def slice(hash, *keys)
Hash[ [keys, hash.values_at(*keys)].transpose]
end
def except(hash, *keys)
desired_keys = hash.keys - keys
Hash[ [desired_keys, hash.values_at(*desired_keys)].transpose]
end
ex:
slice({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2)
# => {'bar' => 'foo', 2 => 'two'}
except({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2)
# => {:foo => 'bar'}
Explicación:
Fuera de lo {:a => 1, :b => 2, :c => 3}
que queremos{:a => 1, :b => 2}
hash = {:a => 1, :b => 2, :c => 3}
keys = [:a, :b]
values = hash.values_at(*keys) #=> [1, 2]
transposed_matrix =[keys, values].transpose #=> [[:a, 1], [:b, 2]]
Hash[transposed_matrix] #=> {:a => 1, :b => 2}
Si siente que el parche de mono es el camino a seguir, lo siguiente es lo que desea:
module MyExtension
module Hash
def slice(*keys)
::Hash[[keys, self.values_at(*keys)].transpose]
end
def except(*keys)
desired_keys = self.keys - keys
::Hash[[desired_keys, self.values_at(*desired_keys)].transpose]
end
end
end
Hash.include MyExtension::Hash
Ruby 2.5 agregó rebanada de Hash # :
h = { a: 100, b: 200, c: 300 }
h.slice(:a) #=> {:a=>100}
h.slice(:b, :c, :d) #=> {:b=>200, :c=>300}
Puede usar slice! (* Keys) que está disponible en las extensiones principales de ActiveSupport
initial_hash = {:a => 1, :b => 2, :c => 3, :d => 4}
extracted_slice = initial_hash.slice!(:a, :c)
initial_hash ahora sería
{:b => 2, :d =>4}
diapositiva_extraída ahora sería
{:a => 1, :c =>3}
Puedes mirar slice.rb in ActiveSupport 3.1.3
module HashExtensions
def subhash(*keys)
keys = keys.select { |k| key?(k) }
Hash[keys.zip(values_at(*keys))]
end
end
Hash.send(:include, HashExtensions)
{:a => :A, :b => :B, :c => :C, :d => :D}.subhash(:a) # => {:a => :A}
def subhash(*keys) select {|k,v| keys.include?(k)} end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
keys = [:b, :d, :e, :f]
h2 = (h1.keys & keys).each_with_object({}) { |k,h| h.update(k=>h1.delete(k)) }
#=> {:b => :B, :d => :D}
h1
#=> {:a => :A, :c => :C}
class Hash
def extract(*keys)
key_index = Hash[keys.map{ |k| [k, true] }] # depends on the size of keys
partition{ |k, v| key_index.has_key?(k) }.map{ |group| Hash[group] }
end
end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2, h1 = h1.extract(:b, :d, :e, :f)
Aquí hay una comparación de rendimiento rápida de los métodos sugeridos, #select
parece ser el más rápido
k = 1_000_000
Benchmark.bmbm do |x|
x.report('select') { k.times { {a: 1, b: 2, c: 3}.select { |k, _v| [:a, :b].include?(k) } } }
x.report('hash transpose') { k.times { Hash[ [[:a, :b], {a: 1, b: 2, c: 3}.fetch_values(:a, :b)].transpose ] } }
x.report('slice') { k.times { {a: 1, b: 2, c: 3}.slice(:a, :b) } }
end
Rehearsal --------------------------------------------------
select 1.640000 0.010000 1.650000 ( 1.651426)
hash transpose 1.720000 0.010000 1.730000 ( 1.729950)
slice 1.740000 0.010000 1.750000 ( 1.748204)
----------------------------------------- total: 5.130000sec
user system total real
select 1.670000 0.010000 1.680000 ( 1.683415)
hash transpose 1.680000 0.010000 1.690000 ( 1.688110)
slice 1.800000 0.010000 1.810000 ( 1.816215)
El refinamiento se verá así:
module CoreExtensions
module Extractable
refine Hash do
def extract(*keys)
select { |k, _v| keys.include?(k) }
end
end
end
end
Y para usarlo:
using ::CoreExtensions::Extractable
{ a: 1, b: 2, c: 3 }.extract(:a, :b)
Ambos delete_if
y keep_if
son parte del núcleo de Ruby. Aquí puede lograr lo que le gustaría sin parchear el Hash
tipo.
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.clone
p h1.keep_if { |key| [:b, :d, :e, :f].include?(key) } # => {:b => :B, :d => :D}
p h2.delete_if { |key, value| [:b, :d, :e, :f].include?(key) } #=> {:a => :A, :c => :C}
Para obtener más información, consulte los enlaces a continuación de la documentación:
Como han mencionado otros, Ruby 2.5 agregó el método Hash # slice.
Rails 5.2.0beta1 también agregó su propia versión de Hash # slice para ajustar la funcionalidad a los usuarios del framework que están usando una versión anterior de Ruby. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8
Si busca implementar el suyo por alguna razón, también es un buen trazador de líneas:
def slice(*keys)
keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
end unless method_defined?(:slice)
Este código inyecta la funcionalidad que está solicitando en la clase Hash:
class Hash
def extract_subhash! *keys
to_keep = self.keys.to_a - keys
to_delete = Hash[self.select{|k,v| !to_keep.include? k}]
self.delete_if {|k,v| !to_keep.include? k}
to_delete
end
end
y produce los resultados que proporcionó:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
p h1.extract_subhash!(:b, :d, :e, :f) # => {b => :B, :d => :D}
p h1 #=> {:a => :A, :c => :C}
Nota: este método en realidad devuelve las claves / valores extraídos.
Aquí hay una solución funcional que puede ser útil si no está ejecutando Ruby 2.5 y en el caso de que no desee contaminar su clase Hash agregando un nuevo método:
slice_hash = -> keys, hash { hash.select { |k, _v| keys.include?(k) } }.curry
Entonces puedes aplicarlo incluso en hashes anidados:
my_hash = [{name: "Joe", age: 34}, {name: "Amy", age: 55}]
my_hash.map(&slice_hash.([:name]))
# => [{:name=>"Joe"}, {:name=>"Amy"}]
Solo una adición al método de corte, si las claves de subhash que desea separar del hash original van a ser dinámicas, puede hacer lo siguiente,
slice(*dynamic_keys) # dynamic_keys should be an array type
Podemos hacerlo haciendo un bucle en las claves que solo queremos extraer y simplemente verificando que la clave exista y luego extraerla.
class Hash
def extract(*keys)
extracted_hash = {}
keys.each{|key| extracted_hash[key] = self.delete(key) if self.has_key?(key)}
extracted_hash
end
end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.extract(:b, :d, :e, :f)