Uniq за атрибутом об'єкта в Ruby


126

Який найелегантніший спосіб виділити об'єкти з масиву, які є унікальними щодо одного або декількох атрибутів?

Ці об'єкти зберігаються в ActiveRecord, тому використання методів AR також буде добре.

Відповіді:


200

Використовувати Array#uniqз блоком:

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

5
Це правильна відповідь для версій ruby 1.9 та новіших версій.
Нуреттін

2
+1. А для попередніх Рубій завжди є 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 вам потрібно прочитати трохи нижче цього ж: 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!метод, але він повинен бути досить легким, якщо ви цього хочете.

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

1
Хороший api, але це буде мати низьку (схожа на O (n ^ 2)) ефективність масштабування для великих масивів. Виправити це можна, зробивши перетворення хештету.
tribalvibes

7
Ця відповідь застаріла. У Ruby> = 1.9 є масив # uniq з блоком, який робить саме це, як у прийнятій відповіді.
Пітер Х. Боллінг


12

Ви можете використовувати цей трюк для вибору унікальних за кількома елементами атрибутів з масиву:

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

так очевидно, так Рубі. Ще один привід благословити Рубі
ToTenMilan

6

Я спочатку запропонував використовувати select метод на Array. А саме:

[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

Мені подобається використання хеш-джима для досягнення унікальності. Ось ще кілька способів зняти кота:

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

Це хороший 1-лайнер, але я підозрюю, що це може бути трохи швидше:

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

3

Якщо я правильно розумію ваше запитання, я вирішив цю проблему, використовуючи квазі-хакі-підхід порівняння об'єктів Маршальованих, щоб визначити, чи змінюються якісь атрибути. Ін'єктом в кінці наступного коду буде приклад:

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)

… Воно також читається краще!


2

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

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



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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.