Ruby Enumerables: Operazioni con le hash

Nel mio ultimo post ho parlato delle hash di Ruby. Oggi riprendo e completo l'argomento descrivendo alcune delle operazioni che si possono eseguire sulle hash per manipolarne i valori. 1. Accedere ai valori iterativamente La classe Hash di ruby mette a disposizione tre diversi iteratori: each, each_key, each_value. Esiste in realtà un quarto iteratore, each_pair che però è solo un alias di each. Come ci si può aspettare l'iteratore each funziona come segue:

{"company" => "DevInterface", "activity" => "Web Agency"}.each do |key, val|
  p key, ": ", val
end
# Will print
"company: DevInterface"
"activity: Web Agency"
Gli altri due operatori permettono rispettivamente di iterare solo sulle chiavi o solo sui valori della hash.

{"name" => "Claudio", "role" => "Web Developer"}.each_key do |key|
  p key
end
# Will print
"name"
"role"
{"name" => "Claudio", "role" => "Web Developer"}.each_value do |val|
  p val
end
# Will print
"Claudio"
"Web Developer"
2. Invertire i valori con le chiavi Un'operazione che può essere molto utile in certi casi è l'inversione dei valori con le chiavi. L'esempio classico è la rubrica telefonica. Supponiamo di avere una hash che contiene la rubrica telefonica come segue

phone_numbers = {"DevInterface" => "339-045-2223836", "John" => "555-6677", "Stefano" => "335-12345678"}
e di voler sapere a chi appartiene il numero "555-6677". Potremmo iterare su tutta la hash fino a trovare il valore cercato e poi ritornare la chiave. Ma se la hash invece di 3 elementi ne contenesse 1000 questa operazione diventerebbe molto lenta. L'alternativa è invertire la nostra hash in modo che i numeri diventino le chiavi ed i nomi i valori. La class Hash mette a disposizione per questo scopo il metodo invert:

inverted_phone_numbers = phone_numbers.invert
inverted_phone_numbers["555-6677"]  # Will return "John"
Prima di utilizzare il metodo invert tuttavia, è necessario tenere presente che le hash hanno chiavi uniche, ma i valori possono essere duplicati. Di conseguenza, quando si inverte una hash eventuali valori duplicati saranno convertiti in un unica chiave, con conseguente perdita di informazioni. Di fatto, non è predicibile quale dei valori verrà tenuto e quali verranno invece scartati. Riprendendo l'esempio della rubrica se avessimo:

phone_numbers = {"DevInterface" => "339-045-2223836", "John" => "555-6677", "Mary" => "555-6677", "Stefano" => "335-12345678"}
ed eseguissimo la stessa operazione di prima

inverted_phone_numbers = phone_numbers.invert
inverted_phone_numbers["555-6677"]  # Will return "John" or "Mary"
non potremo sapere con certezza se ci verrà restituito "John" oppure "Mary". 3. Convertire un hash in un array E' possibile convertire una hash in un array utilizzando il metodo to_a. Il risultato sarà un array in cui gli elementi pari corrispondono alle chiavi ed i dispari ai valori della hash di partenza.

my_hash = {"a" => 1, "b" => 2, "c" => 3}
my_hash.to_a   # ["a", 1, "b", 2, "c", 3]
A volte però è più comodo avere due array separati per chiavi e valori. E' possibile ottenerli nel seguente modo:

my_hash.keys.to_a     # ["a", "b", "c"]
my_hash.values.to_a   # [1, 2, 3]
Infine, utilizzando il metodo values_at della classe Hash è possibile estrarre in un array solo alcuni valori specifici, determinati dalle chiavi passate al metodo:

my_hash.values_at("a", "c")   # [1, 3]
Ruby permette anche la conversione inversa, ovvero creare una hash a partire da un array:

my_array = [1, 2, 3, 4, 5, 6]
my_hash = Hash[*my_array]   # {1 => 2, 3 => 4, 5 => 6}
Questa conversione è possibile solo se l'array ha un numero pari di elementi. 4. Ordinamento Una hash, come visto nel post precedente, è una struttura non ordinata. Tuttavia in alcuni casi può rendersi necesario ordinarne i valori. Esiste quindi un metodo sort anche per le hash il cui risultato però è un array. Questo perchè Ruby, per ordinare una hash la converte in un array di array e poi lo ordina.

beatles = {"Jonn" => "Lennon", "Ringo" => "Starr", "Paul" => "McCartney", "George" => "Harrison"}
beatles.sort  #  [["George", "Harrison"], ["Jonn", "Lennon"], ["Paul", "McCartney"], ["Ringo", "Starr"]]
5. Merge di due Hash L'ultimo argomento che voglio trattare oggi è il merge di due hash. Supponiamo di voler fare un improbabile merge tra due storici gruppi, i Beatles ed i Rolling Stones:

beatles = {"Jonn" => "Lennon", "Ringo" => "Starr", "Paul" => "McCartney", "George" => "Harrison"}
rolling_stones = {"Mick" => "Jagger", "Keith" => "Richards", "Ronnie" => "Wood", "Charlie" => "Watts"}
rolling_beatles = beatles.merge(rolling_stones)
Il risultato è una terza hash composta come segue:

p rolling_beatles
# {"Jonn" => "Lennon", "Ringo" => "Starr", "Paul" => "McCartney", "George" => "Harrison",
 "Mick" => "Jagger", "Keith" => "Richards", "Ronnie" => "Wood", "Charlie" => "Watts"}
Nel caso di chiavi duplicate però, il merge mantiene solo la chiave della seconda hash:

a = {"a" => 1, "b" => 2, "c" =>3}
b = {"a" => 5, "d" => 7, "e" =>9}
c = a.merge(b)  # {"a" => 5, "b" => 2, "c" =>3, "d" => 7, "e" => 9}
In alternativa, è possibile passare al metodo merge un blocco di codice per gestire i conflitti sulle chiavi

a = {"a" => 1, "b" => 2, "c" =>3}
b = {"a" => 5, "d" => 7, "e" =>9}
c = a.merge(b) {|key, old_val, new_val| old_val < new_val ? old_val : new_val}
 # {"a" => 1, "b" => 2, "c" =>3, "d" => 7, "e" => 9}
Come potete vedere il risultato di questo merge è differente dal precedente in base alla condizione specificata nel blocco di codice.