Come estrarre un hash secondario da un hash?


96

Ho un hash:

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

Qual è il modo migliore per estrarre un sotto-hash come questo?

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

4
nota a margine
tokland

possibile duplicato di Ruby Hash Filter
John Dvorak

1
@JanDvorak Questa domanda non riguarda solo la restituzione di subhash ma anche la modifica di uno esistente. Cose molto simili ma ActiveSupport ha mezzi diversi per gestirle.
skalee

Risposte:


59

Se vuoi specificamente che il metodo restituisca gli elementi estratti ma h1 rimanga lo stesso:

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 vuoi applicarlo alla 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 vuoi solo rimuovere gli elementi specificati dall'hash , è molto più facile usare 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
Questo è O (n2) - avrai un loop sulla selezione, un altro loop sull'inclusione che sarà chiamato h1.size times.
metakungfu

1
Sebbene questa risposta sia decente per il rubino puro, se stai usando i binari, la risposta seguente (usando il built-in sliceo except, a seconda delle tue esigenze) è molto più pulita
Krease

137

ActiveSupport, Almeno dal 2.3.8, prevede quattro metodi utili: #slice, #excepte le loro controparti distruttive: #slice!e #except!. Sono stati menzionati in altre risposte, ma per riassumerli in un unico punto:

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}

Nota i valori di ritorno dei metodi bang. Non solo personalizzeranno l'hash esistente, ma restituiranno anche le voci rimosse (non mantenute). Si Hash#except!adatta meglio all'esempio fornito nella domanda:

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}

ActiveSupportnon richiede Rails interi, è piuttosto leggero. In effetti, molte gemme non rails dipendono da questo, quindi molto probabilmente lo hai già in Gemfile.lock. Non c'è bisogno di estendere la classe Hash da solo.


3
Il risultato di x.except!(:c, :d)(con botto) dovrebbe essere # => {:a=>1, :b=>2}. Bene, se puoi modificare la tua risposta.
244 un

28

Se usi rails , Hash # slice è la strada da percorrere.

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

Se non usi rails , Hash # values_at restituirà i valori nello stesso ordine in cui gli hai chiesto, quindi puoi farlo:

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

Spiegazione:

Fuori {:a => 1, :b => 2, :c => 3}che vogliamo{: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 ritieni che il patching delle scimmie sia la strada da percorrere, di seguito è quello che vuoi:

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
Il patch di Mokey è sicuramente la strada da percorrere IMO. Molto più pulito e rende più chiaro l'intento.
Romário

1
Aggiungi per modificare il codice per indirizzare correttamente il modulo principale, definire il modulo e importare estendere Hash core ... modulo CoreExtensions modulo Hash def slice (* keys) :: Hash [[keys, self.values_at (* keys)]. Transpose] end end end Hash.include CoreExtensions :: Hash
Ronan Fauglas


5

È possibile utilizzare slice! (* Keys) disponibile nelle estensioni principali di ActiveSupport

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

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

initial_hash ora sarebbe

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

extracted_slide ora sarebbe

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

Puoi guardare slice.rb in ActiveSupport 3.1.3


Penso che tu stia descrivendo l'estratto !. estratto! rimuove le chiavi dall'hash iniziale, restituendo un nuovo hash contenente le chiavi rimosse. fetta! fa il contrario: rimuove tutte le chiavi tranne quelle specificate dall'hash iniziale (di nuovo, restituendo un nuovo hash contenente le chiavi rimosse). Quindi affetta! è un po 'più come un'operazione di "conservazione".
Russ Egan,

1
ActiveSupport non fa parte del 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
Bel lavoro. Non proprio quello che sta chiedendo. Il tuo metodo restituisce: {: d =>: D,: b =>: B,: e => nil,: f => nil} {: c =>: C,: a =>: A,: d => : D,: b =>: B}
Andy

Una soluzione equivalente a una riga (e forse più veloce): <pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
picco

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 usi rails, potrebbe essere conveniente usare Hash. eccetto

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

Ecco un rapido confronto delle prestazioni dei metodi suggeriti, #selectsembra essere il più veloce

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)

La raffinatezza sarà simile a questa:

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

E per usarlo:

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

1

Entrambi delete_ife keep_iffanno parte del core di Ruby. Qui puoi ottenere ciò che desideri senza applicare la patch al 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}

Per ulteriori informazioni, controlla i collegamenti sottostanti dalla documentazione:


1

Come altri hanno già detto, Ruby 2.5 ha aggiunto il metodo Hash # slice.

Rails 5.2.0beta1 ha anche aggiunto la propria versione di Hash # slice per shimare la funzionalità per gli utenti del framework che utilizzano una versione precedente di Ruby. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

Se stai cercando di implementare il tuo per qualsiasi motivo, è anche un bel rivestimento:

 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

Questo codice inietta la funzionalità che stai chiedendo nella 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 produce i risultati che hai fornito:

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: questo metodo restituisce effettivamente le chiavi / valori estratti.


0

Ecco una soluzione funzionale che può essere utile se non stai utilizzando Ruby 2.5 e nel caso in cui non vuoi inquinare la tua classe Hash aggiungendo un nuovo metodo:

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

Quindi puoi applicarlo anche su hash annidati:

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

0

Solo un'aggiunta al metodo slice, se le chiavi subhash che vuoi separare dall'hash originale saranno dinamiche puoi fare come,

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

0

Possiamo farlo ripetendo solo le chiavi che vogliamo estrarre e semplicemente controllando che la chiave esista e quindi estrarla.

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.