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.

sutee
fonte

Respostas:

200

Use Array#uniqcom um bloco:

@photos = @photos.uniq { |p| p.album_id }
faixa
fonte
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
Daniel Lucraft
fonte
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")
mislav
fonte
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] }
YauheniNinja
fonte
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.

Alex M
fonte
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
Cabeça
fonte
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
Drew Olson
fonte
3

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

enumerable_collection.uniq(&:property)

… Lê melhor também!

iGbanam
fonte
2

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

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

Use a Matriz # uniq com um bloco:

objects.uniq {|obj| obj.attribute}

Ou uma abordagem mais concisa:

objects.uniq(&:attribute)
Muhamad Najjar
fonte
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)}
TKH
fonte
1

Implementação do ActiveSupport:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end
mais grosseiro
fonte
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 ...

Purfideas
fonte