Ruby
プロを目指す人のためのRuby入門

はじめに

この記事は書籍「プロを目指す人のための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!メソッドは元のハッシュを破壊的に変更します。updatemerge!のエイリアスメソッドです。

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_ifdelete_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を紹介します。