Pregunta

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

¿Fue útil?

Solución

Usar Array#uniq con un bloque:

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

Otros consejos

Añade el uniq_by método para Array en su proyecto.Funciona por analogía con sort_by.Entonces uniq_by Es para uniq como sort_by Es para 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.No hemos escrito un uniq_by! método, pero debería ser bastante fácil si así lo deseas.

EDITAR:Tribalvibes señala que esa implementación es O(n^2).Mejor sería algo como (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

Hazlo en el nivel de la base de datos:

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

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

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

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

[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0}Nos da [2,4,6] atrás.

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

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

Aunque no estoy seguro de a qué te refieres aquí.

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

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

Esa es una buena frase, pero sospecho que esto podría ser un poco más rápido:

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

Si entiendo su pregunta correctamente, he abordado este problema utilizando el enfoque cuasi-hacky de comparar los objetos clasificados para determinar si algún atributo varía.La inserció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

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

enumerable_collection.uniq(&:property)

…¡también se lee mejor!

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

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

Rails también tiene un método #uniq_by - ver Matriz parametrizada#uniq (es decir, uniq_by)

Me gustan las respuestas de jmah y Head.¿Pero conservan el orden de la matriz?Es posible que lo hagan en versiones posteriores de Ruby, ya que se han escrito algunos requisitos de preservación del orden de inserción de hash en la especificación del lenguaje, pero aquí hay una solución similar que me gusta usar y que preserva el orden independientemente.

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

Implementación de ActiveSupport:

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

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 único, pero se puede hacer lo mismo con la clasificación lexicográfica...

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top