Uniq per attributo oggetto in Ruby


127

Qual è il modo più elegante per selezionare gli oggetti in un array che sono unici rispetto a uno o più attributi?

Questi oggetti sono memorizzati in ActiveRecord, quindi anche l'utilizzo dei metodi di AR andrebbe bene.

Risposte:


201

Utilizzare Array#uniqcon un blocco:

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

5
Questa è la risposta corretta per ruby 1.9 e versioni successive.
nurettin

2
+1. E per i precedenti Rubies, c'è sempre require 'backports':-)
Marc-André Lafortune

Il metodo hash è migliore se vuoi raggruppare per dire album_id mentre (dire) sommare num_plays.
thekingoftruth

20
Puoi migliorarlo con to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ):@photos.uniq &:album_id
joaomilho

@brauliobo per Ruby 1.8 devi leggere appena sotto in questo stesso SO: stackoverflow.com/a/113770/213191
Peter H. Boling

22

Aggiungi il uniq_bymetodo ad Array nel tuo progetto. Funziona per analogia con sort_by. Così uniq_byè uniqcome sort_byè sort. Uso:

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

L'implemento:

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

Nota che restituisce un nuovo array invece di modificare quello attuale in posizione. Non abbiamo scritto un uniq_by!metodo ma dovrebbe essere abbastanza facile se lo desideri.

EDIT: Tribalvibes sottolinea che tale implementazione è O (n ^ 2). Sarebbe meglio qualcosa come (non testato) ...

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
Bella api ma avrà prestazioni di ridimensionamento scarse (sembra O (n ^ 2)) per array di grandi dimensioni. Potrebbe essere risolto rendendo le trasformazioni un hashset.
tribalvibes

7
Questa risposta non è aggiornata. Ruby> = 1.9 ha Array # uniq con un blocco che fa esattamente questo, come nella risposta accettata.
Peter H. Boling

17

Fallo a livello di database:

YourModel.find(:all, :group => "status")

1
e se fosse più di un campo, per interesse?
Ryan Bigg,

12

Puoi usare questo trucco per selezionare univoco da diversi elementi di attributi dall'array:

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

così ovvio, così Ruby. Solo un altro motivo per benedire Ruby
ToTenMilan

6

Inizialmente avevo suggerito di utilizzare il selectmetodo su Array. Vale a dire:

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

Ma se vuoi il primo oggetto del genere, usa detect.

[1, 2, 3, 4, 5, 6, 7].detect{|e| e>3}ci dà 4.

Non sono sicuro di cosa stai cercando qui, però.


5

Mi piace l'uso di un hash da parte di jmah per rafforzare l'unicità. Ecco un altro paio di modi per scuoiare quel gatto:

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

È un bel 1-liner, ma sospetto che potrebbe essere un po 'più veloce:

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

3

Se capisco correttamente la tua domanda, ho affrontato questo problema utilizzando l'approccio quasi hacker di confrontare gli oggetti Marshaled per determinare se qualche attributo varia. L'inject alla fine del codice seguente sarebbe un esempio:

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

Il modo più elegante che ho trovato è uno spin-off che utilizza Array#uniqcon un blocco

enumerable_collection.uniq(&:property)

... si legge anche meglio!


2

Puoi utilizzare un hash, che contiene un solo valore per ogni chiave:

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



1

Mi piacciono le risposte di Jmah e Head. Ma preservano l'ordine degli array? Potrebbero nelle versioni successive di ruby ​​poiché ci sono stati alcuni requisiti di conservazione dell'ordine di inserimento hash scritti nelle specifiche del linguaggio, ma ecco una soluzione simile che mi piace usare che preserva l'ordine a prescindere.

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

1

Implementazione di ActiveSupport:

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

0

Ora, se puoi ordinare i valori degli attributi, puoi farlo:

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

Questo è per un attributo unico, ma la stessa cosa può essere fatta con l'ordinamento lessicografico ...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.