Comment échanger des clés et des valeurs dans un hachage

154

Comment permuter les clés et les valeurs dans un hachage?

J'ai le hash suivant:

{:a=>:one, :b=>:two, :c=>:three}

que je souhaite transformer en:

{:one=>:a, :two=>:b, :three=>:c}

L'utilisation mapsemble plutôt fastidieuse. Existe-t-il une solution plus courte?

Jonathan Allard
la source

Réponses:

280

Ruby a une méthode d'assistance pour Hash qui vous permet de traiter un Hash comme s'il était inversé (en substance, en vous permettant d'accéder aux clés via des valeurs):

{a: 1, b: 2, c: 3}.key(1)
=> :a

Si vous souhaitez conserver le hachage inversé, Hash # invert devrait fonctionner dans la plupart des situations:

{a: 1, b: 2, c: 3}.invert
=> {1=>:a, 2=>:b, 3=>:c}

MAIS...

Si vous avez des valeurs en double, invertsupprimera toutes les occurrences de vos valeurs sauf la dernière (car elle continuera à remplacer la nouvelle valeur pour cette clé pendant l'itération). De même, keyne renverra que la première correspondance:

{a: 1, b: 2, c: 2}.key(2)
=> :b

{a: 1, b: 2, c: 2}.invert
=> {1=>:a, 2=>:c}

Donc, si vos valeurs sont uniques, vous pouvez utiliser Hash#invert. Sinon, vous pouvez conserver toutes les valeurs sous forme de tableau, comme ceci:

class Hash
  # like invert but not lossy
  # {"one"=>1,"two"=>2, "1"=>1, "2"=>2}.inverse => {1=>["one", "1"], 2=>["two", "2"]} 
  def safe_invert
    each_with_object({}) do |(key,value),out| 
      out[value] ||= []
      out[value] << key
    end
  end
end

Remarque: ce code avec des tests est maintenant sur GitHub .

Ou:

class Hash
  def safe_invert
    self.each_with_object({}){|(k,v),o|(o[v]||=[])<<k}
  end
end
Nigel Thorne
la source
4
each_with_objecta plus de sens ici que inject.
Andrew Marshall
pour que ça devienne each_with_object({}){ |i,o|k,v = *i; o[v] ||=[]; o[v] << k}... sympa
Nigel Thorne
3
OMG. je ne savais pas que vous pouviez faire | (clé, valeur), out |. C'est tellement génial, je détestais que ce tableau arrive au lieu de la clé et de la valeur. Merci beaucoup
Iuri G.
63

Vous pariez qu'il y en a un! Il y a toujours un moyen plus court de faire les choses dans Ruby!

C'est assez simple, il suffit d'utiliser Hash#invert:

{a: :one, b: :two, c: :three}.invert
=> {:one=>:a, :two=>:b, :three=>:c}

Et voilà!

Jonathan Allard
la source
4
Hash # invert ne fonctionne pas si les mêmes valeurs apparaissent plusieurs fois dans votre hachage.
Tilo
2
files = {
  'Input.txt' => 'Randy',
  'Code.py' => 'Stan',
  'Output.txt' => 'Randy'
}

h = Hash.new{|h,k| h[k] = []} # Create hash that defaults unknown keys to empty an empty list
files.map {|k,v| h[v]<< k} #append each key to the list at a known value
puts h

Cela gérera également les valeurs en double.

Riaze
la source
1
Pouvez-vous expliquer brièvement ce qui se passe à chaque étape?
Sajjad Murtaza
Personnellement, j'évite de définir le comportement de la valeur par défaut du hachage. Je crains que quel que soit le code auquel je donne ce hachage, je ne m'attendais pas à ce qu'un hachage se comporte de cette manière, et cela pourrait provoquer une erreur insidieuse plus tard. Je ne peux pas vraiment justifier cette inquiétude. C'est juste un doute que je n'arrive pas à ignorer. Principe de moindre surprise?
Nigel Thorne
Lorsque vous répondez avec du code, il est très important d'expliquer comment le code fonctionne et pourquoi c'est la solution appropriée. Le but est d'éduquer, pas simplement de résoudre le problème immédiat.
the Tin Man le
1
# this doesn't looks quite as elegant as the other solutions here,
# but if you call inverse twice, it will preserve the elements of the original hash

# true inversion of Ruby Hash / preserves all elements in original hash
# e.g. hash.inverse.inverse ~ h

class Hash

  def inverse
    i = Hash.new
    self.each_pair{ |k,v|
      if (v.class == Array)
        v.each{ |x|
          i[x] = i.has_key?(x) ? [k,i[x]].flatten : k
        }
      else
        i[v] = i.has_key?(v) ? [k,i[v]].flatten : k
      end
    }
    return i
  end

end

Hash#inverse vous donne:

 h = {a: 1, b: 2, c: 2}
 h.inverse
  => {1=>:a, 2=>[:c, :b]}
 h.inverse.inverse
  => {:a=>1, :c=>2, :b=>2}  # order might not be preserved
 h.inverse.inverse == h
  => true                   # true-ish because order might change

alors que la invertméthode intégrée est juste cassée:

 h.invert
  => {1=>:a, 2=>:c}    # FAIL
 h.invert.invert == h 
  => false             # FAIL
Tilo
la source
1

Utilisation de Array

input = {:key1=>"value1", :key2=>"value2", :key3=>"value3", :key4=>"value4", :key5=>"value5"}
output = Hash[input.to_a.map{|m| m.reverse}]

Utiliser Hash

input = {:key1=>"value1", :key2=>"value2", :key3=>"value3", :key4=>"value4", :key5=>"value5"}
output = input.invert
Prashant Ravi Darshan
la source
1

Si vous avez un hachage où les clés sont uniques, vous pouvez utiliser Hash # invert :

> {a: 1, b: 2, c: 3}.invert
=> {1=>:a, 2=>:b, 3=>:c} 

Cela ne fonctionnera pas si vous avez des clés non uniques, cependant, où seules les dernières clés vues seront conservées:

> {a: 1, b: 2, c: 3, d: 3, e: 2, f: 1}.invert
=> {1=>:f, 2=>:e, 3=>:d}

Si vous avez un hachage avec des clés non uniques, vous pouvez faire:

> hash={a: 1, b: 2, c: 3, d: 3, e: 2, f: 1}
> hash.each_with_object(Hash.new { |h,k| h[k]=[] }) {|(k,v), h| 
            h[v] << k
            }     
=> {1=>[:a, :f], 2=>[:b, :e], 3=>[:c, :d]}

Si les valeurs du hachage sont déjà des tableaux, vous pouvez faire:

> hash={ "A" => [14, 15, 16], "B" => [17, 15], "C" => [35, 15] }
> hash.each_with_object(Hash.new { |h,k| h[k]=[] }) {|(k,v), h| 
            v.map {|t| h[t] << k}
            }   
=> {14=>["A"], 15=>["A", "B", "C"], 16=>["A"], 17=>["B"], 35=>["C"]}
dawg
la source