views:

1321

answers:

8

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.

+3  A: 

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.

Pi
+1 For detect method ,never aware of that.
pierr
+2  A: 

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
Drew Olson
A: 

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 ...

Purfideas
+8  A: 

Do it on the database level:

YourModel.find(:all, :group => "status")
mislav
and what if it was more than one field, out of interest?
Ryan Bigg
+1  A: 

You can use a hash, which contains only one value for each key:

Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values
jmah
+5  A: 

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
Daniel Lucraft
Nice api but that's going to have poor (looks like O(n^2)) scaling performance for large arrays. Could be fixed by making transforms a hashset.
tribalvibes
+1  A: 

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
Head
A: 

delete this post

Tal