Comparing custom objects in Ruby using uniq.

by Rabbit

You author a Client model. You have an array of clients and you want to call Ruby’s uniq method on them. You define “unique” clients as having the same name. This doesn’t work:

>> c1 = Client.find(1)
=> #<Client id: 1, artist_id: 1, name: "Daniel Waite", alias: "Rabbit", birthdate: nil, created_at: "2008-01-13 13:20:14", updated_at: "2008-01-23 01:03:26">
>> c2 = Client.find(4)
=> #<Client id: 4, artist_id: 1, name: "Daniel Waite", alias: nil, birthdate: nil, created_at: "2008-01-14 23:44:58", updated_at: "2008-01-14 23:44:58">
 
[ c1, c2 ].uniq.size # 2

Being the clever Ruby programmer you are, you quickly modify client…

class Client < ActiveRecord::Base
 
  def ==(other)
    name == other.name
  end
 
end

Satisfied, you run the above code again.

[ c1, c2 ].uniq.size # 2

What the fuck? Frustrated, you go medieval…

class Client < ActiveRecord::Base
 
  def ==(other)
    name == other.name
  end
 
  def ===(other)
    name == other.name
  end
 
  def eql?(other)
    name == other.name
  end
 
  def equal?(other)
    name == other.name
  end
 
end

Huffing and puffing, you carefully run your code again…

[ c1, c2 ].uniq.size # 2

God damn it!

But fear not my friend, for you have sought, and so the answer shall be revealed to you…

class Client < ActiveRecord::Base
 
  def hash
    name.hash
  end
 
  def eql?(other)
    name == other.name
  end
 
end

Wearily, you try again…

[ c1, c2 ].uniq.size # 1

Success!

Yes, both methods must be overridden.

You may have never seen it, but the hash method is available to all objects. I’m not quite sure how or why this works, but it does. Rejoice.

**UPDATE**

Russ pointed out that for new or unsaved objects, yes, all you have to override is the eql? method. However, to compare for equality for existing objects (i.e. objects retrieved from the database) you must override both methods. This is because Rails uses the ID attribute of your models to test for equality, and all unsaved models have an ID attribute of nil, so of course nil == nil.

This makes things more interesting, too. If you wanted to define equality between clients as having both the same name and the same birthdate, you can write…

class Client < ActiveRecord::Base
 
  def hash
    %{#{ name }#{ birthdate }}.hash
  end
 
  def eql?(other)
    hash == other.hash
  end
 
end

And now you have a more complex definition of what equality means. Gotta love Ruby.