Uniq por atributo de objeto en Ruby

126

¿Cuál es la forma más elegante de seleccionar objetos en una matriz que sean únicos con respecto a uno o más atributos?

Estos objetos se almacenan en ActiveRecord, por lo que usar los métodos de AR también estaría bien.

sutee
fuente

Respuestas:

200

Usar Array#uniqcon un bloque:

@photos = @photos.uniq { |p| p.album_id }
carril
fuente
55
Esta es la respuesta correcta para ruby 1.9 y versiones posteriores.
nurettin
2
+1. Y para Rubíes anteriores, siempre hay require 'backports':-)
Marc-André Lafortune
El método hash es mejor si desea agrupar por say album_id mientras (say) resume num_plays.
thekingoftruth
20
Puede mejorarlo con un to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ):@photos.uniq &:album_id
joaomilho
@brauliobo para Ruby 1.8 necesita leer justo debajo en este mismo SO: stackoverflow.com/a/113770/213191
Peter H. Boling
22

Agregue el uniq_bymétodo a Array en su proyecto. Funciona por analogía con sort_by. Así uniq_byes uniqcomo sort_byes sort. Uso:

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

La implementación:

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

Tenga en cuenta que devuelve una nueva matriz en lugar de modificar la actual en su lugar. No hemos escrito un uniq_by!método, pero debería ser lo suficientemente fácil si quisieras.

EDITAR: Tribalvibes señala que esa implementación es O (n ^ 2). Mejor sería algo así (no probado) ...

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
fuente
1
Buena API, pero tendrá un rendimiento de escalado deficiente (parece O (n ^ 2)) para matrices grandes. Podría arreglarse transformando un hashset.
tribalvibes
77
Esta respuesta está desactualizada. Ruby> = 1.9 tiene Array # uniq con un bloque que hace exactamente esto, como en la respuesta aceptada.
Peter H. Boling
17

Hazlo en el nivel de la base de datos:

YourModel.find(:all, :group => "status")
mislav
fuente
1
¿Y si fuera más de un campo, fuera de interés?
Ryan Bigg
12

Puede usar este truco para seleccionar elementos únicos de varios atributos de la matriz:

@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
YauheniNinja
fuente
tan obvio, tan rubí. Solo otra razón para bendecir a Ruby
ToTenMilan
6

Originalmente había sugerido usar el selectmétodo en Array. Esto es:

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

Pero si quieres el primer objeto de este tipo, úsalo detect.

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

Sin embargo, no estoy seguro de lo que vas a hacer aquí.

Alex M
fuente
5

Me gusta el uso que hace Jmah de un Hash para imponer la singularidad. Aquí hay un par de formas más de pelar a ese gato:

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

Es un buen 1-liner, pero sospecho que esto podría ser un poco más rápido:

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

Si entiendo su pregunta correctamente, he abordado este problema utilizando el enfoque casi hacky de comparar los objetos Marshaled para determinar si algunos atributos varían. La inyección al final del siguiente código sería un ejemplo:

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

La forma más elegante que he encontrado es un spin-off usando Array#uniqun bloque

enumerable_collection.uniq(&:property)

... se lee mejor también!

iGbanam
fuente
2

Puede usar un hash, que contiene solo un valor para cada clave:

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

Use Array # uniq con un bloque:

objects.uniq {|obj| obj.attribute}

O un enfoque más conciso:

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

Me gustan las respuestas de JMAH y Head. ¿Pero conservan el orden de la matriz? Es posible que en versiones posteriores de ruby, ya que ha habido algunos requisitos de preservación del orden de inserción hash escritos en la especificación del lenguaje, pero aquí hay una solución similar que me gusta usar que independientemente conserva el orden.

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

Implementación de ActiveSupport:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end
más asqueroso
fuente
0

Ahora, si puede ordenar los valores de los atributos, esto se puede hacer:

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

Eso es para un atributo 1 único, pero se puede hacer lo mismo con el tipo lexicográfico ...

Purfideas
fuente