はじめに
この記事は書籍「プロを目指す人のためのRuby入門」に掲載できなかったトピックを著者自らが紹介するアドベントカレンダーの11日目です。
本文に出てくる章番号や項番号は書籍の中で使われている番号です。
今回はよく使われるハッシュの便利メソッドを紹介していきます。
必要な前提知識
「プロを目指す人のためのRuby入門」の第5章まで読み終わっていること。
よく使われるハッシュの便利メソッド
ハッシュにも数多くのメソッドがありますが、その中でも使用頻度が高いと思われるメソッドを紹介します。
- has_value?/value?
- values_at
- fetch
- dig
- empty?
- merge/merge!/update
- invert
- select/select!/keep_if/reject/reject!/delete_if
has_value?/value?
has_value?
メソッドはハッシュの中に指定された値が存在するかどうか確認するメソッドです。value?
はhas_value?
のエイリアスメソッドです。なお、Hashクラスの実装上、キーの探索は非常に高速に実行できますが、値の探索は要素の個数に比例して遅くなる可能性があるので注意してください。
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.has_value?('dollar')
#=> true
currencies.has_value?('euro')
#=> false
values_at
values_at
メソッドは指定された複数のキーに対応する値を配列に入れて返します。対応するキーが無ければnil
が入ります。
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.values_at(:japan, :india, :italy)
#=> ["yen", "rupee", nil]
fetch
fetch
メソッドは指定したキーの値を取得します。ただし[]
とは異なり、キーが見つからない場合はエラー(KeyError)が発生します。
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.fetch(:japan)
#=> "yen"
currencies.fetch(:italy)
#=> KeyError: key not found: :italy
第2引数を指定すると、キーが見つからないときにその値を返します。
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.fetch(:japan, 'Not found')
#=> "yen"
currencies.fetch(:italy, 'Not found')
#=> "Not found"
ブロックを渡すと、キーが見つからない場合に返す値がブロックの戻り値になります。ブロック引数は引数で指定されたキーです。以下はキーが見つからない場合にキーの値をランダムに入れ替えた文字列を返すコード例です。
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.fetch(:italy) do |key|
key.to_s.chars.shuffle.join
end
#=> "yitla"
dig
dig
メソッドはネストしたハッシュから安全に値を取り出すことができるメソッドです。たとえば以下のようなハッシュがあったとします。
data = {
user: {
address: {
prefecture: 'Tokyo'
}
}
}
prefectureの値を取り出したい場合は次のようにすれば取り出せます。
data[:user][:address][:prefecture]
#=> "Tokyo"
しかし、別のデータではなんらかの理由でaddress以下の情報が入っていなかったとします。
other_data = {
user: {
# address情報が存在しない
}
}
このハッシュに対して、先ほどと同じようにprefectureのデータを取得しようとするとエラーが発生します。
other_data[:user][:address][:prefecture]
#=> NoMethodError: undefined method `[]' for nil:NilClass
なぜなら、addressを取得するとnil
が返り、[:prefecture]
を呼び出せなくなってしまうからです。
other_data[:user][:address]
#=> nil
このようなケースでdig
メソッドを使うと、キーが見つからない場合でもエラーにならず、nil
が返るようになります。
data.dig(:user, :address, :prefecture)
#=> "Tokyo"
other_data.dig(:user, :address, :prefecture)
#=> nil
dig
メソッドは配列やハッシュが混在している場合でも使うことが可能です。配列の場合は添え字を指定します。
# 複数のuserデータ(ハッシュ)を配列に入れる
all_data = [
{
user: {
address: {
prefecture: 'Tokyo'
}
}
},
{
user: {
address: {
# address情報が存在しない
}
}
},
{
user: {
address: {
prefecture: 'Osaka'
}
}
}
]
all_data.dig(0, :user, :address, :prefecture)
#=> "Tokyo"
all_data.dig(1, :user, :address, :prefecture)
#=> nil
all_data.dig(2, :user, :address, :prefecture)
#=> "Osaka"
なお、dig
メソッドはRuby 2.3から登場した比較的新しいメソッドです。それより前のRubyではfetch
メソッドを使うなどして、プログラマ各自でエラーが起きないようにロジックを工夫する必要がありました。
# fetchメソッドを使い、キーが見つからない場合は空のハッシュを返すようにする
data.fetch(:user, {}).fetch(:address, {})[:prefecture]
#=> "Tokyo"
other_data.fetch(:user, {}).fetch(:address, {})[:prefecture]
#=> nil
empty?
empty?
メソッドはハッシュの中身が空の場合にtrue
を返します。
{}.empty?
#=> true
{ japan: 'yen' }.empty?
#=> false
merge/merge!/update
merge
メソッドは元のハッシュに別のハッシュの内容を統合した新しいハッシュを返します。
currencies = { japan: 'yen' }
others = { us: 'dollar', india: 'rupee' }
currencies.merge(others)
#=> {:japan=>"yen", :us=>"dollar", :india=>"rupee"}
# 元のハッシュは変更されない
currencies
#=> {:japan=>"yen"}
merge!
メソッドは元のハッシュを破壊的に変更します。update
はmerge!
のエイリアスメソッドです。
currencies = { japan: 'yen' }
others = { us: 'dollar', india: 'rupee' }
currencies.merge!(others)
currencies
#=> {:japan=>"yen", :us=>"dollar", :india=>"rupee"}
別のハッシュに同じキーがあった場合は、別のハッシュの値が使われます。
currencies = { japan: 'yen' }
others = { japan: '円' }
currencies.merge(others)
#=> {:japan=>"円"}
ブロックを渡すとキーが重複したときに、ブロックの戻り値をそのキーの値にできます。ブロック引数には重複が発生したキー、古い値、新しい値の3つが渡されます。以下は常に古い値の方を採用する場合のコード例です。
currencies = { japan: 'yen' }
others = { japan: '円' }
currencies.merge(others) do |key, old_value, new_value|
old_value
end
#=> {:japan=>yen"}
merge!
メソッドでも同じようにブロックを使ってキーが重複した場合の値を決めることが可能です。
invert
invert
メソッドはキーと値を入れ替えた新しいハッシュを返します。
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.invert
#=> {"yen"=>:japan, "dollar"=>:us, "rupee"=>:india}
値が重複している場合(つまり変換後のキーが重複する場合)は、最後に出てきた要素が採用されます。
hash = { tanaka: 'Hanako', sato: 'Hanako' }
hash.invert
#=> {"Hanako"=>:sato}
select/select!/keep_if/reject/reject!/delete_if
select
メソッドはハッシュ内のキーと値を順にブロックに渡し、ブロックがtrue
を返した要素を集めて新しいハッシュを返します。reject
メソッドは反対にブロックの戻り値がtrue
になった要素を除外して新しいハッシュを返します。
# 生徒ごとにテストの成績を集めたハッシュを作成する
results = { alice: 100, bob: 40, carol: 70 }
# 点数が50点以上の要素だけを集める
results.select { |key, value| value >= 50 }
#=> {:alice=>100, :carol=>70}
# 点数が50点以上の要素を除外する(つまり50点未満の要素を集める)
results.reject { |key, value| value >= 50 }
#=> {:bob=>40}
# 元のハッシュは変更されない
results
#=> {:alice=>100, :bob=>40, :carol=>70}
select!
メソッドやreject!
メソッドは元のハッシュを破壊的に変更します。
results = { alice: 100, bob: 40, carol: 70 }
results.select! { |key, value| value >= 50 }
results
#=> {:alice=>100, :carol=>70}
results = { alice: 100, bob: 40, carol: 70 }
results.reject! { |key, value| value >= 50 }
results
#=> {:bob=>40}
keep_if
メソッドとdelete_if
メソッドも、それぞれselect!
メソッドやreject!
メソッドと同じように動作するメソッドで、ブロックの戻り値に応じて元のハッシュを破壊的に変更します。ただし、削除する要素が1つもなかったときの戻り値が異なります。keep_if
やdelete_if
は元のハッシュを返しますが、select!
やreject!
はnil
を返します。
# 削除するものがなければ元のハッシュを返す
results = { alice: 100, bob: 40, carol: 70 }
results.keep_if { |key, value| value >= 0 }
#=> {:alice=>100, :bob=>40, :carol=>70}
# 削除するものがなければnilを返す
results = { alice: 100, bob: 40, carol: 70 }
results.select! { |key, value| value >= 0 }
#=> nil
# 削除するものがなければ元のハッシュを返す
results = { alice: 100, bob: 40, carol: 70 }
results.delete_if { |key, value| value >= 200 }
#=> {:alice=>100, :bob=>40, :carol=>70}
# 削除するものがなければnilを返す
results = { alice: 100, bob: 40, carol: 70 }
results.reject! { |key, value| value >= 200 }
#=> nil
次回予告
次回は配列リテラルで最後の要素をハッシュにする場合のTipsを紹介します。