配列をユニークにしてその個数とともに返せ

配列をユニークにしてその個数とともに返せ - rubyco(るびこ)の日記経由で知った、面白そうな問題。

配列をユニークにしてその個数とともに返せ。

具体的には、["foo", "bar", "foo", "baz", "bar", "foo"] を、[ ["foo", 3], ["bar", 2], ["baz", 1] ] にする。

golf - babie, you're my home

結城さん(rubycoさんの中の人)が出力結果で配列の文字列の出現順にこだわってたのを見て(目的が「タグクラウドで文字の大きさ変える」ためらしいので、こだわる必要はないのだけど)、ハッシュを使わないアルゴリズムの方が良いんじゃないかと思ったので、直接配列を組み立てる方針の回答を考えてみた。


まずは、単純に考えてみると、こんな感じになった。

a = ["foo", "bar", "foo", "baz", "bar", "foo"]
r = []
a.each do |e|
  ec = r.find { |ec| ec[0] == e }
  unless ec
    r << [e, 1]
  else
    ec[1] += 1
  end
end
p r

unless ... else ... end の部分を何とか短くできないかと考えて、find メソッドの仕様を確認。引数 ifnone に、真になる要素がひとつも見つからなかったときの値が設定できるので、それを使えばうまくできそうだと思いつく。
引数 ifnone には Proc を渡すのだけど、使い方がよく分からなくてちょっと苦労したが、こんな感じになった。

a = ["foo", "bar", "foo", "baz", "bar", "foo"]
r = []
a.each do |e|
  ec = r.find(proc{(r<<[e,0]).last}) { |ec| ec[0] == e }
  ec[1] += 1
end
p r

ここで、a.each ではなくて、 a.inject にすれば、変数 r の宣言がいらなくなるのでは?と思い、injectの仕様を確認しつつ、以下のようにしてみる。

a = ["foo", "bar", "foo", "baz", "bar", "foo"]
r = a.inject([]) do |r, e|
  ec = r.find(proc{(r<<[e,0]).last}) { |ec| ec[0] == e }
  ec[1] += 1
  r
end
p r

で、このコードを関数 p の呼び出しから始まるようにして、空白やら改行やらを除去して短くすると、こうなった。

p %w|foo bar foo baz bar foo|.inject([]){|r,e|c=r.find(proc{(r<<[e,0]).last}){|d|d[0]==e};c[1]+= 1;r}

もはや、呪文ですな(笑)。

こういう、出来るだけ短い文字数(バイト数)でプログラミングする遊びをコードゴルフと呼ぶらしい。このコードゴルフは、新しい言語を勉強するのに役立つかもしれない。この問題に取り組んだおかげで、今まで使ったことのなかったArray#findやArray#inject、procを使う良い練習になった。

それにしても、明日も会社の研修で朝早いというのに、遅い時間まで起きて何やっているんだろう私は・・・(苦笑)