Wie extrahiere ich einen Sub-Hash aus einem Hash?

95

Ich habe einen Hash:

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

Was ist der beste Weg, um einen solchen Sub-Hash zu extrahieren?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
sawa
quelle
4
Randnotiz: apidock.com/rails/Hash/slice%21
tokland
mögliches Duplikat von Ruby Hash Filter
John Dvorak
1
@JanDvorak Bei dieser Frage geht es nicht nur um die Rückgabe von Subhash, sondern auch um die Änderung eines vorhandenen. Sehr ähnliche Dinge, aber ActiveSupport hat verschiedene Mittel, um damit umzugehen.
Skalee

Antworten:

58

Wenn Sie speziell möchten, dass die Methode die extrahierten Elemente zurückgibt, h1 jedoch gleich bleibt:

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} 

Und wenn Sie das in die Hash-Klasse patchen möchten:

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

Wenn Sie nur die angegebenen Elemente aus dem Hash entfernen möchten, ist dies mit delete_if viel einfacher .

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
quelle
2
Dies ist O (n2) - Sie haben eine Schleife auf der Auswahl, eine andere Schleife auf dem Include, die h1.size times genannt wird.
Metakung Fu
1
Während diese Antwort für reinen Rubin anständig ist, ist die folgende Antwort (mit eingebauten sliceoder except, abhängig von Ihren Bedürfnissen) viel sauberer
Krease
137

ActiveSupportZumindest seit 2.3.8 stellt vier praktische Möglichkeiten: #slice, #exceptsowie deren Pendants destruktive: #slice!und #except!. Sie wurden in anderen Antworten erwähnt, aber um sie an einer Stelle zusammenzufassen:

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}

Beachten Sie die Rückgabewerte der Bang-Methoden. Sie passen nicht nur vorhandenen Hash an, sondern geben auch entfernte (nicht beibehaltene) Einträge zurück. Das Hash#except!passt am besten zum Beispiel in der Frage:

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}

ActiveSupportbenötigt keine ganzen Schienen, ist ziemlich leicht. Tatsächlich hängen viele Edelsteine, die keine Schienen sind, davon ab, sodass Sie sie höchstwahrscheinlich bereits in Gemfile.lock haben. Sie müssen die Hash-Klasse nicht selbst erweitern.

Skalee
quelle
3
Das Ergebnis von x.except!(:c, :d)(mit Knall) sollte sein # => {:a=>1, :b=>2}. Gut, wenn Sie Ihre Antwort bearbeiten können.
244an
28

Wenn Sie Schienen verwenden , ist Hash # Slice der richtige Weg.

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

Wenn Sie keine Schienen verwenden , gibt Hash # values_at die Werte in derselben Reihenfolge zurück, in der Sie sie gefragt haben, damit Sie Folgendes tun können:

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

Erläuterung:

Raus {:a => 1, :b => 2, :c => 3}wollen wir{: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}

Wenn Sie der Meinung sind, dass das Patchen von Affen der richtige Weg ist, möchten Sie Folgendes:

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
Metakung Fu
quelle
2
Mokey Patching ist definitiv der richtige Weg für IMO. Viel sauberer und macht die Absicht klarer.
Romário
1
Hinzufügen, um den Code zu ändern, um das Kernmodul korrekt zu adressieren, das Modul zu definieren und das erweiterte Hash-Kern-Modul zu importieren Ende Hash.include CoreExtensions :: Hash
Ronan Fauglas
27

Ruby 2.5 hat Hash # Slice hinzugefügt :

h = { a: 100, b: 200, c: 300 }
h.slice(:a)           #=> {:a=>100}
h.slice(:b, :c, :d)   #=> {:b=>200, :c=>300}
Dhulihan
quelle
5

Sie können Slice! (* -Tasten) verwenden, die in den Kernerweiterungen von ActiveSupport verfügbar sind

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

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

initial_hash wäre jetzt

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

extract_slide wäre jetzt

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

Sie können anschauen slice.rb in ActiveSupport 3.1.3

Vijay
quelle
Ich denke, Sie beschreiben Auszug!. Extrakt! Entfernt die Schlüssel aus dem anfänglichen Hash und gibt einen neuen Hash zurück, der die entfernten Schlüssel enthält. Scheibe! macht das Gegenteil: Entfernen Sie alle bis auf die angegebenen Schlüssel aus dem anfänglichen Hash (und geben Sie erneut einen neuen Hash zurück, der die entfernten Schlüssel enthält). Also in Scheiben schneiden! ist ein bisschen mehr wie eine "Beibehaltungs" -Operation.
Russ Egan
1
ActiveSupport ist nicht Teil des 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
quelle
1
Gut gemacht. Nicht ganz das, wonach er fragt. Ihre Methode gibt Folgendes zurück: {: d =>: D ,: b =>: B ,: e => nil ,: f => nil} {: c =>: C ,: a =>: A ,: d => : D ,: b =>: B}
Andy
Eine äquivalente einzeilige (und möglicherweise schnellere) Lösung: <pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
Peak
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
quelle
2

Wenn Sie Schienen verwenden, kann es zweckmäßig sein, Hash.except zu verwenden

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

Hier ist ein schneller Leistungsvergleich der vorgeschlagenen Methoden, #selectscheint der schnellste zu sein

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)

Die Verfeinerung wird folgendermaßen aussehen:

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

Und um es zu benutzen:

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

Beide delete_ifund keep_ifsind Teil des Ruby-Kerns. Hier können Sie erreichen, was Sie möchten, ohne den HashTyp zu patchen .

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}

Weitere Informationen finden Sie unter den folgenden Links in der Dokumentation:

Kennzeichen
quelle
1

Wie bereits erwähnt, hat Ruby 2.5 die Hash # -Slice-Methode hinzugefügt.

Rails 5.2.0beta1 hat auch eine eigene Version von Hash # Slice hinzugefügt, um die Funktionalität für Benutzer des Frameworks zu verbessern, die eine frühere Version von Ruby verwenden. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

Wenn Sie aus irgendeinem Grund Ihre eigenen implementieren möchten, ist dies auch ein guter Einzeiler:

 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
quelle
0

Dieser Code fügt die von Ihnen angeforderte Funktionalität in die Hash-Klasse ein:

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

und erzeugt die von Ihnen angegebenen Ergebnisse:

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}

Hinweis: Diese Methode gibt tatsächlich die extrahierten Schlüssel / Werte zurück.

Andy
quelle
0

Hier ist eine funktionale Lösung, die nützlich sein kann, wenn Sie nicht mit Ruby 2.5 arbeiten und Ihre Hash-Klasse nicht durch Hinzufügen einer neuen Methode verschmutzen möchten:

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

Dann können Sie es auch auf verschachtelte Hashes anwenden:

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

Nur eine Ergänzung zur Slice-Methode: Wenn die Subhash-Schlüssel, die Sie vom ursprünglichen Hash trennen möchten, dynamisch sind, können Sie Folgendes tun:

slice(*dynamic_keys) # dynamic_keys should be an array type 
YasirAzgar
quelle
0

Wir können dies tun, indem wir nur die Schlüssel schleifen, die wir extrahieren möchten, und nur überprüfen, ob der Schlüssel vorhanden ist, und ihn dann extrahieren.

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
quelle