give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

Rubyでハッシュを別の形式のハッシュに変換する方法

2012.4.6 追記

えーっと、このエントリを公開したらコメントにて最強の変換方法を教えていただきました。

p Hash[initial_hash.map { |k,v| [@convert_table[k], v] }]

わざわざメソッド化しなくても、これなら一撃必殺ですね。
keyesberryさん、どうもありがとうございました!!


・・・というわけで、ここから下はあまり意味のないエントリです。
まあ、ヒマつぶしにでもどうぞ(^^;

はじめに

先日発見したRubyのハッシュに関するイディオムの紹介です。
タイトルにもある通り、あるハッシュを別の形式のハッシュに変換する場合のイディオムです。

例題

例えばこんな例題を考えてみます。

initial_hash   = { 'Red' => '#ff0000', 'Green' => '#00ff00', 'Blue' => '#0000ff' }
p method_x(initial_hash) #{"赤"=>"#ff0000", "緑"=>"#00ff00", "青"=>"#0000ff"}

ここではmethod_xを通ると、ハッシュのキーが日本語から英語に変わっています。
まあ、あくまで例題なんで、この仕様自体に対する細かいツッコミはなしでお願いします。。。

each_pairメソッドを使う(普通)

素直に実装するとこんな感じになるんじゃないでしょうか?

# -*- encoding: utf-8 -*-
initial_hash   = { 'Red' => '#ff0000', 'Green' => '#00ff00', 'Blue' => '#0000ff' }
@convert_table = { 'Red' => '',      'Green' => '',      'Blue' => '' }

def use_each_pair(hash)
  new_hash = {}
  hash.each_pair do |key, value|
    new_hash[@convert_table[key]] = value
  end
  new_hash
end

p use_each_pair(initial_hash) #{"赤"=>"#ff0000", "緑"=>"#00ff00", "青"=>"#0000ff"}

新しいハッシュを作って、そこに順番に変更後の要素を追加して、最後にそのハッシュを返しています。
もちろんこれでも問題ないのですが、もっと短く、もっとスマートに実装できないでしょうか?

each_with_objectメソッドを使う(ベスト!)

そこで、each_with_objectというメソッドの登場です。これを使うとより短く、よりスマートにかけます。

# -*- encoding: utf-8 -*-
initial_hash   = { 'Red' => '#ff0000', 'Green' => '#00ff00', 'Blue' => '#0000ff' }
@convert_table = { 'Red' => '',      'Green' => '',      'Blue' => '' }

def use_each_with_object(hash)
  hash.each_with_object({}) {|(key, value), new_hash|
    new_hash[@convert_table[key]] = value
  }
end

p use_each_with_object(initial_hash) #{"赤"=>"#ff0000", "緑"=>"#00ff00", "青"=>"#0000ff"}

メソッドの引数で返却用の値を初期化します(今回は空のハッシュを指定)。
その後は毎回ブロック引数に返却用のハッシュが渡されるので、ここに要素を追加します。
メソッドの戻り値は返却用の値そのものになります。
やっていることは最初の例とほとんど同じですが、より短く、よりスマートに書けました。

injectメソッドを使う(ちょっと惜しい)

これと似たようなメソッドにinjectがあります。
injectを使うとこうなります。

def use_inject(hash)
  hash.inject({}) {|new_hash,(key, value)|
    new_hash[@convert_table[key]] = value
    new_hash
  }
end

each_with_objectの場合とほとんど同じなのですが、ブロックの中で毎回返却用の値を返す必要があります。これがちょっと惜しいですね。
また、ブロック引数の順番がeach_with_objectと異なっています。

mapメソッドを使う(う〜ん、イマイチ)

あと、こういう処理だと直感的にmapメソッドが使えそうな気がしますが、ハッシュでmapメソッドを使うと配列が返されてしまうので、ひとひねり加える必要があります。

def use_map(hash)
  #array = [["赤", "#ff0000"], ["緑", "#00ff00"], ["青", "#0000ff"]]
  array = hash.map {|key, value|
    [@convert_table[key], value]
  }
  #array.flatten = ["赤", "#ff0000", "緑", "#00ff00", "青", "#0000ff"]
  Hash[*array.flatten]
end

コメントを参照してもらいたいのですが、いったん「キーと値の配列が並んだ配列」を作り、それをフラットな配列にして(flatten)ハッシュに変換しています。
短くもないし、全体的にトリッキーなので、あまり使いたくないですね。

まとめ

というわけでハッシュを別の形式のハッシュに変換するイディオムを4つ紹介しました。
やはりベストなのはeach_with_objectメソッドを使うやり方だと思います。
もし同じような処理を実装する場合は参考にしてみてください。

参考情報

Module: Enumerable (Ruby 1.9.3)
each_with_objectメソッドはハッシュクラスではなく、Enumerableモジュールに定義されているようですね。


Hash#map - Ruby Forum
each_with_objectメソッドを使う方法はこのページで見つけました。