以下のように、日時で入れ子になったハッシュの値にAとBが含まれるとき、Aの日時、Bの日時の一覧が欲しくなったので、ハッシュをこねくり回して目的のデータを作成したい。
{ "2016-08-15" => { "18:00" => "A", "19:00" => "B", "20:00" => "A" },
"2016-08-16" => { "19:00" => "A" } }
{ "A" => ["2016-08-15 18:00", "2016-08-15 20:00", "2016-08-16 19:00"],
"B" => ["2016-08-15 19:00"] }
結論
だいぶ長いが1行で書ける。
input.map{ |date, daily_hash| daily_hash.inject({}) { |h,(time,v)| (h[v] ||= []) << "#{date} #{time}"; h } }
.inject({}){|result, part| result.merge(part){ |_, v, v2| v + v2 } }
…さすがに重たいので順を追って解説する。
解説
外側のmap
細かい処理をhogeに書き換えるとこう。
input.map{ |date, daily_hash| daily_hash.hoge }
まず、input
をmap
することで、日付ごとに入れ子になったハッシュが1つづつ取り出されてdate
、 daily_hash
に格納される。
# 1回目: date = "2016-08-15", daily_hash = { "18:00" => "A", "19:00" => "B", "20:00" => "A" }
# 2回目: date = "2016-08-16", daily_hash = { "19:00" => "A" }
このdaily_hash
を1つめのinject
でこねくり回して目的のデータにしていく。
1つめのinject
daily_hash.inject({}) { |h,(time,v)| (h[v] ||= []) << "#{date} #{time}"; h } }
inject
は初期値に対して畳み込みを行っていく。
この場合は空のハッシュが初期値であり、ブロックのh
に呼び出されるので、ここに値を追加していきたい。daily_hash
の値v
をキーとして、日付date
と時刻time
から日時の文字列をつくり、配列h[v]
に追加していく。date
はdaily_hash
と同じブロックで取得している。
初期値を定義しないとh[v]
がnil
の状態で<<
はできないので、h[v] ||= []
で初期化している。
ブロックの中の最後の評価値がinject
で畳み込まれていくので、セミコロンで区切って最後にh
を呼んでいる。
8月15日ぶんのデータのみ細かく内訳を追っていこう。
# 初期値:{}
# 1回目: { "A" => ["2015-08-15 18:00"] }
# 2回目: { "A" => ["2015-08-15 18:00"], "B" => ["2015-08-15 19:00"] }
# 3回目: { "A" => ["2015-08-15 18:00", "2015-08-15 20:00"], "B" => ["2015-08-15 19:00"] }
2つめのinject
map〜1つめのinject
をfuga
に置き換えるとこうなる。
input.fuga.inject({}){|result, part| result.merge(part){ |_, v, v2| v + v2 } }
まず、input.fuga
は以下の様な状態になっている。
[{ "A" => ["2015-08-15 18:00", "2015-08-15 20:00"], "B" => ["2015-08-15 19:00"] }, { "A" => ["2015-08-16 19:00"] }]
これをinject
を使ってHash.merge
で畳み込んでいき、ハッシュの配列を1つの配列に統合する。
ここで、merge
にブロックが与えられているのはハッシュの重複対策である。もしこれがなかったとしたら、ハッシュを結合する時に重複したキーA
の値が上書きされてしまい、最終データは以下のようになってしまう。
{ "A" => ["2016-08-16 19:00"], "B" => ["2016-08-15 19:00"] }
それでは困るので、キーが重複した場合は競合する値のv
とv2
を足して新しい配列を作る。前の処理で空の配列は初期化されているのでここでは+
ができないケースを考える必要はない。