読者です 読者をやめる 読者になる 読者になる

アトラシエの開発ブログ

株式会社アトラシエのブログです

Ruby初心者にオススメ。Array・Hashの練習問題

tkotです。

弊社ではRuby・Railsを覚えたい初心者の方向けに学習用のプログラムを用意しています。基本的にはRailsで動くものを作っていく中で細かい文法やプログラミングのルールを覚えていこうという方針でやっています。

Railsはすぐ動くものができるのでとっつきやすいのですが、その分Rubyの基礎的な文法がおろそかになってしまいます。とくにArray・Hashの処理が鬼門です。

そこで次の練習問題をやってもらうことにしました。

目的

  • Array・Hashのデータ処理に慣れる。
  • Enumerableモジュールについて知る。

Q1

問題

{ a: 1, b: 2, c: 3, d: 4 }
# 上のHashから
[:a, :b, :c, :d]
# という結果を得てください。

解答

1. eachを使う方法

hash = { a: 1, b: 2, c: 3, d: 4 }
result = []
hash.each do |k,v|
  result << k
end
hash

新しい集合を返すものは上のように返したい集合を先に定義し、それを加工した上で返すという考え方ができればほとんどのケースで対応できます。

2. keysを使う方法

{ a: 1, b: 2, c: 3, d: 4 }.keys

といっても、Hashのキーだけを取り出したいなどはよくあるケースなので、上のようなショートカットメソッドがあることを知っておきましょう。valueだけ取り出したければhash.valuesで。

Q2

問題

[ 1, 2, 3, 4]
# という配列を
[ [1], [2], [3], [4] ]
# という配列に変換

解答

1.eachを使う方法

result = []
[1,2,3,4].each do |n|
  result << [n]
end
result

2.mapを使う方法

[1,2,3,4].map { |n| [n] }

集合をある変換法則に従って別の集合に変換する時はmapというメソッドが使えます。このときもとの集合と結果となる集合の要素数は同じになります。 もとの集合と結果の集合の要素数が同じ場合はmapが使えないか検討してみるといいでしょう。

また、初心者だと

[1,2,3,4].each { |n| [n] }

ではなぜだめなんだろうという疑問があると思います。確かにeachmapも要素をループさせるので似ているのですが、map{ ... }の中身の最終的な結果を保持して、その結果群を返します。eachのほうはそういう機能はありません。 これは重要なことなので理解しましょう。

Q3

問題

[ [:a, 1], [:b, 2], [:c, 3], [:d, 4] ]
# という配列を
{ a: 1, b: 2, c: 3, d: 4 }
# に変換

解答

eachを使う場合

hash = {}
[ [:a, 1], [:b, 2], [:c, 3], [:d, 4] ].each do |ary|
  key = ary[0]
  value = ary[1]
  hash[key] = value
end
hash

each_with_objectを使う場合

[ [:a, 1], [:b, 2], [:c, 3], [:d, 4] ].each_with_object({}) do |ary, hash|
  key = ary[0]
  value = ary[1]
  hash[key] = value
end

each_with_objectは概念が難しいのですが、ループを回しながら段階的にブロックの第二引数を加工して、新しい変数を返すメソッドです。 初期値はeach_with_objectの引数である{}から始まり、はじめのループではブロックローカル変数の ary, hashにはそれぞれ [:a, 1], {}が入ります。 ループでhashを加工するので、次のループを開始すると ary, hashには [:b, 2], { a: 1 }が入っています。 最終的に加工したhashが戻り値になります。

Hashクラスから直接生成する

実はこれでもいけたりします。

Hash[[ [:a, 1], [:b, 2], [:c, 3], [:d, 4] ]]

Q4

問題

[1,2,3]
# という配列を
[2,4,6]
# に変換

解答

1. eachを使う場合

result = []
[1,2,3].each do |n|
  result << n * 2
end
result

2.mapを使う場合

