Uniq par attribut d'objet dans Ruby

127

Quelle est la manière la plus élégante de sélectionner des objets dans un tableau qui sont uniques par rapport à un ou plusieurs attributs?

Ces objets sont stockés dans ActiveRecord, donc utiliser les méthodes d'AR serait également très bien.

sutee
la source

Réponses:

201

Utiliser Array#uniqavec un bloc:

@photos = @photos.uniq { |p| p.album_id }
voie
la source
5
C'est la bonne réponse pour ruby 1.9 et les versions ultérieures.
nurettin
2
+1. Et pour les Rubis plus tôt, il y a toujours require 'backports':-)
Marc-André Lafortune
La méthode de hachage est meilleure si vous souhaitez regrouper par exemple album_id tout en (par exemple) additionner num_plays.
thekingoftruth
20
Vous pouvez l'améliorer avec un to_proc ( ruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc ):@photos.uniq &:album_id
joaomilho
@brauliobo pour Ruby 1.8, vous devez lire juste ci-dessous dans ce même SO: stackoverflow.com/a/113770/213191
Peter H. Boling
22

Ajoutez la uniq_byméthode à Array dans votre projet. Cela fonctionne par analogie avec sort_by. Il en uniq_byva de même pour uniqtel sort_byquel sort. Usage:

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

La mise en oeuvre:

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

Notez qu'il renvoie un nouveau tableau plutôt que de modifier votre tableau actuel sur place. Nous n'avons pas écrit de uniq_by!méthode mais cela devrait être assez simple si vous le souhaitez.

EDIT: Tribalvibes souligne que cette implémentation est O (n ^ 2). Mieux vaut quelque chose comme (non testé) ...

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
la source
1
Nice api mais qui va avoir de mauvaises performances de mise à l'échelle (ressemble à O (n ^ 2)) pour les grands tableaux. Peut être corrigé en faisant des transformations un hashset.
tribalvibes
7
Cette réponse est périmée. Ruby> = 1.9 a Array # uniq avec un bloc qui fait exactement cela, comme dans la réponse acceptée.
Peter H.Boling
17

Faites-le au niveau de la base de données:

YourModel.find(:all, :group => "status")
mislav
la source
1
et si c'était plus d'un domaine, par intérêt?
Ryan Bigg
12

Vous pouvez utiliser cette astuce pour sélectionner unique par plusieurs éléments d'attributs du tableau:

@photos = @photos.uniq { |p| [p.album_id, p.author_id] }
YauheniNinja
la source
si évident, si Ruby. Juste une autre raison de bénir Ruby
ToTenMilan
6

J'avais initialement suggéré d'utiliser la selectméthode sur Array. En être témoin:

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

Mais si vous voulez le premier objet de ce type, utilisez detect.

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

Je ne suis pas sûr de ce que vous faites ici, cependant.

Alex M
la source
5

J'aime l'utilisation par jmah d'un Hash pour renforcer l'unicité. Voici quelques autres façons d'écorcher ce chat:

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

C'est un joli 1-liner, mais je suppose que cela pourrait être un peu plus rapide:

h = {}
objs.each {|e| h[e.attr]=e}
h.values
Tête
la source
3

Si je comprends bien votre question, j'ai abordé ce problème en utilisant l'approche quasi-hacky de comparaison des objets Marshaled pour déterminer si des attributs varient. L'injection à la fin du code suivant serait un exemple:

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
la source
3

Le moyen le plus élégant que j'ai trouvé est un spin-off utilisant Array#uniqavec un bloc

enumerable_collection.uniq(&:property)

… Ça se lit mieux aussi!

iGbanam
la source
2

Vous pouvez utiliser un hachage, qui ne contient qu'une seule valeur pour chaque clé:

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

Utilisez Array # uniq avec un bloc:

objects.uniq {|obj| obj.attribute}

Ou une approche plus concise:

objects.uniq(&:attribute)
Muhamad Najjar
la source
1

J'aime les réponses de Jmah et Head. Mais préservent-ils l'ordre des tableaux? Ils pourraient le faire dans les versions ultérieures de ruby ​​car il y a eu des exigences de préservation de l'ordre d'insertion de hachage écrites dans la spécification du langage, mais voici une solution similaire que j'aime utiliser qui préserve l'ordre malgré tout.

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

Implémentation ActiveSupport:

def uniq_by
  hash, array = {}, []
  each { |i| hash[yield(i)] ||= (array << i) }
  array
end
plus grossier
la source
0

Maintenant, si vous pouvez trier les valeurs d'attribut, cela peut être fait:

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

C'est pour un attribut unique, mais la même chose peut être faite avec le tri lexicographique ...

Purfideas
la source