Uniq by object attribute in Ruby

Ruby on-RailsRubyArraysUnique

Ruby on-Rails Problem Overview


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.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Use Array#uniq with a block:

@photos = @photos.uniq { |p| p.album_id }

Solution 2 - Ruby on-Rails

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

Solution 3 - Ruby on-Rails

Do it on the database level:

YourModel.find(:all, :group => "status")

Solution 4 - Ruby on-Rails

You can use this trick to select unique by several attributes elements from array:

@photos = @photos.uniq { |p| [p.album_id, p.author_id] }

Solution 5 - Ruby on-Rails

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.

Solution 6 - Ruby on-Rails

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

Solution 7 - Ruby on-Rails

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!

Solution 8 - Ruby on-Rails

Use Array#uniq with a block:

objects.uniq {|obj| obj.attribute}

Or a more concise approach:

objects.uniq(&:attribute)

Solution 9 - Ruby on-Rails

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

Solution 10 - Ruby on-Rails

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

Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values

Solution 11 - Ruby on-Rails

Solution 12 - Ruby on-Rails

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)}

Solution 13 - Ruby on-Rails

ActiveSupport implementation:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end

Solution 14 - Ruby on-Rails

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

Solution 15 - Ruby on-Rails

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsuteeView Question on Stackoverflow
Solution 1 - Ruby on-RailsLaneView Answer on Stackoverflow
Solution 2 - Ruby on-RailsDaniel LucraftView Answer on Stackoverflow
Solution 3 - Ruby on-RailsmislavView Answer on Stackoverflow
Solution 4 - Ruby on-RailsyauhenininjiaView Answer on Stackoverflow
Solution 5 - Ruby on-RailsAlex MView Answer on Stackoverflow
Solution 6 - Ruby on-RailsHeadView Answer on Stackoverflow
Solution 7 - Ruby on-RailsIgbanamView Answer on Stackoverflow
Solution 8 - Ruby on-Rails7modeView Answer on Stackoverflow
Solution 9 - Ruby on-RailsDrew OlsonView Answer on Stackoverflow
Solution 10 - Ruby on-RailsjmahView Answer on Stackoverflow
Solution 11 - Ruby on-RailsapbView Answer on Stackoverflow
Solution 12 - Ruby on-RailsTKHView Answer on Stackoverflow
Solution 13 - Ruby on-RailsgrosserView Answer on Stackoverflow
Solution 14 - Ruby on-RailsPurfideasView Answer on Stackoverflow
Solution 15 - Ruby on-RailsVasanth SaminathanView Answer on Stackoverflow