Changer chaque valeur d'un hachage dans Ruby

170

Je veux changer chaque valeur dans un hachage afin d'ajouter '%' avant et après la valeur afin

{ :a=>'a' , :b=>'b' }

doit être changé en

{ :a=>'%a%' , :b=>'%b%' }

Quelle est la meilleure façon de procéder?

theReverseFlick
la source
1
Veuillez clarifier si vous voulez muter les objets de chaîne d'origine, muter simplement l'original a ou ne rien muter.
Phrogz
1
Alors vous avez accepté la mauvaise réponse. (Aucune infraction à l'intention de @pst, car je préconise personnellement également la programmation de style fonctionnel au lieu de faire muter des objets.)
Phrogz
Mais l'approche était toujours agréable soo
theReverseFlick

Réponses:

178

Si vous voulez que les chaînes réelles elles-mêmes muter sur place (affectant éventuellement et de manière souhaitable d'autres références aux mêmes objets chaîne):

# Two ways to achieve the same result (any Ruby version)
my_hash.each{ |_,str| str.gsub! /^|$/, '%' }
my_hash.each{ |_,str| str.replace "%#{str}%" }

Si vous voulez que le hachage change en place, mais que vous ne voulez pas affecter les chaînes (vous voulez qu'il obtienne de nouvelles chaînes):

# Two ways to achieve the same result (any Ruby version)
my_hash.each{ |key,str| my_hash[key] = "%#{str}%" }
my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }

Si vous voulez un nouveau hachage:

# Ruby 1.8.6+
new_hash = Hash[*my_hash.map{|k,str| [k,"%#{str}%"] }.flatten]

# Ruby 1.8.7+
new_hash = Hash[my_hash.map{|k,str| [k,"%#{str}%"] } ]
Phrogz
la source
1
@Andrew Marshall C'est vrai, merci. Dans Ruby 1.8, Hash.[]n'accepte pas un tableau de paires de tableaux, il nécessite un nombre pair d'arguments directs (d'où le splat à l'avant).
Phrogz
2
En fait, Hash. [Key_value_pairs] a été introduit dans la 1.8.7, donc seul Ruby 1.8.6 n'a pas besoin de splat & aplatir.
Marc-André Lafortune
2
@Aupajo renvoie Hash#eachla clé et la valeur au bloc. Dans ce cas, je me fichais de la clé et je ne lui ai donc rien donné d'utile. Les noms de variables peuvent commencer par un trait de soulignement et peuvent en fait être simplement un trait de soulignement. Il n'y a aucun avantage en termes de performances à faire cela, c'est juste une note subtile d'auto-documentation indiquant que je ne fais rien avec cette première valeur de bloc.
Phrogz
1
Je pense que vous voulez dire my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }, devez retourner le hachage du bloc
aceofspades
1
Vous pouvez également utiliser la méthode each_value, qui est un peu plus facile à comprendre que d'utiliser un trait de soulignement pour la valeur de clé inutilisée.
Strand McCutchen
266

Dans Ruby 2.1 et supérieur, vous pouvez faire

{ a: 'a', b: 'b' }.map { |k, str| [k, "%#{str}%"] }.to_h
shock_one
la source
5
Merci, c'était en fait ce que je cherchais. Je ne sais pas pourquoi vous n'êtes pas si bien voté.
simperreault
7
C'est, cependant, très lent et très gourmand en RAM. Le hachage d'entrée est itéré pour produire un ensemble intermédiaire de tableaux imbriqués qui sont ensuite convertis en un nouveau hachage. En ignorant l'utilisation maximale de la RAM, le temps d'exécution est bien pire - l'analyse comparative de cela par rapport aux solutions de modification sur place dans une autre réponse montre 2,5 s contre 1,5 s sur le même nombre d'itérations. Puisque Ruby est un langage relativement lent, éviter les parties lentes du langage lent a beaucoup de sens :-)
Andrew Hodgkinson
2
@AndrewHodgkinson alors qu'en général, je suis d'accord et je ne préconise pas de ne pas prêter attention aux performances d'exécution, le suivi de tous ces pièges de performance ne commence-t-il pas à devenir un problème et à aller à l'encontre de la philosophie «productivité des développeurs d'abord» de ruby? Je suppose que c'est moins un commentaire pour vous, que plus un commentaire général sur le paradoxe éventuel auquel cela nous amène, en utilisant le rubis.
elsurudo
4
L'énigme étant: eh bien, nous abandonnons déjà la performance dans notre décision d'utiliser même le rubis, alors quelle différence fait "cet autre petit morceau"? C'est une pente glissante, n'est-ce pas? Pour mémoire, je préfère cette solution à la réponse acceptée, du point de vue de la lisibilité.
elsurudo
1
Si vous utilisez Ruby 2.4+, il est encore plus facile à utiliser #transform_values!comme indiqué par sschmeck ( stackoverflow.com/a/41508214/6451879 ).
Finn
128

Ruby 2.4 a introduit la méthode Hash#transform_values!que vous pouvez utiliser.

{ :a=>'a' , :b=>'b' }.transform_values! { |v| "%#{v}%" }
# => {:a=>"%a%", :b=>"%b%"} 
sschmeck
la source
Exactement ce que je cherchais!
Dolev
4
Bien sûr, il y a aussi Hash#transform_values(sans le bang), qui ne modifie pas le récepteur. Sinon une excellente réponse, merci!
iGEL
1
Cela va vraiment réduire mon utilisation de reduce:-p
Igel
86

