2008-09-20 16 views
100

¿Cuál es la manera más elegante de seleccionar objetos en una matriz que son únicos con respecto a uno o más atributos?Uniq por atributo de objeto en Ruby

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

Respuesta

156

Uso Array#uniq con un bloque: aplicación

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

Esta es la respuesta correcta para [ruby 1.9] (http://ruby-doc.org/core-1.9.2/Array.html#method-i-uniq) y versiones posteriores. – nurettin

+2

+1. Y para los primeros Rubies, siempre hay 'require 'backports'' :-) –

+0

El método hash es mejor si quieres agrupar por decir album_id mientras (digamos) sumando num_plays. – thekingoftruth

6

Originalmente sugerí usar el método select en Array. A saber:

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

Pero si quiere el primer objeto, use detect.

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

No estoy seguro de a qué te refieres aquí.

+0

+1 Para el método de detección, nunca se sabe de eso. – pierrotlefou

3

Si entiendo su pregunta correctamente, he abordado este problema utilizando el enfoque casi hacky de comparar los objetos Marshaled para determinar si los atributos varían. La inyección al final del código siguiente 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 
0

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

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 un atributo 1 único, pero lo mismo se puede hacer w/tipo lexicográfico ...

13

hacerlo en el nivel de base de datos:

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

y ¿qué pasa si se trata de más de un campo, por interés? –

2

Se puede utilizar un hash, que tiene un único valor para cada clave:

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

Añadir el método uniq_by a la matriz en su proyecto. Funciona por analogía con sort_by. Entonces uniq_by es a uniq como sort_by es a sort. Uso:

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

La aplicació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 

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

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 
+1

Buena API, pero tendrá un rendimiento de escalado pobre (se ve como O (n^2)) para arreglos grandes. Podría arreglarse haciendo que las transformaciones sean un hashset. – tribalvibes

+6

Esta respuesta no está actualizada. Ruby> = 1.9 tiene Array # uniq con un bloque que hace exactamente esto, como en la respuesta aceptada. –

5

me gusta el uso de JMAH de un hash para hacer cumplir la unicidad.Aquí hay un par de maneras de pelar el gato:

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

Eso es un bonito 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 
1

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

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

ActiveSupport:

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

Puede utilizar este truco para seleccionar único por varios elementos de atributos de tabla:

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

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

enumerable_collection.uniq(&:property) 

... se lee mejor también!

Cuestiones relacionadas