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

127

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

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

остаться, таких
источник

Ответы:

202

Используйте Array#uniqс блоком:

@photos = @photos.uniq { |p| p.album_id }
полоса дороги
источник
5
Это правильный ответ для Ruby 1.9 и более поздних версий.
nurettin
2
+1. А для более ранних Rubies всегда есть require 'backports':-)
Марк-Андре Лафортюн
Метод хеширования лучше, если вы хотите сгруппировать, скажем, album_id, при (скажем) суммируя num_plays.
thekingoftruth
20
Вы можете улучшить его с помощью to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ):@photos.uniq &:album_id
joaomilho
@brauliobo для Ruby 1.8, вам нужно прочитать чуть ниже в этом же SO: stackoverflow.com/a/113770/213191
Питер Х. Болинг,
22

Добавьте 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
Дэниел Люкрафт
источник
1
Хороший api, но у него будет плохая (похоже, O (n ^ 2)) производительность масштабирования для больших массивов. Может быть исправлено путем преобразования хеш-набора.
tribalvibes
7
Этот ответ устарел. Ruby> = 1.9 имеет Array # uniq с блоком, который делает именно это, как в принятом ответе.
Питер Х. Болинг,
17

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

YourModel.find(:all, :group => "status")
mislav
источник
1
а что, если это было несколько полей, из интереса?
Райан Бигг,
12

Вы можете использовать этот трюк для выбора уникальных по нескольким атрибутам элементов из массива:

@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
YauheniNinja
источник
так очевидно, так Руби. Еще одна причина благословить Руби
ToTenMilan
6

Первоначально я предлагал использовать 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 .

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

Алекс М
источник
5

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

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

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

h = {}
objs.each {|e| h[e.attr]=e}
h.values
Глава
источник
3

Если я правильно понимаю ваш вопрос, я решил эту проблему, используя квази-хакерский подход, заключающийся в сравнении объектов 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
Дрю Олсон
источник
3

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

enumerable_collection.uniq(&:property)

… Это тоже лучше читается!

iGbanam
источник
2

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

Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values
jmah
источник
2

Используйте Array # uniq с блоком:

objects.uniq {|obj| obj.attribute}

Или более лаконичный подход:

objects.uniq(&:attribute)
Мухамад Наджар
источник
1

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

h = Set.new
objs.select{|el| h.add?(el.attr)}
ТКН
источник
1

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

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end
грубее
источник
0

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

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-атрибута, но то же самое можно сделать с лексикографической сортировкой ...

Purfideas
источник