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}
sawa
fonte
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} 
Gazler
fonte
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.

Skalee
fonte
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
metakungfu
fonte
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
27

Ruby 2.5 adicionado Hash # slice :

h = { a: 100, b: 200, c: 300 }
h.slice(:a)           #=> {:a=>100}
h.slice(:b, :c, :d)   #=> {:b=>200, :c=>300}
dhulihan
fonte
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

Vijay
fonte
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}
Ryan LeCompte
fonte
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}
Cary Swoveland
fonte
2

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

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}
gayavat
fonte
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)
Victor Moroz
fonte
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)
Vadym Tyemirov
fonte
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:

Marca
fonte
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)
Josh
fonte
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.

Andy
fonte
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"}]
Martinos
fonte
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 
YasirAzgar
fonte
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)
Praveen
fonte