Inverting a Hash
Problem
Given a hash, you want to switch the keys and values. That is, you want to create a new hash whose keys are the values of the old hash, and whose values are the keys of the old hash. If the old hash mapped "human" to "wolf;" you want the new hash to map "wolf" to "human."
Solution
The simplest technique is to use the Hash#invert method:
phone_directory = { 'Alice' => '555-1212', 'Bob' => '555-1313', 'Mallory' => '111-1111' } phone_directory.invert # => {"111-1111"=>"Mallory", "555-1212"=>"Alice", "555-1313"=>"Bob"}
Discussion
Hash#invert probably won't do what you want if your hash maps more than one key to the same value. Only one of the keys for that value will show up as a value in the inverted hash:
phone_directory = { 'Alice' => '555-1212', 'Bob' => '555-1313', 'Carol' => '555-1313', 'Mallory' => '111-1111', 'Ted' => '555-1212' } phone_directory.invert # => {"111-1111"=>"Mallory", "555-1212"=>"Ted", "555-1313"=>"Bob"}
To preserve all the data from the original hash, borrow the idea behind Recipe 5.6, and write a version of invert that keeps an array of values for each key. The following is based on code by Tilo Sloboda:
class Hash def safe_invert new_hash = {} self.each do |k,v| if v.is_a? Array v.each { |x| new_hash.add_or_append(x, k) } else new_hash.add_or_append(v, k) end end return new_hash end
The add_or_append method a lot like the method MultivaluedHash#[]= defined in Recipe 5.6:
def add_or_append(key, value) if has_key?(key) self[key] = [value, self[key]].flatten else self[key] = value end end end
Here's safe_invert in action:
phone_directory.safe_invert # => {"111-1111"=>"Mallory", "555-1212"=>["Ted", "Alice"], # "555-1313"=>["Bob", "Carol"]} phone_directory.safe_invert.safe_invert # => {"Alice"=>"555-1212", "Mallory"=>"111-1111", "Ted"=>"555-1212", # => "Carol"=>"555-1313", "Bob"=>"555-1313"}
Ideally, if you called an inversion method twice you'd always get the same data you started with. The safe_invert method does better than invert on this score, but it's not perfect. If your original hash used arrays as hash keys, safe_invert will act as if you'd individually mapped each element in the array to the same value. Call safe_invert twice, and the arrays will be gone.
See Also
- Recipe 5.5, "Using an Array or Other Modifiable Object as a Hash Key"
- "True Inversion of a Hash in Ruby," by Tilo Sloboda (http://www.unixgods.org/~tilo/Ruby/invert_hash.html)
- The Facets library defines a Hash#inverse method much like safe_invert