178

How do I swap keys and values in a Hash?

I have the following Hash:

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

that I want to transform into:

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

Using map seems rather tedious. Is there a shorter solution?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Jonathan Allard
  • 18,429
  • 11
  • 54
  • 75

6 Answers6

331

Ruby has a helper method for Hash that lets you treat a Hash as if it was inverted (in essence, by letting you access keys through values):

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

If you want to keep the inverted hash, then Hash#invert should work for most situations:

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

BUT...

If you have duplicate values, invert will discard all but the last occurrence of your values (because it will keep replacing new value for that key during iteration). Likewise, key will only return the first match:

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

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

So, if your values are unique you can use Hash#invert. If not, then you can keep all the values as an array, like this:

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

Note: This code with tests is now on GitHub.

Or:

class Hash
  def safe_invert
    self.each_with_object({}){|(k,v),o|(o[v]||=[])<<k}
  end
end
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Nigel Thorne
  • 21,158
  • 3
  • 35
  • 51
  • 4
    [`each_with_object`](http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-each_with_object) makes more sense here than `inject`. – Andrew Marshall Jun 12 '12 at 03:35
  • so that becomes `each_with_object({}){ |i,o|k,v = *i; o[v] ||=[]; o[v] << k}` ... nice – Nigel Thorne Jun 12 '12 at 03:50
  • 3
    Omg. i didnt know you could do |(key,value),out|. That so awesome, I hated that array coming in there instead of key and value. Thanks so much – Iuri G. Jun 13 '14 at 17:03
70

You bet there is one! There is always a shorter way to do things in Ruby!

It's pretty simple, just use Hash#invert:

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

Et voilà!

Jonathan Allard
  • 18,429
  • 11
  • 54
  • 75
3
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

This will handle the duplicate values too.

Nigel Thorne
  • 21,158
  • 3
  • 35
  • 51
Riaze
  • 64
  • 3
  • 1
    Can you bit explain the answer what's happening in each step? – Sajjad Murtaza May 07 '19 at 23:08
  • Personally I avoid setting the hash's default value behaviour. I worry that whatever code I give this hash to wouldn't be expecting a hash to behave in that way, and it could cause some insidious error later. I can't really justify this concern. It's just a doubt I can't seem to ignore. Principle of least surprise? – Nigel Thorne May 10 '19 at 02:30
  • When answering with code it's really important to explain how the code works and why it's the appropriate solution. The goal is to educate, not merely solve the immediate problem. – the Tin Man Jan 14 '20 at 00:21
2

If you have a hash where are the keys are unique, you can use Hash#invert:

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

That won't work if you have non unique keys, however, where only the last keys seen will be kept:

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

If you have a hash with non unique keys, you might do:

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

If the values of the hash are already arrays, you can do:

> 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
  • 98,345
  • 23
  • 131
  • 206
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 gives you:

 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

whereas the built-in invert method is just broken:

 h.invert
  => {1=>:a, 2=>:c}    # FAIL
 h.invert.invert == h 
  => false             # FAIL
Tilo
  • 33,354
  • 5
  • 79
  • 106
1

Using Array

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

Using Hash

input = {:key1=>"value1", :key2=>"value2", :key3=>"value3", :key4=>"value4", :key5=>"value5"}
output = input.invert