give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

Ruby -「ビンゴカード作成問題」の優秀作品ベスト3を発表します! #codeiq

はじめに

先月、CodeIQにビンゴカード作成問題を出題しました。

CodeIQに「ビンゴカード作成問題」を出題しました。みなさんの挑戦をお待ちしてます! - give IT a try

f:id:JunichiIto:20150209100505p:plain:w400

このビンゴカード作成問題、ありがたいことに50人もの方が解答を送ってくれました。
挑戦してくださったみなさん、どうもありがとうございました。


今回のエントリではその中から「これはイケてる!」と僕が思ったコード・ベスト3を発表します!
加えて、僕が作った模範解答も紹介します。

おさらい「ビンゴカード作成問題」とは?

ビンゴカード作成問題とはその名の通り、Rubyを使ってビンゴカードを出力する問題です。
Bingo.generate_cardというメソッドを呼ぶと以下のような文字列を出力する、というのが要求仕様です。

 B |  I |  N |  G |  O
13 | 22 | 32 | 48 | 61
 3 | 23 | 43 | 53 | 63
 4 | 19 |    | 60 | 65
12 | 16 | 44 | 50 | 75
 2 | 28 | 33 | 56 | 68

ビンゴカードには作成のルールがあります。

  • 各列の値は以下の条件を満たすこと
    • B:1~15のどれか
    • I:16~30のどれか
    • N:31~45のどれか
    • G:46~60のどれか
    • O:61~75のどれか

その他にも次のような条件があります。

  • 毎回異なるカードを生成すること
  • どの数値も重複しないこと
  • 各列はパイプ(|)で区切ること
  • 数字や"BINGO"の文字は右寄せで出力すること
  • 真ん中(FREEになる場所)はスペースを出力すること
予め用意されたテストを全部パスすればクリア!

解答を提出する際はこちらで予め用意したRSpecのテストを全てパスさせる必要があります。
RSpecのテストを含む解答テンプレートについてはこちらに置いてます。

ビンゴカード作成問題・解答テンプレート

それでは優秀賞を発表します!

ではここから優秀作品ベスト3の発表です!
ちなみにベスト3は1位~3位のランクは付けずに、「3名とも同率1位」とします。

優秀作品その1・sadzさん
class Bingo
  def self.generate_card
    cols = %w(B I N G O).each_with_index.map do |key, i|
      [key] + [*(i * 15 + 1)..(i * 15 + 15)].sample(5)
    end
    cols[2][3] = " "
    cols.transpose.map do |row|
      row.map { |cell| cell.to_s.rjust(2) }.join(" | ")
    end.join("\n")
  end
end

僕からのコメント
おお、これはスマートなコードですね!!
ムダが全然ありません!
僕の解答例にも結構似ていて、あまり文句を付けるところがないです。
困ったなあ(苦笑)。

あえてコメントするなら、"end.join"になっているところでしょうか。
なんとなく、"}.join" になっている方が個人的にはしっくりきます。
まあ、個人的な趣味なので絶対NGというレベルの話ではありませんが。。。

優秀作品その2・おじけんさん
class Bingo
  def self.generate_card
    cols = (1..75).each_slice(15).map { |rng| rng.sample(5) }
    cols[2][2] = ""
    table = ["BINGO".chars] + cols.transpose
    table.map do |row|
      row.map { |v| v.to_s.rjust(2) }.join(' | ')
    end
    .join("\n")
  end
end

僕からのコメント
おー、これは素晴らしい!
というか、「あなたは僕ですか?」というぐらい僕の書いたコードと酷似していましたw

あえて書くとすれば、最後の部分は僕だったら、

table.map { |row|
  row.map { |v| v.to_s.rjust(2) }.join(' | ')
}.join("\n")

こう書くかな?っていうぐらいです。
end.join ってちょっと気持ち悪いんですよね~。
}.join って書きたくなってしまう。

とはいえ、ほぼ満点の回答だと思います!

優秀作品その3・ttakuru88さん
class Bingo
  FORMAT    = (["%2s"] * 5).join(' | ').freeze
  HEADER    = (FORMAT % %w(B I N G O)).freeze

  def self.generate_card
    numbers = (1..75).each_slice(15).map(&:shuffle).transpose.take(5)
    numbers[2][2] = nil

    body = numbers.map { |row_numbers| FORMAT % row_numbers }.join("\n")

    "#{HEADER}\n#{body}"
  end
end

僕からのコメント
おお、これはちょっとヒネりの効いた面白いロジックですね!
最初のFORMATとHEADERを見たときは「ん?何やってるんだろう??」と思ったんですが、FORMATを使って一気に一行分を整形してしまうという発想がすごいと思いました!

コードもすごくシンプルでほとんど文句の付けようがないんですが、それだと面白くないと思うので、僕の方でちょこっとリファクタリングしてみました。

 class Bingo