[1,2,3].map { |n| n*2 }

Q5

問題

[:a, :b, :c]
# を
[ { a: 1 }, { b: 2 }, { c: 3 } ]
# に変換する

解答

1. eachを使う

結果の配列にあるvalueである1,2,3をどのようにカウントするかがポイントです。

n = 1
result = []
[:a, :b, :c].each do |key|
  hash = {}
  hash[key] = n
  n += 1
end
hash

あとは変数をハッシュのキーにするにはどうすればいいかという点ですが、上のような方法でもいけますし、

{ :"#{key}" => n }

でも可能です。

2. each_with_indexとmapを使う

[:a, :b, :c].each_with_index.map { |n, idx| { :"#{n}" => idx + 1 } }

まずループのカウンタはeach_with_indexというメソッドで取得可能です。こうするとブロックの第二引数にはループカウンタが入るようになります。 で、普通はeach_with_indexの直後にブロックを渡すんですが、これをさらにmapでチェーンすることが可能です。こうすればmapでカウンタが使えるようになります。

Q6

問題

{ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
# を
[ :b, :d, :f ]
# に変換する(valueが偶数のkeyだけ返す)

解答

1. eachを使う場合

result  = []
{ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }.each do |key,value|
  if value % 2 == 0
    result << key
  end
end
result

2. selectを使う場合

{ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }.select { |_,v| v%2 == 0 }.keys

selectは集合の中から条件を満たしたものだけを選択して新しい集合を返してくれます。mapと違って新しく返ってくる結果は元のものとは要素数が一致するとは限りません。 これで新しいハッシュを返したあとに、Q1のようにキーだけにすればいいわけです。 ちなみにブロックローカル変数を渡す時、ブロックの中で使用しないものは_にしておくのがちょっとした慣例です。

Q7

問題

[1,2,3,4,5]
# を
120
# に変換(すべてかけ合わせる)

解答

1.eachを使う場合

result = 1
[1,2,3,4,5].each do |n|
  result = result * n
end
result

2.injectを使う場合

[1,2,3,4,5].inject(&:*)

injectは集合から一つの結果を返してくれます。 &:*という見慣れない文法が出てきましたが、これはブロックを省略して書く特殊な書き方になります。省略しない場合は

[1,2,3,4,5].inject { |n, result| result * n }

となります。これ、each_with_objectに似ているのは気づきましたか? これらはけっこう役割も似ていますしどちらでも書けるときがあります。 詳しくはこちらを見てください。

qiita.com

Q8

問題

{ taro: { result: true, score: 50 }, jiro: { result: false, score: 30 }, saburo: { result: true, score: 10 }, shiro: { result: false, score: 100 } }
# を
60
# に変換する(resultがtrueの人物のscoreを合計する)

解答

eachを使う場合

n = 0
hash = { taro: { result: true, score: 50 }, jiro: { result: false, score: 30 }, saburo: { result: true, score: 10 }, shiro: { result: false, score: 100 } }
hash.each { |k, v| n += v[:score] if v[:result] }
n

select, map, injectを使う

hash = { taro: { result: true, score: 50 }, jiro: { result: false, score: 30 }, saburo: { result: true, score: 10 }, shiro: { result: false, score: 100 } }
hash.select { |_,v| v[:result] }.map { |_,v| v[:score] }.inject(&:+)

まとめ

これくらいはRuby使いなら当たり前なんですが、意外と練習問題が存在しなかったのでまとめてみました。上で示したように、たいていはeachを使って実装できます。eachを基本としながら少しずつmapやselectに慣れていって、injectやeach_with_objectが自然と使えるようになってくると良いでしょう。

これらはEnumerableというモジュールで提供されていて、ArrayやHashのどちらでも使えますし、一定の条件を満たせば自分で作ったクラスでもmapなどを使うことができます。モジュールとは何かが分からない方はそれも一緒に理解してみるといいでしょう。

qiita.com