RubyでネストしたHashをflatな1次元のHashに変換する
{ :foo => "bar", :hello => { :world => "Hello World", :bro => "What's up dude?", }, :a => { :b => { :c => "d" } } }
みたいなネストした Hash があったとして、これを
{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?", :"a.b.c" => "d" }
にしたい時がきっとあると思います。
今回それを実現するためのメソッドをStack Overflow から丸パクリしたので書いたので、メモ。
こんな感じ。引数にネストした Hash を渡すと目的の Hash が返ってくるはず。
def flatten_hash_from hash hash.each_with_object({}) do |(key, value), memo| next flatten_hash_from(value).each do |k, v| memo["#{key}.#{k}".intern] = v end if value.is_a? Hash memo[key] = value end end
each_with_object
を使う。初期値{}
をセットして、これに欲しい情報をどんどん突っ込んでいって目的の Hash を完成させる。inject
がブロックの最後に評価した値がmemo
にセットされていくのに対して、each_with_object
は常に最初に渡したこのオブジェクトだけを見ていてくれるのでわかりやすい。そして flat になるまでひたすら再帰処理をする。
実際に上述のネストした Hash に対して適用した場合を考えて、処理の流れを追ってみる。
まず
key = :foo
,value = "bar"
。これはvalue
が Hash ではないのでmemo
が{ :foo => "bar" }
となって次へ。次に
key = :hello
,value = { :world => "Hello World", :bro => "What's up dude?" }
。これはvalue
が Hash なので、このvalue
に対してさらにflatten_hash_from
メソッドが呼ばれる。再帰処理の中の
memo
は新たな{}
であり、1 のmemo
とは異なるので注意。まずkey = :world
,value = "Hello World"
。これはvalue
が Hash ではないので、memo
は{ :world => "Hello World" }
となる。key = :bro
についても同様なので、最終的にmemo
は{ :world => "Hello World", :bro => "What's up dude?" }
となり、これが 2 のflatten_hash_from(value)
に返る。3 からさらに
each
処理へ。ここでkey = :hello
になっているので、memo["#{key}.#{k}".intern] = v
とすることで、memo
のkey
がhello.world
,hello.bro
のようになる。最終的にこの処理の後のmemo
は{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?" }
になる。次に
key = :a
,value = { :b => { :c => "d" } }
。これもvalue
が Hash なので再帰処理へ。key = :b
,value = { :c => "d" }
なのでさらに再帰処理へ。key = :c
,value = "d"
なので{ :c => "d" }
が 6 のflatten_hash_from
に返る。{ :"b.c" => "d" }
となり、5 のflatten_hash_from
に返る。key = :a
の階層に戻ってきてeach
するので{:"a.b.c" => "d" }
が新たにmemo
に追加される。その結果、memo
は{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?", :"a.b.c" => "d" }
となるので終了。
まとめ
再帰処理はちゃんと処理の流れを理解して使っていきたい。