Reputation: 12828
What's the most elegant way to select out objects in an array that are unique with respect to one or more attributes?
These objects are stored in ActiveRecord so using AR's methods would be fine too.
Upvotes: 144
Views: 73157
Reputation: 585
If you are not married with arrays, we can also try eliminating duplicates through sets
set = Set.new
set << obj1
set << obj2
set.inspect
Note that in case of custom objects, we need to override eql?
and hash
methods
Upvotes: 0
Reputation: 3370
Rails also has a #uniq_by
method.
Reference: Parameterized Array#uniq (i.e., uniq_by)
Upvotes: 2
Reputation: 106
Use Array#uniq with a block:
objects.uniq {|obj| obj.attribute}
Or a more concise approach:
objects.uniq(&:attribute)
Upvotes: 7
Reputation: 6082
The most elegant way I have found is a spin-off using Array#uniq
with a block
enumerable_collection.uniq(&:property)
…it reads better too!
Upvotes: 4
Reputation: 308
You can use this trick to select unique by several attributes elements from array:
@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
Upvotes: 17
Reputation: 4986
Use Array#uniq
with a block:
@photos = @photos.uniq { |p| p.album_id }
Upvotes: 229
Reputation: 15097
ActiveSupport implementation:
def uniq_by
hash, array = {}, []
each { |i| hash[yield(i)] ||= (array << i) }
array
end
Upvotes: 1
Reputation: 838
I like jmah and Head's answers. But do they preserve array order? They might in later versions of ruby since there have been some hash insertion-order-preserving requirements written into the language specification, but here's a similar solution that I like to use that preserves order regardless.
h = Set.new
objs.select{|el| h.add?(el.attr)}
Upvotes: 1
Reputation: 7356
Add the uniq_by
method to Array in your project. It works by analogy with sort_by
. So uniq_by
is to uniq
as sort_by
is to sort
. Usage:
uniq_array = my_array.uniq_by {|obj| obj.id}
The implementation:
class Array
def uniq_by(&blk)
transforms = []
self.select do |el|
should_keep = !transforms.include?(t=blk[el])
transforms << t
should_keep
end
end
end
Note that it returns a new array rather than modifying your current one in place. We haven't written a uniq_by!
method but it should be easy enough if you wanted to.
EDIT: Tribalvibes points out that that implementation is O(n^2). Better would be something like (untested)...
class Array
def uniq_by(&blk)
transforms = {}
select do |el|
t = blk[el]
should_keep = !transforms[t]
transforms[t] = true
should_keep
end
end
end
Upvotes: 22
Reputation: 4761
I like jmah's use of a Hash to enforce uniqueness. Here's a couple more ways to skin that cat:
objs.inject({}) {|h,e| h[e.attr]=e; h}.values
That's a nice 1-liner, but I suspect this might be a little faster:
h = {}
objs.each {|e| h[e.attr]=e}
h.values
Upvotes: 5
Reputation: 2214
You can use a hash, which contains only one value for each key:
Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values
Upvotes: 2
Reputation: 15209
Do it on the database level:
YourModel.find(:all, :group => "status")
Upvotes: 17
Reputation: 3288
Now if you can sort on the attribute values this can be done:
class A
attr_accessor :val
def initialize(v); self.val = v; end
end
objs = [1,2,6,3,7,7,8,2,8].map{|i| A.new(i)}
objs.sort_by{|a| a.val}.inject([]) do |uniqs, a|
uniqs << a if uniqs.empty? || a.val != uniqs.last.val
uniqs
end
That's for a 1-attribute unique, but the same thing can be done w/ lexicographical sort ...
Upvotes: 0
Reputation: 3737
If I understand your question correctly, I've tackled this problem using the quasi-hacky approach of comparing the Marshaled objects to determine if any attributes vary. The inject at the end of the following code would be an example:
class Foo
attr_accessor :foo, :bar, :baz
def initialize(foo,bar,baz)
@foo = foo
@bar = bar
@baz = baz
end
end
objs = [Foo.new(1,2,3),Foo.new(1,2,3),Foo.new(2,3,4)]
# find objects that are uniq with respect to attributes
objs.inject([]) do |uniqs,obj|
if uniqs.all? { |e| Marshal.dump(e) != Marshal.dump(obj) }
uniqs << obj
end
uniqs
end
Upvotes: 3
Reputation: 2488
I had originally suggested using the select
method on Array. To wit:
[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0}
gives us [2,4,6]
back.
But if you want the first such object, use detect
.
[1, 2, 3, 4, 5, 6, 7].detect{|e| e>3}
gives us 4
.
I'm not sure what you're going for here, though.
Upvotes: 6