True Inversion of a Hash in Ruby
by Tilo Sloboda, Nov 2004

The Ruby Hash.invert method should come with the following warning label:

 

What do you expect if you want to compute an inverted Hash?

Wouldn't you expect that:

Let's see what Ruby's built-in Hash.invert method does to a hash:

        h = {"eins"=>1, "drei"=>3, "uno"=>1, "one"=>1, "two"=>2, "san"=>3, "ichi"=>1, "three"=>3, "four"=>4}
        h.invert
        => {1=>"one", 2=>"two", 3=>"drei", 4=>"four"}

        #   the above result IS SIMPLY WRONG!   Why?

        h.invert.invert
	=> {"two"=>2, "one"=>1, "drei"=>3, "four"=>4}

	h.invert.invert == h
	=> false

Not only is the above result very questionable, but you actually lose data which was stored in the original hash. I would say that Ruby's built-in Hash.invert method is simply broken! For an explanaition on why, see below

 

Correctly working Hash.inverse Implementation

If you use the new implemnentation of Hash.invert , you can either access it with Hash.inverse , or you can overload the old method with the new one and still access the old method with Hash.old_invert (see below)
	> h = {"eins"=>1, "drei"=>3, "uno"=>1, "one"=>1, "two"=>2, "san"=>3, "ichi"=>1, "three"=>3, "four"=>4}
	=> {"uno"=>1, "three"=>3, "two"=>2, "eins"=>1, "ichi"=>1, "san"=>3, "one"=>1, "drei"=>3, "four"=>4}

	> h.inverse
	=> {1=>["one", "ichi", "eins", "uno"], 2=>"two", 3=>["drei", "san", "three"], 4=>"four"}

	> h.inverse.inverse
	=> {"uno"=>1, "three"=>3, "two"=>2, "eins"=>1, "san"=>3, "ichi"=>1, "one"=>1, "drei"=>3, "four"=>4}

	> h.inverse.inverse == h
	=> true
 
Isn't that a much more pleasing result?

 

Overloading Hash.invert

In case you want to overload the old method, and replace it completely, you may want to do this:
	class Hash
	    alias old_invert invert

	    def invert
	       self.inverse
	    end
	end

 

If you always want to overload the Hash.invert method , you can modify the file invert_hash.rb , by removing the line which contains __END__

 

Download

Hash.inverse is also available through the Ruby Facets library.

... and is referenced in the Ruby Cookbook

 

License

Freely available under the terms of the OpenSource "Artistic License" in combination with the Addendum A (below)
In case you did not get a copy of the license along with the software, it is also available at:   http://www.unixgods.org/~tilo/artistic-license.html

 

 

Why is Ruby's Hash.invert broken?

I beleive that it's broken because of an inaccurate design-assumption.

Simple explanation:

Typically you want to use a Hash when you try to keep track of some data, and store some values associated with each item's key. In the real world multiple keys can map to the same value. The Ruby Hash class does not assume this, hence it can't cope with it.

More Lengthy Explaination:

The Ruby Hash class is a mis-nomer at best.

If you studied algorithms in computer science, then you learned that a hash is a data structure which has a mapping function to compute a key for each piece of data you want to place in the hash, e.g. f(value) = key . The key-concept of a hash is that the key is computed from the data/value. And often (for a hashes without collision resolution) the algorithm designers assume that the key is unique for each piece of data, and that no two pieces of data generate the same key:

    (A1)  Foreach  f(value1) = key1 , f(value2) = key2 :  value1 != value2 <==> key1 != key2

    That means in plain English: each value has only one key, and each key has only one value

Now here's what's wrong with Ruby's implementation of class Hash, and why class Hash in Ruby is not the same as a hash datastructure in CS at all! Ruby's class Hash is actually a mis-nomer.. it should rather be called Dictionary, lacking a better word. And that's how users use it - like a look-up dictionary for arbitrary key/value pairs, for which (in general) multiple different keys can lookup the same data.

Now Ruby's Hash.invert method was probably based on assumption A1 , which is not necessarily true for the data we may want to put into the hash.. that's why Hash.invert is not working properly..