-  FORMAT    = (["%2s"] * 5).join(' | ').freeze
-  HEADER    = (FORMAT % %w(B I N G O)).freeze
+  FORMAT    = Array.new(5, '%2s').join(' | ').freeze
+  HEADER    = (FORMAT % 'BINGO'.chars).freeze
 
   def self.generate_card
-    numbers = (1..75).each_slice(15).map(&:shuffle).transpose.take(5)
+    numbers = (1..75).each_slice(15).to_a.transpose.sample(5)
     numbers[2][2] = nil
 
     body = numbers.map { |row_numbers| FORMAT % row_numbers }.join("\n")
 
-    "#{HEADER}\n#{body}"
+    [HEADER, body].join("\n")
   end
 end

冒頭の(["%2s"] * 5)はArray.new(5, '%2s')にした方が、ぱっと見たときの意図が伝わりやすい気がします。

shuffle + take は sample(n) に置き換えられます。(そのぶんto_aが入っていますが。。)

最後の"#{HEADER}\n#{body}"も[HEADER, body].join("\n")の方が、やりたいことがコードからつかみやすいかな、と思いました。

とはいえ、どれも「個人の趣味じゃね?」というレベルのものなので、絶対こっちの方がいい!という話ではないですね ^^;

僕の作った模範解答

では僕の作った模範解答もお見せします。
僕の書いたコードはこんな感じです。じゃーん!

class Bingo
  FORMAT = Array.new(5, '%2s').join(' | ')

  def self.generate_card
    ['BINGO'.chars, *numbers].map { |row| FORMAT % row }.join("\n")
  end

  def self.numbers
    (1..75).each_slice(15)
      .map { |sequence| sequence.sample(5) }
      .tap { |table| table[2][2] = '' }
      .transpose
  end
end
コードの解説

このコードを言葉で説明するとこんな感じになります。

  1. 1~15、16~31、... 61~75の数が並んだ、配列の配列(行列)を作る
    • (1..75).each_slice(15)
  2. それぞれの配列の中からランダムに5つずつ数字を選んだ配列の配列を作る
    • .map { |sequence| sequence.sample(5) }
  3. 真ん中を空白にする
    • .tap { |table| table[2][2] = '' }
  4. 行列の行と列を入れ替える
    • .transpose
    • ここまでがnumbersメソッド
  5. ヘッダー行の"BINGO"を1文字ずつ分割して配列にする
    • 'BINGO'.chars
  6. ヘッダー行と数字の配列をまとめて配列の配列(行列)にする
    • ['BINGO'.chars, *numbers]
  7. フォーマット指定用の文字列を用意する
    • FORMAT = Array.new(5, '%2s').join(' | ')
    • => "%2s | %2s | %2s | %2s | %2s" という文字列ができる
  8. 各行を右詰め、縦棒区切りにフォーマットする
    • .map { |row| FORMAT % row }
    • 例) row = [3, 22, 32, 48, 61] だと、" 3 | 22 | 32 | 48 | 61"になる
  9. 各行を改行文字で連結して一つの文字列にする
    • .join("\n")
  10. できあがり!

いかがでしょうか?
説明を読んでもピンと来ない場合はirbで実際に動かしながら確認してみてくださいね。

Special thanks to ttakuru88さん

「FORMAT % row」のところはttakuru88さんの解答を参考にさせてもらいました!
こんな書き方ができるとは僕も知りませんでした ^^;

まとめ

というわけで今回のエントリでは「ビンゴカード作成問題」の優秀作品ベスト3を紹介しました。
どの解答もシンプルかつDRYなコードになっていましたね!
sadzさん、おじけんさん、ttakuru88さん、素敵な解答をありがとうございました!!


さて、「ビンゴカード作成問題」でもうひとつみなさんに紹介したいお話があります。
このエントリにそれを書くとかなり長くなってしまいそうなので、その話は次のエントリに回しますね。


ひとまず今回のエントリはこれで終わりたいと思います。

後編はこちら!

後編のエントリでは解答例を僕がリファクタリングしている様子を動画で紹介しています。


Ruby初心者必見!?「ビンゴカード作成問題」のリファクタリング風景をお見せします #codeiq - give IT a try


PR: みなさんもCodeIQの問題に挑戦&出題してみませんか?

CodeIQでは様々な言語、様々なレベルのプログラミング問題が出題されています。
解答すれば出題者の方からのフィードバックがもらえたり、転職を考えている方には企業からのスカウトが来たりします。
ちょっとした自分の腕試しとして面白そうな問題を探して挑戦してみましょう!


また、挑戦者だけでなく、問題の出題者も募集しています。
「出題に興味がある」という方はぜひCodeIQのお問い合せなどからご連絡ください。

https://codeiq.jp/
f:id:JunichiIto:20140710084322p:plain:w400