ハッシュの便利メソッドあれこれ


## はじめに

この記事は書籍「[プロを目指す人のためのRuby入門](http://gihyo.jp/book/2017/978-4-7741-9397-7)」に掲載できなかったトピックを著者自らが紹介するアドベントカレンダーの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クラスの実装上、キーの探索は非常に高速に実行できますが、値の探索は要素の個数に比例して遅くなる可能性があるので注意してください。

```ruby
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.has_value?('dollar')
#=> true
currencies.has_value?('euro')
#=> false
```

### values_at

`values_at`メソッドは指定された複数のキーに対応する値を配列に入れて返します。対応するキーが無ければ`nil`が入ります。

```ruby
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.values_at(:japan, :india, :italy)
#=> ["yen", "rupee", nil]
```

### fetch

`fetch`メソッドは指定したキーの値を取得します。ただし`[]`とは異なり、キーが見つからない場合はエラー（KeyError）が発生します。

```ruby
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.fetch(:japan)
#=> "yen"
currencies.fetch(:italy)
#=> KeyError: key not found: :italy
```

第2引数を指定すると、キーが見つからないときにその値を返します。

```ruby
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.fetch(:japan, 'Not found')
#=> "yen"
currencies.fetch(:italy, 'Not found')
#=> "Not found"
```

ブロックを渡すと、キーが見つからない場合に返す値がブロックの戻り値になります。ブロック引数は引数で指定されたキーです。以下はキーが見つからない場合にキーの値をランダムに入れ替えた文字列を返すコード例です。

```ruby
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.fetch(:italy) do |key|
  key.to_s.chars.shuffle.join
end
#=> "yitla"
```

### dig

`dig`メソッドはネストしたハッシュから安全に値を取り出すことができるメソッドです。たとえば以下のようなハッシュがあったとします。

```ruby
data = {
    user: {
        address: {
            prefecture: 'Tokyo'
        }
    }
}
```

prefectureの値を取り出したい場合は次のようにすれば取り出せます。

```ruby
data[:user][:address][:prefecture]
#=> "Tokyo"
```

しかし、別のデータではなんらかの理由でaddress以下の情報が入っていなかったとします。

```ruby
other_data = {
    user: {
      # address情報が存在しない
    }
}
```

このハッシュに対して、先ほどと同じようにprefectureのデータを取得しようとするとエラーが発生します。

```ruby
other_data[:user][:address][:prefecture]
#=> NoMethodError: undefined method `[]' for nil:NilClass
```

なぜなら、addressを取得すると`nil`が返り、`[:prefecture]`を呼び出せなくなってしまうからです。

```ruby
other_data[:user][:address]
#=> nil
```

このようなケースで`dig`メソッドを使うと、キーが見つからない場合でもエラーにならず、`nil`が返るようになります。

```ruby
data.dig(:user, :address, :prefecture)
#=> "Tokyo"
other_data.dig(:user, :address, :prefecture)
#=> nil
```

`dig`メソッドは配列やハッシュが混在している場合でも使うことが可能です。配列の場合は添え字を指定します。

```ruby
# 複数の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`メソッドを使うなどして、プログラマ各自でエラーが起きないようにロジックを工夫する必要がありました。

```ruby
# fetchメソッドを使い、キーが見つからない場合は空のハッシュを返すようにする
data.fetch(:user, {}).fetch(:address, {})[:prefecture]
#=> "Tokyo"
other_data.fetch(:user, {}).fetch(:address, {})[:prefecture]
#=> nil
```

### empty?

`empty?`メソッドはハッシュの中身が空の場合に`true`を返します。

```ruby
{}.empty?
#=> true

{ japan: 'yen' }.empty?
#=> false
```

### merge/merge!/update

`merge`メソッドは元のハッシュに別のハッシュの内容を統合した新しいハッシュを返します。

```ruby
currencies = { japan: 'yen' }
others = { us: 'dollar', india: 'rupee' }
currencies.merge(others)
#=> {:japan=>"yen", :us=>"dollar", :india=>"rupee"}

# 元のハッシュは変更されない
currencies
#=> {:japan=>"yen"}
```

`merge!`メソッドは元のハッシュを破壊的に変更します。`update`は`merge!`のエイリアスメソッドです。

```ruby
currencies = { japan: 'yen' }
others = { us: 'dollar', india: 'rupee' }
currencies.merge!(others)
currencies
#=> {:japan=>"yen", :us=>"dollar", :india=>"rupee"}
```

別のハッシュに同じキーがあった場合は、別のハッシュの値が使われます。

```ruby
currencies = { japan: 'yen' }
others = { japan: '円' }
currencies.merge(others)
#=> {:japan=>"円"}
```

ブロックを渡すとキーが重複したときに、ブロックの戻り値をそのキーの値にできます。ブロック引数には重複が発生したキー、古い値、新しい値の3つが渡されます。以下は常に古い値の方を採用する場合のコード例です。

```ruby
currencies = { japan: 'yen' }
others = { japan: '円' }
currencies.merge(others) do |key, old_value, new_value|
  old_value
end
#=> {:japan=>yen"}
```

`merge!`メソッドでも同じようにブロックを使ってキーが重複した場合の値を決めることが可能です。

### invert

`invert`メソッドはキーと値を入れ替えた新しいハッシュを返します。

```ruby
currencies = { japan: 'yen', us: 'dollar', india: 'rupee' }
currencies.invert
#=> {"yen"=>:japan, "dollar"=>:us, "rupee"=>:india}
```

値が重複している場合（つまり変換後のキーが重複する場合）は、最後に出てきた要素が採用されます。

```ruby
hash = { tanaka: 'Hanako', sato: 'Hanako' }
hash.invert
#=> {"Hanako"=>:sato}
```

### select/select!/keep_if/reject/reject!/delete_if

`select`メソッドはハッシュ内のキーと値を順にブロックに渡し、ブロックが`true`を返した要素を集めて新しいハッシュを返します。`reject`メソッドは反対にブロックの戻り値が`true`になった要素を除外して新しいハッシュを返します。

```ruby
# 生徒ごとにテストの成績を集めたハッシュを作成する
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!`メソッドは元のハッシュを破壊的に変更します。

```ruby
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`を返します。

```ruby
# 削除するものがなければ元のハッシュを返す
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を紹介します。