https://blog.jnito.com/entry/2019/05/03/121235
にて紹介されている、ビンゴカード作成問題を解いてみました。
詳しい問題仕様につきましては元リンク先をご参照ください。
さてこの記事ですが、単に作成した解答コードを載せるだけですと既に同じような記事がいくつもQiita上にあるのではと思ったので、コード記述のステップを詳しく書いてみることで差別化を図っています。
またRubyの実行はpaiza.ioで行っています。(Ruby 2.5.3)
大雑把にまず考えた自分の解答までの道筋は
①「1..15」「16..30」…「61..75」の各配列を作成。
②各配列の中から、重複が出ないように数字を5個ずつ取り出す。
(例:[1,4,7,10,13],[16,19,22,25,28],……,[61,64,67,70,73])
③各配列の1番目の要素、2番めの要素…を順々に組み合わせる。
(例:[1,16,31,46,61],[4,19,34,49,64],……,[13,28,43,58,73])
④以下の条件を加えつつ出力
・パイプ(|)で区切った文字列として出力する
・3行目の3番目の要素は空文字(ビンゴのFreeに当たるため)
・1桁の数字は右詰め
という感じでした。
##①
連続する数字の配列は、配列([])の中でRangeクラスのオブジェクトを*を付けて展開することで作成できます。
[*1..15] # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
今回取得したい5種類の配列は二次元配列の形で取得します。
[[*1..15],[*16..30],[*31..45],[*46..60],[*61..75]]
##②
①で作成した各配列から、ランダムに5個の要素を取り出します。
配列から指定した個数の要素をランダムに取り出すにはArray#sampleメソッドを使います。
[*1..15].sample(5) # => [12, 7, 4, 11, 10]
[*1..15].sample(5) # => [6, 9, 10, 4, 3]
道筋で想定したように、取り出した要素から新しく二次元配列を作るにはEnumerable#mapを使います(余談ですが、map派です)。
[[*1..15],[*16..30],[*31..45],[*46..60],[*61..75]].map{|s| s.sample(5)}
# => [[12, 7, 4, 15, 3], [24, 30, 26, 29, 20],
# [32, 39, 33, 43, 34], [50, 56, 55, 46, 53], [68, 67, 63, 70, 65]]
##③
お次は、上で作成した各配列から1つ目の要素、2つ目の要素…をそれぞれ取得して各要素ごとの新しい配列を作成します。
が、正にその動きをするメソッドの存在自体は知っているのに名前を忘れるという失態。
力技で切り抜けようかとも思いましたが、少し考えても良い感じの書き方を見つけられなかったのでググってしまいました…
探していたのはArray#transposeです。
[[*1..15],[*16..30],[*31..45],[*46..60],[*61..75]].map{|s|
s.sample(5)
}.transpose
# => [[13, 18, 35, 49, 75], [11, 19, 42, 53, 61],
# [8, 27, 43, 59, 71], [9, 20, 44, 56, 65], [15, 24, 34, 51, 74]]
これで出力する番号の元になる数字たちが作れました。
##④
後は要件に沿って出力するだけなのですがこれがまた厄介。
まず、Bingoクラスのクラスメソッドとして実装する必要があります。
class Bingo
def self.generate_card
puts " B| I| N| G| O"
# ここにロジックを移植していく
end
end
次に3行目の3番目の要素はビンゴのFreeに当たるので空文字に置換する必要があります。
class Bingo
def self.generate_card
puts " B| I| N| G| O"
[[*1..15],[*16..30],[*31..45],[*46..60],[*61..75]].map{|s|
s.sample(5)
}.transpose.each_with_index{|array, i|
array[2] = ' ' if i == 2 # Free部分置換
# ここで1行ずつ出力していく
}
end
end
最後に1桁の数字は右詰め&&各数字をパイプで区切って出力するように調整します。
class Bingo
def self.generate_card
puts " B| I| N| G| O"
[[*1..15],[*16..30],[*31..45],[*46..60],[*61..75]].map{|s|
s.sample(5)
}.transpose.each_with_index{|array, i|
array[2] = ' ' if i == 2
puts array.map{|s| s.to_s.size < 2 ? " #{s}" : s}.join('|')
}
end
end
Bingo.generate_card
# => B| I| N| G| O
# 9|20|34|46|64
# 13|27|33|47|72
# 5|21| |58|70
# 12|24|38|55|67
# 2|23|44|56|66
一旦条件は満たせたように思えます。
うーん、とはいえ最後のFree置換と自力で判定している数字の右詰めがかなり苦しいですね…
##もう少し調べてみた
自力での解答はここまでにして(ググってるけど)、より良い書き方がないか調べてみました。
数字の右詰めに関しては、わざわざ自分で判定せずともString#rjustメソッドという素晴らしい解答がありました。
class Bingo
def self.generate_card
puts " B| I| N| G| O"
[[*1..15],[*16..30],[*31..45],[*46..60],[*61..75]].map{|s|
s.sample(5)
}.transpose.each_with_index{|array, i|
array[2] = ' ' if i == 2
puts array.map{|s| s.to_s.rjust(2)}.join('|')
}
end
end
Bingo.generate_card
すこし綺麗になりましたね♪
とはいえまだまだリファクタリング出来ると思いますのでアドバイス、修正すべき点などありましたらコメント頂けると幸いです。
今後も少しずつ別の問題にもチャレンジしていこうと思います。