Uniq по объектному атрибуту в Ruby


каков наиболее элегантный способ выбора объектов в массиве, которые уникальны по отношению к одному или нескольким атрибутам?

эти объекты хранятся в ActiveRecord, поэтому использование методов AR тоже было бы хорошо.

13 104

13 ответов:

использовать Array#uniq блок:

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

добавить uniq_by метод выбора в вашем проекте. Он работает по аналогии с sort_by. Так что uniq_by - это uniq как sort_by это sort. Использование:

uniq_array = my_array.uniq_by {|obj| obj.id}

реализация:

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

обратите внимание, что он возвращает новый массив, а не изменять текущий на месте. Мы еще не написали uniq_by! метод, но это должно быть достаточно легко, если вы хотите.

EDIT: Tribalvibes указывает, что эта реализация является O(n^2). Лучше бы что-то вроде (непроверенных)...

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

сделать это на уровне базы данных:

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

Я изначально предложил использовать select метод на массив. А именно:

[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0} дает нам [2,4,6] обратно.

но если вы хотите первый такой объект, используйте detect.

[1, 2, 3, 4, 5, 6, 7].detect{|e| e>3} дает 4.

Я не уверен, что вы собираетесь здесь, хотя.

мне нравится использовать jmah по хэш-функций для обеспечения уникальности. Вот еще несколько способов освежевать эту кошку:

objs.inject({}) {|h,e| h[e.attr]=e; h}.values

это хороший 1-лайнер, но я подозреваю, что это может быть немного быстрее:

h = {}
objs.each {|e| h[e.attr]=e}
h.values

вы можете использовать этот трюк, чтобы выбирать несколько элементов из массива атрибутов:

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

Если я правильно понял ваш вопрос, я решил эту проблему, используя квази-хакерский подход сравнения Маршалированных объектов, чтобы определить, различаются ли какие-либо атрибуты. Инъекция в конце следующего кода будет примером:

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

вы можете использовать хеш-код, который содержит только одно значение для каждого ключа:

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

Rails также имеет метод #uniq_by-см. параметризованный массив#uniq (т. е. uniq_by)

Мне нравятся ответы jmah и Head. Но сохраняют ли они порядок массива? Они могут быть в более поздних версиях ruby, поскольку в спецификацию языка были записаны некоторые требования к сохранению порядка вставки хэша, но вот аналогичное решение, которое мне нравится использовать, которое сохраняет порядок независимо.

h = Set.new
objs.select{|el| h.add?(el.attr)}

реализация ActiveSupport:

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

самый элегантный способ я нашел-это спин-офф с помощью Array#uniq в блоке

enumerable_collection.uniq(&:property)

...он тоже читает лучше!

теперь, если вы можете сортировать по значениям атрибутов, это можно сделать:

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

Это для уникального атрибута 1, но то же самое можно сделать с лексикографической сортировкой ...