Uniq według atrybutu obiektu w Rubim

127

Jaki jest najbardziej elegancki sposób wybierania obiektów w tablicy, które są unikalne pod względem jednego lub więcej atrybutów?

Te obiekty są przechowywane w ActiveRecord, więc użycie metod AR również byłoby w porządku.

sutee
źródło

Odpowiedzi:

201

Używaj Array#uniqz blokiem:

@photos = @photos.uniq { |p| p.album_id }
Pas ruchu
źródło
5
To jest poprawna odpowiedź dla Rubiego 1.9 i nowszych wersji.
nurettin
2
+1. A dla wcześniejszych Rubinów zawsze jest require 'backports':-)
Marc-André Lafortune
Metoda hash jest lepsza, jeśli chcesz grupować, powiedzmy album_id, podczas (powiedzmy) sumowania num_plays.
thekingoftruth
20
Możesz to ulepszyć za pomocą to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ):@photos.uniq &:album_id
joaomilho
@brauliobo dla Ruby 1.8, które musisz przeczytać tuż poniżej w tym samym SO: stackoverflow.com/a/113770/213191
Peter H. Boling
22

Dodaj uniq_bymetodę do Array w swoim projekcie. Działa przez analogię z sort_by. Tak uniq_byjest, uniqjak sort_byjest sort. Stosowanie:

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

Implementacja:

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

Zwróć uwagę, że zwraca on nową tablicę, zamiast modyfikować bieżącą w miejscu. Nie napisaliśmy uniq_by!metody, ale powinna być dość łatwa, jeśli chcesz.

EDYCJA: Tribalvibes zwraca uwagę, że ta implementacja to O (n ^ 2). Lepiej byłoby coś takiego (niesprawdzone) ...

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
Daniel Lucraft
źródło
1
Niezły interfejs API, ale będzie miał słabą (wygląda jak O (n ^ 2)) wydajność skalowania dla dużych tablic. Można to naprawić, przekształcając transformacje w skrót.
tribalvibes
7
Ta odpowiedź jest nieaktualna. Ruby> = 1.9 ma Array # uniq z blokiem, który robi dokładnie to, tak jak w zaakceptowanej odpowiedzi.
Peter H. Boling
17

Zrób to na poziomie bazy danych:

YourModel.find(:all, :group => "status")
mislav
źródło
1
a co by było, gdyby było to więcej niż jedno pole, które nie było interesujące?
Ryan Bigg,
12

Możesz użyć tej sztuczki, aby wybrać unikalne przez kilka atrybutów elementów z tablicy:

@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
YauheniNinja
źródło
tak oczywiste, więc Ruby. Kolejny powód, by błogosławić Ruby
ToTenMilan
6

Początkowo sugerowałem użycie selectmetody w Array. Dowcip:

[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0} oddaje nam [2,4,6].

Ale jeśli chcesz mieć pierwszy taki obiekt, użyj detect.

[1, 2, 3, 4, 5, 6, 7].detect{|e| e>3}daje nam 4.

Nie jestem jednak pewien, po co tu się wybierasz.

Alex M
źródło
5

Podoba mi się użycie skrótu w jmah do wymuszenia wyjątkowości. Oto kilka innych sposobów na oskórowanie tego kota:

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

To fajny 1-liniowiec, ale podejrzewam, że może być trochę szybszy:

h = {}
objs.each {|e| h[e.attr]=e}
h.values
Głowa
źródło
3

Jeśli dobrze rozumiem twoje pytanie, rozwiązałem ten problem, stosując quasi-hackowe podejście do porównywania obiektów Marshaled w celu ustalenia, czy jakiekolwiek atrybuty się różnią. Przykładem może być wstrzyknięcie na końcu następującego kodu:

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
Drew Olson
źródło
3

Najbardziej elegancki sposób, jaki znalazłem, to spin-off Array#uniqz blokiem

enumerable_collection.uniq(&:property)

… Też czyta się lepiej!

iGbanam
źródło
2

Możesz użyć skrótu, który zawiera tylko jedną wartość dla każdego klucza:

Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values
jmah
źródło
2

Użyj Array # uniq z blokiem:

objects.uniq {|obj| obj.attribute}

Lub bardziej zwięzłe podejście:

objects.uniq(&:attribute)
Muhamad Najjar
źródło
1

Lubię odpowiedzi jmah i Head. Ale czy zachowują porządek w tablicy? Mogą być w późniejszych wersjach ruby, ponieważ w specyfikacji języka zostały zapisane pewne wymagania dotyczące zachowania kolejności wstawiania skrótów, ale tutaj jest podobne rozwiązanie, którego lubię używać, które zachowuje porządek niezależnie.

h = Set.new
objs.select{|el| h.add?(el.attr)}
TKH
źródło
1

Wdrożenie ActiveSupport:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end
grubszy
źródło
0

Teraz, jeśli możesz sortować według wartości atrybutów, możesz to zrobić:

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

To dla unikatowego atrybutu 1, ale to samo można zrobić z sortowaniem leksykograficznym ...

Purfideas
źródło