Какой самый элегантный способ выделить объекты в массиве, уникальные по одному или нескольким атрибутам?
Эти объекты хранятся в ActiveRecord, поэтому можно использовать методы AR.
Какой самый элегантный способ выделить объекты в массиве, уникальные по одному или нескольким атрибутам?
Эти объекты хранятся в ActiveRecord, поэтому можно использовать методы AR.
Ответы:
Используйте Array#uniq
с блоком:
@photos = @photos.uniq { |p| p.album_id }
require 'backports'
:-)
@photos.uniq &: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!
метод, но он должен быть достаточно простым, если вы захотите.
РЕДАКТИРОВАТЬ: 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")
Вы можете использовать этот трюк для выбора уникальных по нескольким атрибутам элементов из массива:
@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
Первоначально я предлагал использовать 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
Это хороший однострочник, но я подозреваю, что это может быть немного быстрее:
h = {}
objs.each {|e| h[e.attr]=e}
h.values
Если я правильно понимаю ваш вопрос, я решил эту проблему, используя квази-хакерский подход, заключающийся в сравнении объектов Marshaled, чтобы определить, изменяются ли какие-либо атрибуты. Примером может служить инъекция в конце следующего кода:
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
Самый элегантный способ, который я нашел, - это спин-офф Array#uniq
с использованием блока
enumerable_collection.uniq(&:property)
… Это тоже лучше читается!
Используйте Array # uniq с блоком:
objects.uniq {|obj| obj.attribute}
Или более лаконичный подход:
objects.uniq(&:attribute)
Rails также имеет #uniq_by
метод.
Мне нравятся ответы jmah и Head. Но сохраняют ли они порядок в массиве? Они могут быть в более поздних версиях ruby, поскольку в спецификации языка были прописаны некоторые требования к сохранению порядка вставки хэшей, но вот аналогичное решение, которое я люблю использовать, которое сохраняет порядок независимо.
h = Set.new
objs.select{|el| h.add?(el.attr)}
Теперь, если вы можете отсортировать значения атрибутов, это можно сделать:
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-атрибута, но то же самое можно сделать с лексикографической сортировкой ...