Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: ArrayからHashを作るいろいろな方法

Rubyを書いていると、「Array(配列)をHash(ハッシュ)に変換したい」という場面は意外と多く出てきます。
例えば、APIレスポンスを加工してキーと値を対応付けたいとき、集計結果を整理したいとき、Railsでフォーム入力を扱うときなど。
しかし、一口に「配列からハッシュを作る」と言ってもやり方はいくつもあり、どれを選ぶかでコードの読みやすさや意図の伝わり方が大きく変わります。

この記事では、Ruby標準の機能からRails拡張メソッドまで、ArrayからHashを作る代表的な方法を体系的に紹介します。
「どの方法を使えばよいのか?」が自然に判断できるようになることを目指します。


[key, value] ペアを要素とするArrayをHashに変換する

もっとも基本的なパターンです。
Arrayの各要素が [key, value] のペアで構成されている場合、Ruby標準の Array#to_h を使えば一発です。

[
  [:a, "A"], [:b, "B"], [:c, "C"], [:d, "D"], [:e, "E"]
].to_h
#=> {:a=>"A", :b=>"B", :c=>"C", :d=>"D", :e=>"E"}

シンプルで直感的。Ruby 2.1以降で利用できます。
「すでにペア構造を持っている配列をハッシュ化する」ケースでは、まずこれを使いましょう。


2つのArrayを結合してHashにする

キーと値が別々の配列として存在するケースも多いですよね。
その場合に使えるのが Array#zipArray#transpose です。

Array#zip + to_h

zip は2つの配列を「横に」結合し、それを to_h で変換します。

[:a, :b, :c, :d, :e].zip(["A", "B", "C", "D", "E"]).to_h
#=> {:a=>"A", :b=>"B", :c=>"C", :d=>"D", :e=>"E"}

Array#transpose + to_h

transpose は「縦横を入れ替える」メソッドです。
2つの配列をひとつの配列にまとめてから、行と列を入れ替えます。

[
  [:a, :b, :c, :d, :e],
  ["A", "B", "C", "D", "E"]
].transpose.to_h
#=> {:a=>"A", :b=>"B", :c=>"C", :d=>"D", :e=>"E"}

見た目はやや異なりますが、結果は同じです。
zip のほうがコードの意図が分かりやすく、実務でもよく使われます。


Arrayを逐次評価してHashを作る

単に配列の要素を変換するだけでなく、処理を挟みながらHashを構築したい場合に便利な方法を紹介します。

Array#to_h(ブロック付き)

ブロックを渡すと、要素ごとに [key, value] のペアを返す形で処理できます。

%w[a b c d e].to_h do |alphabet|
  [alphabet.to_sym, alphabet.upcase]
end
#=> {:a=>"A", :b=>"B", :c=>"C", :d=>"D", :e=>"E"}

1行で「配列の要素を加工しつつハッシュに変換」ができる、Rubyらしい書き方です。


Enumerable#inject

Enumerable#inject は畳み込み操作。
初期値に空ハッシュ {} を与え、ブロック内で都度値を追加していきます。

%w[a b c d e].inject({}) do |acc, alphabet|
  acc[alphabet.to_sym] = alphabet.upcase
  acc
end
#=> {:a=>"A", :b=>"B", :c=>"C", :d=>"D", :e=>"E"}

acc に都度値を追加して返す構造が少し冗長ですが、柔軟性が高く、条件付き追加など複雑な処理に向いています。


Enumerable#each_with_object

Enumerable#each_with_object は、inject の「より読みやすい」版といえます。

%w[a b c d e].each_with_object({}) do |alphabet, acc|
  acc[alphabet.to_sym] = alphabet.upcase
end
#=> {:a=>"A", :b=>"B", :c=>"C", :d=>"D", :e=>"E"}

こちらはブロックの戻り値を意識しなくてよく、可読性が高いです。
ハッシュ構築で迷ったらまず each_with_object を選ぶとよいでしょう。


ArrayをHashに集約する(集計・分類)

配列を集約・グルーピングしてハッシュにしたい場合は、Enumerable#tallyEnumerable#group_by が強力です。

Enumerable#tally

各要素の出現回数を自動で数えてくれます(Ruby 2.7以降)。

%w[a b b B c c c C C d D].tally
#=> {"a"=>1, "b"=>2, "B"=>1, "c"=>3, "C"=>2, "d"=>1, "D"=>1}

特にログ解析や統計処理で重宝します。
大文字・小文字を区別したくない場合は、map(&:downcase).tally のように前処理を加えるのが定番です。


Enumerable#group_by

条件ごとに要素を分類してHashにする場合はこちら。

%w[a b b B c c c C C d D]
  .group_by { |alphabet| /^[a-z]+$/.match?(alphabet) ? 'lower' : 'upper' }
#=> {"lower"=>["a", "b", "b", "c", "c", "c", "d"], "upper"=>["B", "C", "C", "D"]}

グルーピング後に .transform_values(&:count) のように加工すれば、
「分類別の件数」を1行で求めることもできます。


Railsで使える便利メソッド

Rails(ActiveSupport)環境では、さらに便利なメソッドが追加されています。
Ruby標準よりも直感的に使えるため、実務での登場頻度も高いです。

Enumerable#index_with

キーを配列の要素、値をブロックの戻り値にしてHashを作ります。

%w[a b c d e].index_with(&:upcase)
#=> {"a"=>"A", "b"=>"B", "c"=>"C", "d"=>"D", "e"=>"E"}

Ruby 3.1 でもほぼ同名のメソッド(with_index)が追加されましたが、
Railsでは古くからサポートされており、ほぼ同じ挙動です。


Enumerable#index_by

逆に、ブロックの結果をキー、元の要素を値とするのが index_by です。

%w[a b c d e].index_by(&:upcase)
#=> {"A"=>"a", "B"=>"b", "C"=>"c", "D"=>"d", "E"=>"e"}

APIレスポンスやデータベース結果を「特定の属性でキー化したい」ケースに非常に便利です。


まとめ

Rubyの世界では、ArrayからHashを作る手段が非常に豊富です。
どの方法を選ぶかは「目的」と「データ構造」によって変わります。

目的 推奨メソッド
[key, value] ペアをHash化 to_h
2つの配列からHash zip または transpose
処理しながら構築 to_h {}, each_with_object
集約・グループ化 tally, group_by
Rails環境で簡潔に index_with, index_by

コードを短く書くことが目的ではなく、「意図が伝わる書き方をする」ことが重要です。
Rubyにはそのための表現力が備わっています。
ぜひ状況に合わせて、最適な変換メソッドを選んでみてください。

関連記事

Ruby: eachよりもmapなどのコレクションを積極的に使おう(社内勉強会)

Ruby: injectとeach_with_objectをうまく使い分ける(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。