¿Cómo extraigo un sub-hash de un hash?

95

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}
sawa
fuente
4
nota al margen: apidock.com/rails/Hash/slice%21
tokland
posible duplicado de Ruby Hash Filter
John Dvorak
1
@JanDvorak Esta pregunta no solo se trata de devolver subhash sino también de modificar uno existente. Cosas muy similares, pero ActiveSupport tiene diferentes medios para lidiar con ellas.
skalee

Respuestas:

58

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} 
Gazler
fuente
2
Esto es O (n2): tendrá un bucle en la selección, otro bucle en la inclusión que se llamará h1.size times.
metakungfu
1
Si bien esta respuesta es decente para rubí puro, si está usando rieles, la respuesta a continuación (usando una función integrada sliceo except, según sus necesidades) es mucho más limpia
Krease
137

ActiveSupport, Al menos desde 2.3.8, proporciona cuatro métodos convenientes: #slice, #excepty 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}

ActiveSupportno 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.

skalee
fuente
3
El resultado de x.except!(:c, :d)(con bang) debería ser # => {:a=>1, :b=>2}. Bien si puede editar su respuesta.
244an
28

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
metakungfu
fuente
2
El parche de Mokey es definitivamente el camino a seguir, en mi opinión. Mucho más limpio y aclara la intención.
Romário
1
Agregar para modificar el código para abordar correctamente el módulo central, definir el módulo e importar extender Hash core ... module CoreExtensions module Hash def slice (* keys) :: Hash [[keys, self.values_at (* keys)]. Transpose] end end end Hash.include CoreExtensions :: Hash
Ronan Fauglas
5

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

Vijay
fuente
¡Creo que estás describiendo extracto !. ¡extraer! elimina las claves del hash inicial, devolviendo un nuevo hash que contiene las claves eliminadas. ¡rebanada! hace lo contrario: elimina todas las claves excepto las especificadas del hash inicial (nuevamente, devuelve un nuevo hash que contiene las claves eliminadas). ¡Así que corta! es un poco más como una operación de "retención".
Russ Egan
1
ActiveSupport no es parte de Ruby STI
Volte
4
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}
Ryan LeCompte
fuente
1
Buen trabajo. No es exactamente lo que pide. Su método devuelve: {: d =>: D,: b =>: B,: e => nil,: f => nil} {: c =>: C,: a =>: A,: d => : D,: b =>: B}
Andy
Una solución equivalente de una línea (y quizás más rápida): <pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
pico
3
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}
Cary Swoveland
fuente
2

si usa rieles, puede ser conveniente usar Hash, excepto

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}
gayavat
fuente
1
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)
Víctor Moroz
fuente
1

Aquí hay una comparación de rendimiento rápida de los métodos sugeridos, #selectparece 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)
Vadym Tyemirov
fuente
1

Ambos delete_ify keep_ifson parte del núcleo de Ruby. Aquí puede lograr lo que le gustaría sin parchear el Hashtipo.

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:

marca
fuente
1

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)
Josh
fuente
0

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.

Andy
fuente
0

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"}]
Martinos
fuente
0

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 
YasirAzgar
fuente
0

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)
Praveen
fuente