La meilleure façon de modifier les valeurs d'un Hash en place est

hash.update(hash){ |_,v| "%#{v}%" }

Moins de code et une intention claire. Aussi plus rapide car aucun nouvel objet n'est alloué au-delà des valeurs qui doivent être modifiées.

Sim
la source
Pas exactement vrai: de nouvelles chaînes sont allouées. Pourtant, une solution intéressante qui est efficace. +1
Phrogz
@Phrogz bon point; J'ai mis à jour la réponse. L'allocation de valeur ne peut pas être évitée en général car toutes les transformations de valeur ne peuvent pas être exprimées en tant que mutateurs tels que gsub!.
Sim
3
Identique à ma réponse mais avec un autre synonyme, je conviens que cela updateexprime mieux l'intention que merge!. Je pense que c'est la meilleure réponse.
1
Si vous n'utilisez pas k, utilisez à la _place.
sekrett
28

Un peu plus lisible, mapc'est à un tableau de hachages à un seul élément et reducecela avecmerge

the_hash.map{ |key,value| {key => "%#{value}%"} }.reduce(:merge)
coupe-bordures
la source
2
Plus clair à utiliserHash[the_hash.map { |key,value| [key, "%#{value}%"] }]
meagar
Il s'agit d'une manière extrêmement inefficace de mettre à jour les valeurs. Pour chaque paire de valeurs, il crée d'abord une paire Array(pour map) puis un Hash. Ensuite, chaque étape de l'opération de réduction dupliquera le "mémo" Hashet y ajoutera la nouvelle paire clé-valeur. Au moins une utilisation :merge!dans reducede modifier la finale Hashen place. Et au final, vous ne modifiez pas les valeurs de l'objet existant mais créez un nouvel objet, ce qui n'est pas ce que la question a posée.
Sim
il renvoie nilsi the_hashest vide
DNNX
16

Une méthode qui n'introduit pas d'effets secondaires sur l'original:

h = {:a => 'a', :b => 'b'}
h2 = Hash[h.map {|k,v| [k, '%' + v + '%']}]

Hash # map peut également être une lecture intéressante car elle explique pourquoi le Hash.mapne renvoie pas de Hash (c'est pourquoi le tableau de [key,value]paires résultant est converti en un nouveau Hash) et fournit des approches alternatives au même modèle général.

Bon codage.

[Clause de non-responsabilité: je ne sais pas si la Hash.mapsémantique change dans Ruby 2.x]


la source
7
Matz sait-il même si la Hash.mapsémantique change dans Ruby 2.x?
Andrew Grimm
1
La méthode Hash # [] est si utile, mais si moche. Existe-t-il une méthode plus jolie pour convertir des tableaux en hachages de la même manière?
Martijn
15
my_hash.each do |key, value|
  my_hash[key] = "%#{value}%"
end
Andrew Marshall
la source
Je n'aime pas les effets secondaires, mais +1 pour l'approche :) Il y en a each_with_objectdans Ruby 1.9 (IIRC) qui évite d'avoir à accéder directement au nom et Map#mergepeut aussi fonctionner. Je ne sais pas en quoi les détails complexes diffèrent.
1
Le hachage initial est modifié - c'est correct si le comportement est anticipé mais peut causer des problèmes subtils s'il est "oublié". Je préfère réduire la mutabilité des objets, mais ce n'est pas toujours pratique. (Ruby est à peine un langage "sans effets secondaires" ;-)
8

Hash.merge! est la solution la plus propre

o = { a: 'a', b: 'b' }
o.merge!(o) { |key, value| "%#{ value }%" }

puts o.inspect
> { :a => "%a%", :b => "%b%" }
Dorian
la source
@MichieldeMare Je suis désolé, je n'ai pas suffisamment testé cela. Vous avez raison, le bloc doit prendre deux paramètres. Corrigée.
5

Après l'avoir testé avec RSpec comme ceci:

describe Hash do
  describe :map_values do
    it 'should map the values' do
      expect({:a => 2, :b => 3}.map_values { |x| x ** 2 }).to eq({:a => 4, :b => 9})
    end
  end
end

Vous pouvez implémenter Hash # map_values ​​comme suit:

class Hash
  def map_values
    Hash[map { |k, v| [k, yield(v)] }]
  end
end

La fonction peut alors être utilisée comme ceci:

{:a=>'a' , :b=>'b'}.map_values { |v| "%#{v}%" }
# {:a=>"%a%", :b=>"%b%"}
wedesoft
la source
1

Si vous êtes curieux de savoir quelle variante en place est la plus rapide, voici:

Calculating -------------------------------------
inplace transform_values! 1.265k  0.7%) i/s -      6.426k in   5.080305s
      inplace update      1.300k  2.7%) i/s -      6.579k in   5.065925s
  inplace map reduce    281.367   1.1%) i/s -      1.431k in   5.086477s
      inplace merge!      1.305k  0.4%) i/s -      6.630k in   5.080751s
        inplace each      1.073k  0.7%) i/s -      5.457k in   5.084044s
      inplace inject    697.178   0.9%) i/s -      3.519k in   5.047857s
lzap
la source