Uniq per attributo oggetto in Ruby
-
02-07-2019 - |
Domanda
Qual è il modo più elegante per selezionare oggetti in un array che sono unici rispetto a uno o più attributi?
Questi oggetti sono memorizzati in ActiveRecord, quindi anche i metodi di AR andrebbero bene.
Soluzione
Usa Array # uniq
con un blocco:
@photos = @photos.uniq { |p| p.album_id }
Altri suggerimenti
Aggiungi il metodo uniq_by
all'array nel tuo progetto. Funziona per analogia con sort_by
. Quindi uniq_by
significa uniq
come sort_by
è sort
. Uso:
uniq_array = my_array.uniq_by {|obj| obj.id}
L'implementazione:
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 atto. Non abbiamo scritto un metodo uniq_by!
ma dovrebbe essere abbastanza semplice se lo desideri.
EDIT: Tribalvibes sottolinea che l'implementazione è O (n ^ 2). Meglio sarebbe qualcosa di simile (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
Fallo a livello di database:
YourModel.find(:all, :group => "status")
Puoi usare questo trucco per selezionare univoco per diversi elementi di attributi dall'array:
@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
Inizialmente avevo suggerito di utilizzare il metodo select
su Array. In parole povere:
[1, 2, 3, 4, 5, 6, 7] .select {| e | e% 2 == 0}
ci restituisce [2,4,6]
.
Ma se vuoi il primo oggetto del genere, usa detect
.
[1, 2, 3, 4, 5, 6, 7] .detect {| e | e > 3}
ci fornisce 4
.
Non sono sicuro di cosa tu voglia fare qui, tuttavia.
Mi piace l'uso da parte di jmah di un hash 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
Se capisco correttamente la tua domanda, ho affrontato questo problema usando l'approccio quasi disordinato del confronto degli oggetti Marshaled per determinare se alcuni attributi variano. L'iniezione alla fine del seguente codice 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
Il modo più elegante che ho trovato è uno spin-off usando Array # uniq
con un blocco
enumerable_collection.uniq(&:property)
... anche meglio!
Puoi usare un hash, che contiene un solo valore per ogni chiave:
Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values
Rails ha anche un metodo #uniq_by - vedi Array con parametri # uniq (ie, uniq_by)
Mi piacciono le risposte di jmah e Head. Ma mantengono l'ordine dell'array? Potrebbero essere nelle versioni successive di ruby ??poiché ci sono stati alcuni requisiti di conservazione dell'ordine di inserimento dell'hash scritti nelle specifiche del linguaggio, ma ecco una soluzione simile che mi piace usare che conserva l'ordine indipendentemente.
h = Set.new
objs.select{|el| h.add?(el.attr)}
Implementazione di ActiveSupport:
def uniq_by
hash, array = {}, []
each { |i| hash[yield(i)] ||= (array << i) }
array
end
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 unico attributo 1, ma la stessa cosa può essere fatta con un ordinamento lessicografico ...