配列内の重複要素を集計してハッシュにまとめる
シェルのuniq -cみたいな機能をrubyでもやりたい
文章の中に同じ単語がそれぞれ何回づつ出てくるのか調べたいな、と思った時、便利なので私はいつもシェル芸を使いがちなのですが、プログラムの中でこういう機能を実現したいときいちいち外部プログラムを呼び出してやるのはあまりエレガントとはいえないので、Rubyで要素の集計を行なうにはどうしたらいいか考え、コードを書いてみました。
コード例
#!/usr/bin/ruby
arr = ["yamada", "yamada", "tanaka", "tanaka", "tanaka", "suzuki", "yasuda", "yasuda"]
arr = arr.sort
arr_words = arr.uniq
hsh = Hash.new
arr_words.each do |word|
count = arr.rindex(word) - arr.index(word) + 1
hsh.store(word, count)
end
# puts hsh => {"suzuki"=>1, "tanaka"=>3, "yamada"=>2, "yasuda"=>2}
使うのは、Array.indexとArray.rindex
配列に対するindex( obj )メソッドは、その配列中にobjと同じ要素があるか調べるもので、その配列中でobjが初めて出てくる場所の位置情報を教えてくれます。たとえば、
arr = ["apple", "melon", "banana", "grape", "apple", "banana"]
puts arr.index("apple")
# => 0
puts arr.index("banana")
# => 2
となります。一方、rindex( obj )メソッドは、indexメソッドとは逆で、その配列中でobjが出てくる最後の位置を教えてくれます。つまり、
arr = ["apple", "melon", "banana", "grape", "apple", "banana"]
puts arr.rindex("apple")
# => 4
puts arr.rindex("banana")
# => 5
となります。
arr.rindex(obj) - arr.index(obj)
ところでシェル芸をつかって要素の集計をするときには、uniq -c
だけではなくて、sort | uniq -c
のようにsortコマンドと組み合わせて使うことが多いです。これと同じように考えて、配列の要素を集計するにはまずその配列にソートをかけたらいいんじゃないかと思いました。
arr = arr.sort
#puts arr => ["apple", "apple", "banana", "banana", "grape", "melon"]
このように要素を整列させると、
arr.rindex(obj) - arr.index(obj) + 1
という関係を使って各要素の個数が調べられることがわかります。
説明するよりやってみたほうが早いので、実際にやってみると、
arr = ["apple", "apple", "banana", "banana", "grape", "melon"]
puts arr.rindex("apple") - arr.index("apple") + 1
# (1 - 0 + 1) => 2
puts arr.rindex("banana") - arr.index("banana") + 1
# (3 - 2 + 1) => 2
puts arr.rindex("grape") - arr.index("grape") + 1
# (4 - 4 + 1) => 1
puts arr.rindex("melon") -arr.index("melon") + 1
# (5 - 5 + 1) => 1
のようになり、確かに各要素の個数をちゃんと数えることができていることがわかります。
集計結果を再利用できるようにハッシュ化する
集計結果をこのあと利用しやすいように、要素の名前とその数を紐つけて管理しておくと便利です。そこで、ハッシュを作成します。
ハッシュを作成するにあたっては、さっきのコードのように一つ一つの要素ごとに式を書いていたら非効率ですし、正確性にも欠けるので、配列arr.uniq
にeachメソッドを使ってループを回します。
また、ハッシュにキーとオブジェクトを登録する際にはHash.store(key,obj)メソッドを活用するといいでしょう。
arr_elements = arr.uniq
# => ["apple", "banana", "grape", "melon"]
hsh = Hash.new
arr_elements.each do |word|
count = arr.rindex(word) - arr.index(word) + 1
hsh.store(word, count)
end
# => {"apple"=>2, "banana"=>2, "grape"=>1, "melon"=>1}