Tous les jours, nous manipulons des tableaux de hachages (des Hash). Créer un nouveau Hash ou récupérer un élément par sa clef sont des actions simples et communes. Mais lorsqu'il s'agit de fusionner 2 Hash imbriqués ou de filtrer de certaines clefs, cela n'est plus aussi simple. Dans la très bonne doc, nous pouvons trouver plein d'explication pour chacune des méthodes de Hash. Mais comme ce n'est présenté par cas d'utilisation, il est difficile de trouver rapidement comment on peut les utiliser dans un cas de figure particulier. Ci-dessous, je partage 7 cas d'usage de Hash que je rencontre régulièrement et qui devrait t'être utile.

  1. Comment convertir un JSON en un Hash ?
  2. Comment convertir un Hash en un JSON ?
  3. Comment définir la valeur par défault d'un Hash imbriqué ?
  4. Comment fusionner deux Hash imbriqués ?
  5. Comment filtrer un Hash de certaines de ses clefs ?
  6. Comment "trier" un Hash par ses valeurs?
  7. Comment trouver les différences entre deux Hash?

1. Comment convertir un JSON en un Hash ?

Tu viens de récupérer un profile Twitter sous format JSON:

data = '{
  "name": "Aaron Patterson",
  "screen_name": "tenderlove",
  "location": "Seattle, WA"
}'

Tu souhaites le transformer en un Hash pour un plus simple manipulation :

require 'json'

profile = JSON.parse(data)

Résultat IRB :

 => {
  "name"=>"Aaron Patterson",
  "screen_name"=>"tenderlove",
  "location"=>"Seattle, WA"
}

Référence : JSON#parse


2. Comment convertir un Hash en un JSON ?

Dans ton application web, tu comptes le nombre d'inscription par jour pour une semaine :

signups_of_the_week = {
    monday: 2,
    tuesday: 3,
    wednesday: 4,
    thursday: 20,
    friday: 5,
    saturday: 2,
    sunday: 5
}

Tu souhaites les fournir via une API sous format JSON :

require 'json'

signups_of_the_week.to_json

Résultat IRB :

 => "{"monday":2,"tuesday":3,"wednesday":4,"thursday":20,"friday":5,"saturday":2,"sunday":5}"

Référence : JSON#generate
Nota Bene : JSON#pretty_generate est très utile pour un affichage élégant ou le débogage.


3. Comme définir la valeur par défaut d'un Hash imbriqué ?

Tu as une collection de contacts indexés par le nom, un Hash imbriqué :

contacts = {
  'John' => {
    name: 'John',
    email: 'john@doe.com'
  },
  'Freddy' => {
    name 'Freddy',
    email: 'freddy@mercury.com'
  }
}

Lors de la manipulation d'un contact, tu ne souhaites pas vérifier à chaque fois s'il existe ou non. Tu peux alors écrire :

contacts['Jane'][:email] = 'jane@doe.com'
puts contacts['Jane']

Résultat IRB :

 => {:name=>"Jane", :email=>"jane@doe.com"}

Pour se faire, il suffit de définir la valeur par défaut lors de la création du Hash :

contacts = Hash.new do |hsh, key|
  hsh[key] = {
    name: key,
    email: ''
  }
end

ou après sa création

contacts.default_proc = Proc.new do |hsh, key|
  hsh[key] = {
    name: key,
    email: ''
  }
end

Références : Hash#new, Hash#default_proc=


4. Comment fusionner deux Hash imbriqués ?

Dans un magasin en ligne, tu souhaites fusionner le panier courant avec la liste de produits souhaités, chacun indexés par l'id produit:

wish_list = {
  8 => {
    title: "The Color of Magic",
  },
  42 => {
    title: "The Hitch-Hiker's Guide to the Galaxy",
    price: 5
  }
}

basket = {
  8 => {
    price: 10
  },
  1729 => {
    title: "Ramanujan:  Twelve Lectures on Subjects Suggested by His Life and Work",
    price: 28
  }
}

