Cambiando ogni valore in un hash in Ruby


170

Voglio cambiare ogni valore in un hash in modo da aggiungere '%' prima e dopo il valore così

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

deve essere modificato in

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

Qual'è il miglior modo per farlo?


1
Si prega di chiarire se si desidera mutare gli oggetti stringa originali, semplicemente mutare l'originale o mutare nulla.
Phrogz,

1
Quindi hai accettato la risposta sbagliata. (Non offendere intenzionalmente @pst, poiché personalmente sostengo anche la programmazione in stile funzionale invece di mutare gli oggetti.)
Phrogz

Ma comunque l'approccio è stato carino
theReverseFlick

Risposte:


178

Se vuoi che le stringhe stesse mutino sul posto (possibilmente e desiderabilmente influenzano altri riferimenti agli stessi oggetti stringa):

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

Se vuoi che l'hash cambi sul posto, ma non vuoi influenzare le stringhe (vuoi che ottenga nuove stringhe):

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

Se vuoi un nuovo hash:

# 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}%"] } ]

1
@Andrew Marshall Hai ragione, grazie. In Ruby 1.8, Hash.[]non accetta un array di coppie di array, richiede un numero pari di argomenti diretti (da qui il simbolo iniziale).
Phrogz,

2
In realtà, Hash. [Key_value_pairs] è stato introdotto in 1.8.7, quindi solo Ruby 1.8.6 non ha bisogno di splat & flatten.
Marc-André Lafortune,

2
@Aupajo Hash#eachrestituisce sia la chiave che il valore al blocco. In questo caso, non mi importava della chiave, e quindi non ho nominato nulla di utile. I nomi delle variabili possono iniziare con un trattino basso, e in effetti può essere solo un trattino basso. Non vi è alcun vantaggio in termini di prestazioni nel fare questo, è solo una sottile nota autocompattante che non sto facendo nulla con quel primo valore di blocco.
Phrogz,

1
Penso che intendi my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }, devi restituire l'hash dal blocco
aceofspades,

1
In alternativa, è possibile utilizzare il metodo each_value, che è un po 'più semplice da comprendere rispetto all'utilizzo di un carattere di sottolineatura per il valore della chiave non utilizzata.
Strand McCutchen,

266

In Ruby 2.1 e versioni successive puoi farlo

{ a: 'a', b: 'b' }.map { |k, str| [k, "%#{str}%"] }.to_h

5
Grazie, in realtà era quello che stavo cercando. Non so perché non sei così votato.
Simreareault

7
Questo è, tuttavia, molto lento e molto affamato di RAM. L'hash di input viene ripetuto per produrre un set intermedio di array nidificati che vengono quindi convertiti in un nuovo hash. Ignorando il picco di utilizzo della RAM, il tempo di esecuzione è molto peggiore: il benchmarking rispetto alle soluzioni di modifica sul posto in un'altra risposta mostra 2,5s contro 1,5s sullo stesso numero di iterazioni. Poiché Ruby è un linguaggio relativamente lento, evitare i bit lenti del linguaggio lento ha molto senso :-)
Andrew Hodgkinson,

2
@AndrewHodgkinson mentre in generale sono d'accordo e non sto sostenendo di non prestare attenzione alle prestazioni di runtime, non tenere traccia di tutte queste insidie ​​delle prestazioni inizia a diventare un dolore e andare contro la filosofia del rubino "produttività prima degli sviluppatori"? Immagino che questo sia meno un commento per te, e più un commento generale sull'eventuale paradosso che questo ci porta, usando ruby.
elsurudo,

4
L'enigma è: beh, stiamo già rinunciando alle prestazioni nella nostra decisione di usare anche il rubino, quindi che differenza fa "quest'altro piccolo"? È una pendenza scivolosa, no? Per la cronaca, preferisco questa soluzione alla risposta accettata, dal punto di vista della leggibilità.
elsurudo,

1
Se si utilizza Rubino 2.4+, è ancora più facile da usare #transform_values!come fuori punte da sschmeck ( stackoverflow.com/a/41508214/6451879 ).
Finn

128

Ruby 2.4 ha introdotto il metodo Hash#transform_values!, che è possibile utilizzare.

