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] }
ではなぜだめなんだろうという疑問があると思います。確かにeach
もmap
も要素をループさせるので似ているのですが、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
に似ているのは気づきましたか? これらはけっこう役割も似ていますしどちらでも書けるときがあります。
詳しくはこちらを見てください。
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などを使うことができます。モジュールとは何かが分からない方はそれも一緒に理解してみるといいでしょう。