Ruby / Railsで、key-value なオブジェクトから値を取得するイディオム


環境

Ruby 2.5.1
Ruby on Rails 5.0.5


きっかけ

Rails を触っていると、Hash から特定の key の値だけ取得したい時がある


hash = { key1: "value1", key2: "value2", key3: "value3" }

hash.slice(:key1, :key2)
=> {:key1=>"value1", :key2=>"value2"}

違う、そうじゃない


おれは値だけがほしいんだ

hash.slice(:key1, :key2)
=> {:key1=>"value1", :key2=>"value2"}
# ↓ 本当はこうしたい
=> ["value1", "value2"]

> hash.extract(:key1, :key2)
NoMethodError: undefined method `extract` for {:key1=>"value1", :key2=>"value2", :key3=>"value3"}:Hash
...

> hash.pluck(:key1, :key2)
TypeError: no implicit conversion of Symbol into Integer
...

> hash.attributes(:key1, :key2)
NoMethodError: undefined method `attributes` for {:key1=>"value1", :key2=>"value2", :key3=>"value3"}:Hash
...


んーなんだっけ?

といつも自分でもわからなくなるので、似たような目的のメソッドとか書き方を自分用としてまとめました


ちなみに、さきほどの問題に対しての回答は

hash.values_at(:key1, :key2)
=> ["value1", "value2"]

でした


Hash

hash = { key1: "value1", key2: "value2", key3: "value3" }

# 値だけ
hash.values_at(:key1, :key2)
=> ["value1", "value2"]

# Hash として
hash.slice(:key1, :key2) # ruby 2.5.0 で ActiveSuport から輸入
=> {:key1=>"value1", :key2=>"value2"}

Hash[]

> hashes = [{ key1: "value1-1", key2: "value2-1"}, { key1: "value1-2", key2: "value2-2"}]

# 値だけ
hash.pluck(:key1) # rails 5.0 からの Enumerable 拡張
=> ["value1-1", "value1-2"]

# Hash として
hashes.map { |h| h.slice(:key1) }
=> [{:key1=>"value1-1"}, {:key1=>"value1-2"}]

ApplicationRecord

message = Message.take

# 値だけ
user.attributes.values_at("title", "text") # #attributes は Hash を返すので、キーの指定が文字列である必要がある
or
user.slice(:title, :text).values # Ruby 1.9 から Hash は順序を保存するようになったのでこれでも大丈夫
=> ["タイトル1", "内容1"]

# Hash として
user.slice(:title, :text)
=> {"title"=>"タイトル1", "text"=>"内容1"} # with_indifferent_access されて帰ってくるので、文字列でもシンボルでもアクセス可能

ActiveRecord::Associations

users = User.take(2)

# 値だけ
users.pluck(:title, :text)
=> [["タイトル1", "内容1"], ["タイトル2", "内容2"]]

# Hash として
users.map { |u| u.slice(:title, :text) }
=> [{"title"=>"タイトル1", "text"=>"内容1"}, {"title"=>"タイトル2", "text"=>"内容2"}]

おまけ

似たようなメソッドがいっぱいあるぞ!


Hash#extract!

最初に間違えたやつのBANメソッド
何故か #extract は存在しない
動作は #slice! と同様

hash = { key1: "value1", key2: "value2", key3: "value3" }

hash.extract!(:key1, :key2)
=> {:key1=>"value1", :key2=>"value2"}
hash
=> {:key3=>"value3"}

Hash#except

#slice の逆
特定のキーとその値を除外したい時に使える

hash = { key1: "value1", key2: "value2", key3: "value3" }

hash.except(:key3)
=> {:key1=>"value1", :key2=>"value2"}

Hash#fetch_values

#values_at と似たような挙動をするメソッド
キーが見つからなければ KeyError が発生する(#values_atnilが返る)
ブロックを渡すと KeyError の代わりに、その評価値が返る

hash = { key1: "value1", key2: "value2", key3: "value3" }

hash.fetch_values(:key1, :key2)
=> ["value1", "value2"]
hash.fetch_values(:key1, :key3)
KeyError: key not found: :key3
hash.fetch_values(:key1, :key3) { :not_found }
=> ["value1", :not_found]

Hash#assoc

Hash のキーを指定すると、引っかかった key-value の配列を返す
キーの指定は1つのみ

元々は、多次元配列に対して要素を取り出すのが目的な感じに見える(Arrayでも使えるし)

{ key1: "value1", key2: "value2" }.assoc(:key1)
=> [:key1, "value1"]
[[:key1, "value1"], [:key2, "value2"]].assoc(:key1)
=> [:key1, "value1"]

ちなみに、rassocというメソッドもあり、こちらはキーではなく値を引数に指定する