Comment extraire un sous-hachage d'un hachage?

96

J'ai un hash:

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

Quelle est la meilleure façon d'extraire un sous-hachage comme celui-ci?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}
Sawa
la source
4
note latérale: apidock.com/rails/Hash/slice%21
tokland
duplication possible de Ruby Hash Filter
John Dvorak
1
@JanDvorak Cette question ne concerne pas seulement le retour de subhash, mais aussi la modification de celui existant. Des choses très similaires, mais ActiveSupport a différents moyens pour les gérer.
skalee

Réponses:

59

Si vous souhaitez spécifiquement que la méthode renvoie les éléments extraits mais que h1 reste le même:

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} 

Et si vous voulez patcher cela dans la 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

Si vous souhaitez simplement supprimer les éléments spécifiés du hachage, c'est beaucoup plus facile en utilisant 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
la source
2
Ceci est O (n2) - vous aurez une boucle sur le select, une autre boucle sur l'inclusion qui sera appelée h1.size times.
metakungfu
1
Bien que cette réponse soit correcte pour le rubis pur, si vous utilisez des rails, la réponse ci-dessous (en utilisant intégré sliceou except, selon vos besoins) est beaucoup plus propre
Krease
137

ActiveSupport, Au moins depuis 2.3.8, fournit quatre méthodes pratiques: #slice, #exceptet leurs homologues destructeurs: #slice!et #except!. Ils ont été mentionnés dans d'autres réponses, mais pour les résumer en un seul endroit:

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}

Notez les valeurs de retour des méthodes bang. Ils vont non seulement personnaliser le hachage existant, mais également renvoyer les entrées supprimées (non conservées). Le Hash#except!mieux correspond à l'exemple donné dans la question:

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}

ActiveSupportne nécessite pas de rails entiers, est assez léger. En fait, beaucoup de gemmes non-rails en dépendent, donc vous l'avez probablement déjà dans Gemfile.lock. Pas besoin d'étendre la classe Hash par vous-même.

skalee
la source
3
Le résultat de x.except!(:c, :d)(avec bang) devrait être # => {:a=>1, :b=>2}. Tant mieux si vous pouvez modifier votre réponse.
244 du
28

Si vous utilisez des rails , Hash # slice est la voie à suivre.

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

Si vous n'utilisez pas de rails , Hash # values_at renverra les valeurs dans le même ordre que vous leur avez demandé afin que vous puissiez faire ceci:

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

Explication:

Hors de {:a => 1, :b => 2, :c => 3}nous voulons{: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 vous sentez que le patching de singe est la voie à suivre, voici ce que vous voulez:

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
la source
2
Les correctifs Mokey sont certainement la voie à suivre pour l'OMI. Beaucoup plus propre et rend l'intention plus claire.
Romário
1
Ajouter pour modifier le code pour adresser correctement le module principal, définir le module et importer étendre 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
27

Ruby 2.5 a ajouté Hash # slice :

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

Vous pouvez utiliser slice! (* Keys) qui est disponible dans les extensions principales d'ActiveSupport

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

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

initial_hash serait maintenant

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

extrait_slide serait maintenant

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

Vous pouvez regarder slice.rb in ActiveSupport 3.1.3

Vijay
la source
Je pense que vous décrivez l'extrait !. extrait! supprime les clés du hachage initial, renvoyant un nouveau hachage contenant les clés supprimées. tranche! fait le contraire: supprimer toutes les clés sauf les clés spécifiées du hachage initial (encore une fois, renvoyer un nouveau hachage contenant les clés supprimées). Alors tranche! est un peu plus comme une opération "retenir".
Russ Egan
1
ActiveSupport ne fait pas partie du 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
la source
1
Bon travail. Pas tout à fait ce qu'il demande. Votre méthode renvoie: {: d =>: D,: b =>: B,: e => nil,: f => nil} {: c =>: C,: a =>: A,: d => : D
Andy
Une solution équivalente sur une ligne (et peut-être plus rapide): <pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
pic
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
la source
2

si vous utilisez des rails, il peut être pratique d'utiliser Hash.except

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

Voici une comparaison rapide des performances des méthodes suggérées, #selectsemble être la plus rapide

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)

Le raffinement ressemblera à ceci:

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

Et pour l'utiliser:

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

Les deux delete_ifet keep_iffont partie du noyau Ruby. Ici, vous pouvez réaliser ce que vous souhaitez sans patcher le Hashtype.

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}

Pour plus d'informations, consultez les liens ci-dessous dans la documentation:

marque
la source
1

Comme d'autres l'ont mentionné, Ruby 2.5 a ajouté la méthode Hash # slice.

Rails 5.2.0beta1 a également ajouté sa propre version de Hash # slice pour modifier la fonctionnalité pour les utilisateurs du framework qui utilisent une version antérieure de Ruby. https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

Si vous cherchez à mettre en œuvre le vôtre pour une raison quelconque, c'est également une belle ligne:

 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
la source
0

Ce code injecte la fonctionnalité que vous demandez dans la 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

et produit les résultats que vous avez fournis:

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}

Remarque: cette méthode retourne en fait les clés / valeurs extraites.

Andy
la source
0

Voici une solution fonctionnelle qui peut être utile si vous n'utilisez pas Ruby 2.5 et dans le cas où vous ne voudriez pas polluer votre classe Hash en ajoutant une nouvelle méthode:

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

Ensuite, vous pouvez l'appliquer même sur des hachages imbriqués:

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

Juste un ajout à la méthode slice, si les clés de sous-hachage que vous souhaitez séparer du hachage d'origine vont être dynamiques, vous pouvez faire comme,

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

Nous pouvons le faire en boucle sur les clés que nous voulons extraire et en vérifiant simplement que la clé existe, puis en l'extrayant.

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
la source