Avec ActiveSupport, tu peux simplement les fusionner avec :

require 'active_support/core_ext/hash' # not necessary if in Rails

basket.deep_merge(wish_list)

our sans ActiveSupport

def deep_merge(h1, h2)
  h1.merge(h2) { |key, h1_elem, h2_elem| deep_merge(h1_elem, h2_elem) }
end

deep_merge(basket, wish_list)

Résultat IRB :

 => {
  8=>{:price=>10, :title=>"The Color of Magic"},
  1729=>{:title=>"Ramanujan:  Twelve Lectures on Subjects Suggested by His Life and Work", :price=>28},
  42=>{:title=>"The Hitch-Hiker's Guide to the Galaxy", :price=>5}
}

Références: Hash#merge, Hash#deep_merge


5. Comment filtrer un Hash de certaines de ses clefs ?

Tu as construit un histogramme de ventes journalières et l'as stocké dans un Hash, la clef étant le jour de la semaine :

histogram = {
  monday: 5,
  tuesday: 7,
  wednesday: 10,
  thursday: 18,
  friday: 7,
  saturday: 2,
  sunday: 0
}

Tu souhaites filtrer l'histogramme du Samedi et Dimanche. Avec ActiveSupport,  tu peux l'effectuer avec le code suivant :

require 'active_support/core_ext/hash' # not necessary if Rails

histogram.except(:saturday, :sunday)

ou sans ActiveSupport

def filter(hsh, *keys)
  hsh.dup.tap do |h|
    keys.each { |k| h.delete(k) }
  end
end

filter(histogram, :saturday, :sunday)

Une autre implémentation plus propre basée sur reject (credits to Thiago A.):

def filter2(hsh, *keys)
  hsh.reject { |k, _| keys.include? k }
end

A noter que lors de la manipulation de très large collection, il est préférable de mesurer la performance de votre implémentation et de sélectionner la meilleure.

Résultat IRB :

 => {:monday=>5, :tuesday=>7, :wednesday=>10, :thursday=>18, :friday=>7}

Références : Hash#except, Hash#delete, Hash#reject, Object#dup, Object#tap


6. Comment "trier" un Hash par valeur?

Dans un jeu de dés, tu gardes dans un Hash le score de chacque joueur :

scores = {
  'The Lady' => 3,
  'Fate' => 2,
  'Death' => 10
}

Tu souhaites les trier par scores. Tu peux le faire en faisant :

leaderboard = scores.sort_by { |_, score| -score }

Résultat IRB :

 => [["Death", 10], ["The Lady", 3], ["Fate", 2]]

Référence : Enumerable#sort_by
Nota Bene : Hash énumère ses valeurs par ordre d'insertion.


7. Comment trouver les différences entre deux Hash ?

Tu récupères régulièrement des données d'un fil RSS et les stocke dans un Hash :

entries = {
  1372284000 => "CVE-2013-4073",
  1368482400 => "CVE-2013-2065"
}

Lors d'une mise à jour, tu récupère un nouvel Hash :

updated_entries = {
  1385074800 => "CVE-2013-4164",
  1372284000 => "CVE-2013-4073",
  1368482400 => "CVE-2013-2065"
}

Tu peux trouver les nouvelles entrées avec

new_entries = updated_entries.reject { |k, _| entries.include? k }

qui te permet de bénéficier de la recherche rapide du Hash (merci à apeiros)

Résultat IRB :

=> {1385074800=>"CVE-2013-4164"}

Références : Hash#delete_if, Hash#keys, Hash#reject, Hash#include?


Et toi ? Quelles sont tes utilisations quotidiennes d'un Hash ?

Après les trésors des tableaux, c'était fort amusant de lister ceux des Hash. Et toi ? Quelles sont tes perles ? Quel sont tes cas d'usage typique d'un Hash et l'élégant code Ruby qui l'implémente ?