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.

Antworten:


200

Verwendung Array#uniqmit einem Block:

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

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

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")

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] }

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.


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

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

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!


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



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)}

1

ActiveSupport-Implementierung:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end

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 ...

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.