Jonathan Allard
Jonathan Allard

Reputation: 19249

How to swap keys and values in a hash

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?

Upvotes: 180

Views: 74850

Answers (8)

some one
some one

Reputation: 1

OK, to make things a bit clearer:

You can use the Array#to_h method in order to construct a Hash object. Actually, what you use is an array of sub-arrays with two respective elements, like this:

arr = [["key0", "value0"], ["key1", "value1"], ["key2", "value2"]]
h = arr.to_h #=> {"key0"=>"value0", "key1"=>"value1", "key2"=>"value2"}

Furthermore, you can extract a Hash object's key values using the Hash#keys method, receiving an array as a result, like this:

key_array = h.keys #=> ["key0", "key1", "key2"]

The same goes for Hash values with the Hash#values method:

value_array = h.values #=> ["value0", "value1", "value2"]

Now, the Array#zip method can combine two (or more) arrays into a single array that has paired (or tripled etc.) sub-arrays. Of course the arrays to be combined in this way must have the same count of elements, like this:

arr_first_sub_elements = ["first0", "first1", "first2"]
arr_second_sub_elements = ["second0", "second1", "second2"]
combined = arr_first_sub_elements.zip(arr_second_sub_elements)
#=> [["first0", "second0"], ["first1", "second1"], ["first2", "second2"]]

So finally we can put it all together, extracting keys and values with Hash#keys and Hash#values and re-combining them (reversing keys and values) with Array#zip and Array#to_h, like this:

myhash = {"zero" => 0, "one" => 1, "two" => 2}
mykeys = myhash.keys
myvalues = myhash.values
reversed_hash = myvalues.zip(mykeys).to_h
#=> {0=>"zero", 1=>"one", 2=>"two"}

Upvotes: 0

some one
some one

Reputation: 1

It isn't so hard at all:

>> numstrs = %w(zero one two three four)
=> ["zero", "one", "two", "three", "four"]
>> numints = (0..4).to_a
=> [0, 1, 2, 3, 4]
>> combarr = numints.zip(numstrs)
=> [[0, "zero"], [1, "one"], [2, "two"], [3, "three"], [4, "four"]]
>> combhash = combarr.to_h
=> {0=>"zero", 1=>"one", 2=>"two", 3=>"three", 4=>"four"}
>> invhash = combhash.values.zip(combhash.keys).to_h
=> {"zero"=>0, "one"=>1, "two"=>2, "three"=>3, "four"=>4}

Upvotes: 0

Nigel Thorne
Nigel Thorne

Reputation: 21548

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

Upvotes: 334

Riaze
Riaze

Reputation: 64

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.

Upvotes: 3

dawg
dawg

Reputation: 103884

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

Upvotes: 2

Prashant Ravi Darshan
Prashant Ravi Darshan

Reputation: 545

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

Upvotes: 1

Tilo
Tilo

Reputation: 33732

# 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

Upvotes: 1

Jonathan Allard
Jonathan Allard

Reputation: 19249

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à!

Upvotes: 72

Related Questions