Uniq nach Objektattribut in Ruby

126

Was ist die eleganteste Methode, um Objekte in einem Array auszuwählen, die in Bezug auf ein oder mehrere Attribute eindeutig sind?

Diese Objekte werden in ActiveRecord gespeichert, daher ist die Verwendung der AR-Methoden ebenfalls in Ordnung.

sutee
quelle

Antworten:

200

Verwendung Array#uniqmit einem Block:

@photos = @photos.uniq { |p| p.album_id }
Fahrbahn
quelle
5
Dies ist die richtige Antwort für Ruby 1.9 und spätere Versionen.
Nurettin
2
+1. Und für frühere Rubine gibt es immer require 'backports':-)
Marc-André Lafortune
Die Hash-Methode ist besser, wenn Sie nach album_id gruppieren möchten, während Sie num_plays zusammenfassen.
thekingoftruth
20
Sie können es mit einem to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ) verbessern :@photos.uniq &:album_id
joaomilho
@brauliobo für Ruby 1.8 müssen Sie nur unten in der gleichen SO lesen: stackoverflow.com/a/113770/213191
Peter H. Boling
22

Fügen Sie die uniq_byMethode Array in Ihrem Projekt hinzu. Es funktioniert analog zu sort_by. So uniq_byist es uniqwie es sort_byist sort. Verwendung:

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

Die Umsetzung:

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

Beachten Sie, dass ein neues Array zurückgegeben wird, anstatt das aktuelle Array zu ändern. Wir haben keine uniq_by!Methode geschrieben , aber es sollte einfach genug sein, wenn Sie wollten.

EDIT: Tribalvibes weist darauf hin, dass diese Implementierung O (n ^ 2) ist. Besser wäre so etwas wie (ungetestet) ...

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
quelle
1
Schöne API, aber das wird eine schlechte Skalierungsleistung (sieht aus wie O (n ^ 2)) für große Arrays haben. Könnte behoben werden, indem Transformationen zu einem Hashset gemacht werden.
Tribalvibes
7
Diese Antwort ist veraltet. Ruby> = 1.9 hat Array # uniq mit einem Block, der genau dies tut, wie in der akzeptierten Antwort.
Peter H. Boling
17

Tun Sie es auf Datenbankebene:

YourModel.find(:all, :group => "status")
mislav
quelle
1
und was wäre, wenn es mehr als ein Feld wäre, aus Interesse?
Ryan Bigg
12

Mit diesem Trick können Sie eindeutige Elemente mit mehreren Attributen aus dem Array auswählen:

@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
YauheniNinja
quelle
so offensichtlich, so Ruby. Nur ein weiterer Grund, Ruby zu segnen
ToTenMilan
6

Ich hatte ursprünglich vorgeschlagen, die selectMethode auf Array zu verwenden. Nämlich:

[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0} gibt uns [2,4,6]zurück.

Wenn Sie jedoch das erste derartige Objekt möchten, verwenden Sie detect.

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

Ich bin mir allerdings nicht sicher, was Sie hier vorhaben.

Alex M.
quelle
5

Ich mag es, wenn jmah einen Hash verwendet, um die Einzigartigkeit durchzusetzen. Hier sind noch ein paar Möglichkeiten, diese Katze zu häuten:

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

Das ist ein schöner 1-Liner, aber ich vermute, dass dies etwas schneller sein könnte:

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

Wenn ich Ihre Frage richtig verstehe, habe ich dieses Problem mithilfe des quasi-hackigen Ansatzes gelöst, bei dem die Marshaled-Objekte verglichen werden, um festzustellen, ob Attribute variieren. Die Injektion am Ende des folgenden Codes wäre ein Beispiel:

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
quelle
3

Der eleganteste Weg, den ich gefunden habe, ist ein Spin-off Array#uniqmit einem Block

enumerable_collection.uniq(&:property)

… Es liest sich auch besser!

iGbanam
quelle
2

Sie können einen Hash verwenden, der nur einen Wert für jeden Schlüssel enthält:

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

Verwenden Sie Array # uniq mit einem Block:

objects.uniq {|obj| obj.attribute}

Oder ein prägnanterer Ansatz:

objects.uniq(&:attribute)
Muhamad Najjar
quelle
1

Ich mag die Antworten von jmah und Head. Aber behalten sie die Array-Reihenfolge bei? Sie könnten in späteren Versionen von Ruby verwendet werden, da einige Anforderungen zur Beibehaltung der Reihenfolge der Hash-Einfügung in die Sprachspezifikation geschrieben wurden, aber hier ist eine ähnliche Lösung, die ich gerne verwende und die die Reihenfolge unabhängig davon beibehält.

h = Set.new
objs.select{|el| h.add?(el.attr)}
TKH
quelle
1

ActiveSupport-Implementierung:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end
gröber
quelle
0

Wenn Sie nun nach den Attributwerten sortieren können, können Sie Folgendes tun:

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

Das ist für ein 1-Attribut einzigartig, aber das gleiche kann mit lexikographischer Sortierung gemacht werden ...

Purfideas
quelle