Uniq по объектному атрибуту в Ruby
каков наиболее элегантный способ выбора объектов в массиве, которые уникальны по отношению к одному или нескольким атрибутам?
эти объекты хранятся в ActiveRecord, поэтому использование методов AR тоже было бы хорошо.
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
Я изначально предложил использовать
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, но то же самое можно сделать с лексикографической сортировкой ...