Como faço para extrair um sub-hash de um hash?


95

Eu tenho um hash:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}

Qual é a melhor maneira de extrair um sub-hash como este?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}

4
observação lateral: apidock.com/rails/Hash/slice%21
tokland

possível duplicata do Filtro Ruby Hash
John Dvorak

1
@JanDvorak Esta questão não é apenas sobre retornar o subhash, mas também sobre como modificar um existente. Coisas muito semelhantes, mas o ActiveSupport tem meios diferentes para lidar com elas.
skalee

Respostas:


58

Se você deseja especificamente que o método retorne os elementos extraídos, mas h1 permaneça o mesmo:

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} 

E se você quiser corrigir isso na classe 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

Se você deseja apenas remover os elementos especificados do hash, isso é muito mais fácil usando 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} 

2
Este é O (n2) - você terá um loop no select, outro loop no include que será chamado h1.size times.
metakungfu

1
Embora esta resposta seja decente para rubi puro, se você estiver usando trilhos, a resposta abaixo (usando integrado sliceou except, dependendo de suas necessidades) é muito mais limpa
Krease

137

ActiveSupport, Pelo menos uma vez 2.3.8, fornece quatro métodos convenientes: #slice, #excepte os seus homólogos destrutivas: #slice!e #except!. Eles foram mencionados em outras respostas, mas para resumi-los em um só 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}

Observe os valores de retorno dos métodos bang. Eles não apenas adaptarão o hash existente, mas também retornarão entradas removidas (não mantidas). O que Hash#except!melhor se adequa ao exemplo dado na pergunta:

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}

ActiveSupportnão requer Rails inteiros, é bem leve. Na verdade, muitas joias não-rails dependem dele, então provavelmente você já o tem em Gemfile.lock. Não há necessidade de estender a aula de Hash por conta própria.


3
O resultado de x.except!(:c, :d)(com estrondo) deve ser # => {:a=>1, :b=>2}. Bom se você puder editar sua resposta.
244 em

28

Se você usa trilhos , o Hash # slice é o caminho a seguir.

{:a => :A, :b => :B, :c => :C, :d => :D}.slice(:a, :c)
# =>  {:a => :A, :c => :C}

Se você não usar rails , Hash # values_at retornará os valores na mesma ordem em que você os pediu, para que você possa fazer isso:

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'}

Explicação:

Fora de {:a => 1, :b => 2, :c => 3}nós 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}

Se você acha que o patching do macaco é o caminho a percorrer, seguir o que você deseja:

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

2
O patch Mokey é definitivamente o caminho a seguir na IMO. Muito mais limpo e torna a intenção mais clara.
Romário

1
Adicione para modificar o código para endereçar corretamente o módulo do núcleo, definir o módulo e importar estender o núcleo do Hash ... módulo Módulo CoreExtensions Hash def slice (* keys) :: Hash [[keys, self.values_at (* keys)]. Transpose] end end fim Hash.include CoreExtensions :: Hash
Ronan Fauglas


5

Você pode usar o slice! (* Keys) que está disponível nas extensões principais do ActiveSupport

initial_hash = {:a => 1, :b => 2, :c => 3, :d => 4}

extracted_slice = initial_hash.slice!(:a, :c)

initial_hash agora seria

{:b => 2, :d =>4}

extraído_slide seria agora

{:a => 1, :c =>3}

Você pode olhar para slice.rb in ActiveSupport 3.1.3


Acho que você está descrevendo extrato !. extrair! remove as chaves do hash inicial, retornando um novo hash contendo as chaves removidas. fatia! faz o oposto: remove todas as chaves, exceto as especificadas, do hash inicial (novamente, retornando um novo hash contendo as chaves removidas). Então, corte! é um pouco mais como uma operação de "retenção".
Russ Egan

1
ActiveSupport não faz parte do 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}

1
Bom trabalho. Não é bem o que ele está pedindo. Seu método retorna: {: d =>: D,: b =>: B,: e => nulo,: f => nulo} {: c =>: C,: a =>: A,: d => : D,: b =>: B}
Andy

Uma solução equivalente de uma linha (e talvez mais rápida): <pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
pico de

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}

2

se você usa rails, pode ser conveniente usar Hash.except

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}

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)

1

Aqui está uma rápida comparação de desempenho dos métodos sugeridos, #selectparece ser o mais 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)

O refinamento ficará assim:

module CoreExtensions
  module Extractable
    refine Hash do
      def extract(*keys)
        select { |k, _v| keys.include?(k) }
      end
    end
  end
end

E para usar:

using ::CoreExtensions::Extractable
{ a: 1, b: 2, c: 3 }.extract(:a, :b)

1

Ambos delete_ife keep_iffazem parte do núcleo Ruby. Aqui você pode conseguir o que deseja sem corrigir o 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 obter mais informações, verifique os links abaixo da documentação:


1

Como outros mencionaram, Ruby 2.5 adicionou o método Hash # slice.

O Rails 5.2.0beta1 também adicionou sua própria versão do Hash # slice para reduzir a funcionalidade para usuários do framework que estão usando uma versão anterior do Ruby. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

Se estiver procurando implementar o seu próprio por qualquer motivo, é um bom forro também:

 def slice(*keys)
   keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
 end unless method_defined?(:slice)

0

Este código injeta a funcionalidade que você está pedindo na classe 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

e produz os resultados que você forneceu:

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 realmente retorna as chaves / valores extraídos.


0

Aqui está uma solução funcional que pode ser útil se você não estiver executando o Ruby 2.5 e no caso de não querer poluir sua classe Hash adicionando um novo método:

slice_hash = -> keys, hash { hash.select { |k, _v| keys.include?(k) } }.curry

Em seguida, você pode aplicá-lo até mesmo em hashes aninhados:

my_hash = [{name: "Joe", age: 34}, {name: "Amy", age: 55}]
my_hash.map(&slice_hash.([:name]))
# => [{:name=>"Joe"}, {:name=>"Amy"}]

0

Apenas uma adição ao método de fatia, se as chaves de subhash que você deseja separar do hash original forem dinâmicas, você pode fazer como,

slice(*dynamic_keys) # dynamic_keys should be an array type 

0

Podemos fazer isso fazendo um loop nas chaves que queremos extrair e verificando se a chave existe e depois extraí-la.

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)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.