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}
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}
Réponses:
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}
slice
ou except
, selon vos besoins) est beaucoup plus propre
ActiveSupport
, Au moins depuis 2.3.8, fournit quatre méthodes pratiques: #slice
, #except
et 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}
ActiveSupport
ne 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.
x.except!(:c, :d)
(avec bang) devrait être # => {:a=>1, :b=>2}
. Tant mieux si vous pouvez modifier votre réponse.
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
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}
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
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}
def subhash(*keys) select {|k,v| keys.include?(k)} end
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}
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)
Voici une comparaison rapide des performances des méthodes suggérées, #select
semble ê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)
Les deux delete_if
et keep_if
font partie du noyau Ruby. Ici, vous pouvez réaliser ce que vous souhaitez sans patcher le Hash
type.
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:
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)
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.
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"}]
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
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)