Rubyのオブジェクト属性によるUniq


126

1つ以上の属性に関して一意である配列内のオブジェクトを選択する最もエレガントな方法は何ですか?

これらのオブジェクトはActiveRecordに格納されるため、ARのメソッドを使用しても問題ありません。

回答:


200

Array#uniqブロックで使用:

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

5
これはruby 1.9以降のバージョンの正解です。
nurettin

2
+1。以前のルビーのために、常にありますrequire 'backports':-)
マルク=アンドレ・Lafortune

(たとえば)num_playsを合計しているときに、たとえばalbum_idでグループ化する場合は、ハッシュ方式の方が適しています。
thekingoftruth 2013

20
あなたはto_proc(とそれを向上させることができruby-doc.org/core-1.9.3/Symbol.html#method-i-to_proc:)@photos.uniq &:album_id
joaomilho

あなたはこの同じSOにちょうど下記読む必要はルビー1.8用@brauliobo:stackoverflow.com/a/113770/213191
ピーター・H. Boling

22

uniq_byプロジェクトのArrayにメソッドを追加します。それはとの類推によって機能しsort_byます。そうuniq_byしているuniqようsort_byにですsort。使用法:

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

実装:

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

現在の配列を変更するのではなく、新しい配列を返すことに注意してください。私たちは書いていないuniq_by!メソッドが、必要に応じて簡単に作成できます。

編集:Tribalvibesは、その実装はO(n ^ 2)であると指摘しています。(テストされていない)のようなものが良いでしょう...

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ですが、大きな配列のスケーリングパフォーマンスは(O(n ^ 2)のように見えます)低くなります。変換をハッシュセットにすることで修正できます。
tribalvibes

7
この回答は古くなっています。Ruby> = 1.9には、受け入れられた回答のように、これを正確に行うブロックを持つArray#uniqがあります。
Peter H.Boling、2014

17

データベースレベルで実行します。

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

1
そして、それが複数のフィールドである場合はどうでしょうか?
Ryan Bigg

12

このトリックを使用して、配列からいくつかの属性要素によって一意を選択できます。

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

とても明白なので、Ruby。Rubyを祝福するもう1つの理由
ToTenMilan

6

私は元々select、Array のメソッドを使用することを提案していました。ウィットするには:

[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0}[2,4,6]返し てくれます。

ただし、そのような最初のオブジェクトが必要な場合は、を使用しますdetect

[1, 2, 3, 4, 5, 6, 7].detect{|e| e>3}私たちに与え4ます。

ここで何をしようとしているのかよくわかりません。


5

私はjmahがハッシュを使用して一意性を強制するのが好きです。その猫の皮をむく方法は他にいくつかあります:

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

これはすばらしい1ライナーですが、これは少し高速かもしれません。

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

3

私があなたの質問を正しく理解していれば、マーシャリングされたオブジェクトを比較して属性が変化するかどうかを判断するという準ハックなアプローチを使用して、この問題に取り組みました。次のコードの最後にあるインジェクトがその例です。

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

私が見つけた最もエレガントな方法はArray#uniq、ブロックで使用するスピンオフです

enumerable_collection.uniq(&:property)

…それも読みやすい!


2

各キーに値を1つだけ含むハッシュを使用できます。

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



1

私はjmahとHeadの答えが好きです。しかし、それらは配列の順序を保持しますか?言語仕様にいくつかのハッシュの挿入順序を維持する要件が記述されているため、Rubyの新しいバージョンではそれらが使用される可能性があります。

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

0

ここで、属性値でソートできる場合、これを行うことができます。

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

これは1属性の一意のものですが、同じことを辞書式ソートで実行できます...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.