配列の重複した要素を取得するメソッドが無い問題とその対処方法

【結論】

RubyRails)では、
 重複した値を除去するメソッドは用意されているが、
 重複している値を取得するメソッドは存在しない

・重複した値を取得する場合は、
 複数のメソッドを組み合わせて
 処理を行う必要がある

・「array.group_by{|i| i}.reject{|k,v| v.one?}.keys」

【目次】

【本題】

CSVからユーザー情報を登録する機能を実装した時の話

仕事でCSVからユーザーの情報を
一括で登録出来る機能の実装をしたのですが、
仕様にある問題がありました。

それは、CSVファイル内で、メールアドレスなどの
ユニークであるべき値が重複していた場合に発生します。

モデルにバリデーションは設定していたので、
同じ値のレコードが複数生成される事はありませんが、
一番最初に読み込まれたデータは登録されてしまうという仕様になっていました。

重複している以上、そのデータが誤りである可能性も考えられ流ので、
重複しているメールアドレスを持っているデータは、
一切登録されない様に、あらかじめ除外するコードを書こうとしました。

RubyRails)には、重複した値を除去するメソッドが無い

Ruby 重複」などで検索してみると、下記の様なメソッドがヒットしました。

uniq
uniq!

これらは、配列に対して利用できるメソッドで、
配列内の重複している要素を削除した新しい配列を返します。
「uniq!」はレシーバー自体の値を変更します。

mail = ["mail1", "mail1", "mail1", "mail1", "mail2", "mail3", "mail4", "mail5"]

mail.uniq
=> ["mail1", "mail2", "mail3", "mail4", "mail5”]

mail
=> ["mail1", "mail1", "mail1", "mail1", "mail2", "mail3", "mail4", "mail5"]

mail.uniq!
=> ["mail1", "mail2", "mail3", "mail4", "mail5”]

mail
=> ["mail1", "mail2", "mail3", "mail4", "mail5"]

しかし、これは重複している要素を除去するだけで、
重複している要素の特定には使えません。

他にも色々探してみましたが、
重複している要素を特定するメソッドは見当たりませんでした。

メソッドを組み合わせて、重複を特定する

そんな時に、下記のサイトを発見しました。

https://doruby.jp/users/pe/entries/ruby%E3%81%A7%E9%85%8D%E5%88%97%E3%81%8B%E3%82%89%E9%87%8D%E8%A4%87%E3%81%99%E3%82%8B%E5%80%A4%E3%82%92%E6%8A%BD%E5%87%BA%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95%E3%82%92%E6%8E%A2%E3%81%97%E3%81%9F%E9%9A%9B%E3%81%AB%E8%A6%8B%E3%81%A4%E3%81%91%E3%81%9Fgroup_by%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%8C%E4%BE%BF%E5%88%A9%E3%81%A0%E3%81%A3%E3%81%9Fdoruby.jp

これによると「group_by」「reject」「one?」「keys」のメソッドを
組み合わせて使用することで、直接重複している要素を取得できる様です。

それぞれのメソッドの仕様は下記の通りです。

「group_by」
ブロック引数に各要素を入れて、要素の数だけブロックを実行。
ブロックの返り値が同じ要素を下記の様に一つのハッシュにまとめて返す。
{ 返り値 => [要素, 要素, ...], ... }

mail = ["mail1", "mail1", "mail1", "mail1", "mail2", "mail3", "mail4", "mail5”]

mail.group_by{|i| I}
=> {"mail1"=>["mail1", "mail1", "mail1", "mail1"], "mail2"=>["mail2"], "mail3"=>["mail3"], "mail4"=>["mail4"], "mail5"=>["mail5"]}

reject
ブロック引数に各要素を入れて、要素の数だけブロックを実行。
ブロックの返り値がfalseになった要素を集めた配列を返す。

mail = ["mail1", "mail1", "mail1", "mail1", "mail2", "mail3", "mail4", "mail5”]

mail.reject {|i| i =="mail1" }
=> ["mail2", "mail3", "mail4", "mail5"]

「one?」
ブロック引数に各要素を入れて、要素の数だけブロックを実行。
返り値が1つだけtureだった場合にtrueを返し、それ以外はfalseを返す。

mail = ["mail1", "mail1", "mail1", "mail1", "mail2", "mail3", "mail4", "mail5”]

 mail.one? {|i| i =="mail1" }
=> false

mail.one? {|i| i =="mail2" }
=> true

「keys」
ハッシュのキーを集めて配列にして返す。

user = { name: "ryoutaku", email: "mail@mail.com"}
=> {:name=>"ryoutaku", :email=>"mail@mail.com"}

user.keys
=> [:name, :email]

使ってみた

mail = ["mail1", "mail1", "mail1", "mail1", "mail2", "mail3", "mail4", "mail5”]

mail.group_by{|i| i}.reject{|k,v| v.one?}.keys
=> ["mail1”]

きちんと、重複している値のみを取得できました!
一応、段階を追ってみていきます。

#重複している要素をグループにまとめる
mail.group_by{|i| i}
=> {"mail1"=>["mail1", "mail1", "mail1", "mail1"], "mail2"=>["mail2"], "mail3"=>["mail3"], "mail4"=>["mail4"], "mail5"=>["mail5”]}

#重複している要素のグループだけを取得
mail.group_by{|i| i}.reject{|k,v| v.one?}
=> {"mail1"=>["mail1", "mail1", "mail1", "mail1”]}

#グループのキーを取得
mail.group_by{|i| i}.reject{|k,v| v.one?}.keys
=> ["mail1"]

これにより、重複している要素を取得する事が可能です。