Uniq por atributo de objeto em Ruby


126

Qual é a maneira mais elegante de selecionar objetos em uma matriz exclusivos em relação a um ou mais atributos?

Esses objetos são armazenados no ActiveRecord, portanto, o uso dos métodos de AR também seria bom.

Respostas:


200

Use Array#uniqcom um bloco:

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

5
Esta é a resposta correta para o ruby 1.9 e versões posteriores.
nurettin

2
+1. E para Rubies anteriores, há sempre require 'backports':-)
Marc-André Lafortune

O método hash é melhor se você deseja agrupar digitando album_id enquanto (digamos) resumindo num_plays.
thekingoftruth 13/09

20
Você pode aprimorá-lo com um to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ):@photos.uniq &:album_id
joaomilho 10/13/13

@brauliobo para Ruby 1.8 você precisa ler logo abaixo neste mesmo SO: stackoverflow.com/a/113770/213191
Peter H. Boling

22

Adicione o uniq_bymétodo à matriz em seu projeto. Funciona por analogia com sort_by. Então uniq_byé uniqcomo sort_byé sort. Uso:

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

A implementação:

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 ele retorna uma nova matriz em vez de modificar a atual. Não escrevemos um uniq_by!método, mas deve ser fácil o suficiente, se você quiser.

EDIT: Tribalvibes indica que essa implementação é O (n ^ 2). Melhor seria algo como (não testado) ...

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
API agradável, mas isso terá um desempenho de dimensionamento ruim (parece O (n ^ 2)) para matrizes grandes. Pode ser corrigido transformando um hashset.
Tribalvibes 27/10/10

7
Esta resposta está desatualizada. Ruby> = 1.9 possui a Matriz # uniq com um bloco que faz exatamente isso, como na resposta aceita.
Peter H. Boling

17

Faça no nível do banco de dados:

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

1
e se fosse mais de um campo, por interesse?
Ryan Bigg

12

Você pode usar esse truque para selecionar exclusivo por vários elementos de atributos da matriz:

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

tão óbvio, tão Ruby. Apenas uma outra razão para abençoar Rubi
ToTenMilan

6

Originalmente, eu sugeri o uso do selectmétodo Array. A saber:

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

Mas se você quiser o primeiro objeto desse tipo, use detect.

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

Não tenho certeza do que você está procurando aqui, no entanto.


5

Gosto do uso de um Hash por jmah para reforçar a exclusividade. Aqui estão mais algumas maneiras de esfolar esse gato:

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

Esse é um bom liner, mas suspeito que isso possa ser um pouco mais rápido:

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

3

Se entendi sua pergunta corretamente, resolvi esse problema usando a abordagem quase hacky de comparar os objetos Marshaled para determinar se algum atributo varia. A injeção no final do código a seguir seria um exemplo:

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

A maneira mais elegante que encontrei é um spin-off usando Array#uniqum bloco

enumerable_collection.uniq(&:property)

… Lê melhor também!


2

Você pode usar um hash, que contém apenas um valor para cada chave:

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



1

Eu gosto das respostas de jmah e Head. Mas eles preservam a ordem dos arrays? Eles podem estar em versões posteriores do ruby, pois existem alguns requisitos de preservação de ordem de inserção de hash gravados na especificação da linguagem, mas aqui está uma solução semelhante que eu gosto de usar que preserva a ordem independentemente.

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

1

Implementação do ActiveSupport:

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

0

Agora, se você pode classificar os valores dos atributos, isso pode ser feito:

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

Isso é exclusivo para um atributo, mas a mesma coisa pode ser feita com classificação lexicográfica ...

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.