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.

Réponses:


201

Utiliser Array#uniqavec un bloc:

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

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

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")

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] }

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.


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

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

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!


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



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)}

1

Implémentation ActiveSupport:

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

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.