Extending uniq method
-
21-09-2019 - |
Question
This is Ruby 1.8 Question:
We all know how to use Array#uniq
:
[1,2,3,1].uniq #=> [1,2,3]
However I'm wondering if we can monkey patch it in a way to work with complex objects. The current behavior is like this:
[{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}].uniq
#=> [{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}]
The requested one is:
[{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}].uniq
#=> [{"three"=>"3"}, {"three"=>"4"}]
Solution
It already works for me in 1.8.7.
1:~$ ruby -v
ruby 1.8.7 (2008-08-11 patchlevel 72) [i486-linux]
1:~$ irb -v
irb 0.9.5(05/04/13)
1:~$ irb
>> [{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}].uniq
=> [{"three"=>"3"}, {"three"=>"4"}]
OTHER TIPS
To make Array#uniq work for any object you must override two methods: hash and eql?
All objects have a hash method which calculates the hash value for that object, so for two objects to be equal their values when hashed must also be equal.
Example--a user is unique when their email address is unique:
class User
attr_accessor :name,:email
def hash
@email.hash
end
def eql?(o)
@email == o.email
end
end
>> [User.new('Erin Smith','roo@example.com'),User.new('E. Smith','roo@example.com')].uniq
=> [#<User:0x1015a97e8 @name="Erin Smith", @email="maynurd@example.com"]
The problem is that Hash#hash
and Hash#eql?
both give bogus results in Ruby 1.8.6. This is one of the very rare monkey patches I've been willing to perform, because this bug seriously breaks a lot of code — in particular memoizing functions. Just be careful with monkey patches that you don't override non-broken behavior.
So:
class Hash
if {}.hash != {}.hash
def hash
# code goes here
end
end
if !{}.eql?({})
def eql?(other)
# code goes here
end
end
end
But if you're doing something where you control the deploy environment, just raise an error if the app gets started with 1.8.6.
How about this?
h={}
[{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}].select {|e| need=!h.key?(e) ; h[e]=1 ; need}
#=> [{"three"=>"3"}, {"three"=>"4"}]
I've run into this myself many times. Hash equality in Ruby 1.8.6 is broken:
require 'test/unit'
class TestHashEquality < Test::Unit::TestCase
def test_that_an_empty_Hash_is_equal_to_another_empty_Hash
assert({}.eql?({}), 'Empty Hashes should be eql.')
end
end
Passes in Ruby 1.9 and Ruby 1.8.7, fails in Ruby 1.8.6.
1.8.7 :039 > [{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}].uniq {|x|x.values}
=> [{"three"=>"3"}, {"three"=>"4"}]
1.8.7 :040 > [{"three"=>"3"}, {"three"=>"4"}, {"three"=>"3"}].uniq {|x|x.keys}
=> [{"three"=>"3"}]
How about something like that? just uniq_by the hash value or hash key via the block.