{ :a=>'a' , :b=>'b' }.transform_values! { |v| "%#{v}%" }
# => {:a=>"%a%", :b=>"%b%"} 

Esattamente quello che stavo cercando!
Dolev,

4
Naturalmente c'è anche Hash#transform_values(senza il botto), che non modifica il ricevitore. Altrimenti un'ottima risposta, grazie!
iGEL,

1
Questo ridurrà davvero il mio uso di reduce:-p
iGEL il

86

Il modo migliore per modificare i valori di un hash sul posto è

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

Meno codice e intenti chiari. Anche più veloce perché non vengono allocati nuovi oggetti oltre i valori che devono essere modificati.


Non esattamente vero: vengono allocate nuove stringhe. Tuttavia, una soluzione interessante che è efficace. +1
Phrogz,

@Phrogz buon punto; Ho aggiornato la risposta. L'allocazione del valore non può essere evitata in generale perché non tutte le trasformazioni di valore possono essere espresse come mutatori come gsub!.
Sim

3
Come la mia risposta, ma con un altro sinonimo, sono d'accordo che updatetrasmetta l'intenzione meglio di merge!. Penso che questa sia la risposta migliore.

1
Se non lo usi k, usa _invece.
sekrett,

28

Un po 'più leggibile, mapper un array di hash a elemento singolo e reducequello conmerge

the_hash.map{ |key,value| {key => "%#{value}%"} }.reduce(:merge)

2
Più chiaro da usareHash[the_hash.map { |key,value| [key, "%#{value}%"] }]
meagar

Questo è un modo estremamente inefficiente per aggiornare i valori. Per ogni coppia di valori crea prima una coppia Array(per map) quindi a Hash. Quindi, ogni passaggio dell'operazione di riduzione duplicherà il "memo" Hashe vi aggiungerà la nuova coppia chiave-valore. Almeno uso :merge!nel reducemodificare il finale Hashin luogo. E alla fine, non stai modificando i valori dell'oggetto esistente ma stai creando un nuovo oggetto, che non è quello che la domanda ha posto.
Sim

restituisce nilse the_hashè vuoto
DNNX il


16

Un metodo che non introduce effetti collaterali nell'originale:

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

La mappa Hash # può anche essere una lettura interessante in quanto spiega perché Hash.mapnon restituisce un hash (motivo per cui la risultante matrice di[key,value] coppie viene convertita in un nuovo hash) e fornisce approcci alternativi allo stesso schema generale.

Buona codifica.

[Dichiarazione di non responsabilità: non sono sicuro che la Hash.mapsemantica cambi in Ruby 2.x]


7
Matz sa anche se la Hash.mapsemantica cambia in Ruby 2.x?
Andrew Grimm,

1
Il metodo Hash # [] è così utile, ma così brutto. Esiste un metodo più grazioso per convertire array in hash nello stesso modo?
Martijn,

15
my_hash.each do |key, value|
  my_hash[key] = "%#{value}%"
end

Non mi piacciono gli effetti collaterali, ma +1 per l'approccio :) C'è each_with_objectin Ruby 1.9 (IIRC) che evita la necessità di accedere direttamente al nome e Map#mergepotrebbe anche funzionare. Non sono sicuro di come differiscano i dettagli complessi.

1
L'hash iniziale viene modificato: va bene se il comportamento è anticipato ma può causare problemi sottili se "dimenticato". Preferisco ridurre la mutabilità degli oggetti, ma potrebbe non essere sempre pratico. (Ruby non è quasi un linguaggio "privo di effetti collaterali" ;-)

8

Hash.merge! è la soluzione più pulita

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

puts o.inspect
> { :a => "%a%", :b => "%b%" }

@MichieldeMare Mi dispiace, non l'ho provato abbastanza a fondo. Hai ragione, il blocco deve prendere due parametri. Corretto.

5

Dopo averlo testato con RSpec in questo modo:

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

È possibile implementare Hash # map_values ​​come segue:

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

La funzione può quindi essere utilizzata in questo modo:

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

1

Se sei curioso di sapere quale variante inplace è la più veloce qui è:

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
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.