はじめに
Ruby力向上のための基礎トレーニングが面白そうだったので僕も解いてみました。
問題
行単位、列単位で合計値を出しましょう、という問題です。
たとえばこういうインプットであれば、
| - | col1 | col2 | col3 | col4 | 
|---|---|---|---|---|
| row1 | 9 | 85 | 92 | 20 | 
| row2 | 68 | 25 | 80 | 55 | 
| row3 | 43 | 96 | 71 | 73 | 
| row4 | 43 | 19 | 20 | 87 | 
| row5 | 95 | 66 | 73 | 62 | 
こういう結果になります。
| - | col1 | col2 | col3 | col4 | sum | 
|---|---|---|---|---|---|
| row1 | 9 | 85 | 92 | 20 | 206 | 
| row2 | 68 | 25 | 80 | 55 | 228 | 
| row3 | 43 | 96 | 71 | 73 | 283 | 
| row4 | 43 | 19 | 20 | 87 | 169 | 
| row5 | 95 | 66 | 73 | 62 | 296 | 
| sum | 258 | 291 | 336 | 297 | 1182 | 
ただし、出題元のブログでは以下のような仕様になっていました。
- ランダムに数字を出力する
- 計算結果は以下のようなフォーマットで出力する
   9|  75|  83|  74| 241
   0|  27|  32|  48| 107
  51|  66|  76|   3| 196
   2|  37|  69|  85| 193
  55|  40|  25|  88| 208
 117| 245| 285| 298| 945
僕の解答例
先に僕の解答例を載せておきます。
require 'minitest/autorun'
module SumMatrix
  extend self
  def generate_sum_matrix(col: 4, row: 5, number_range: 1..99)
    matrix = generate_matrix(col: col, row: row, number_range: number_range)
    format_matrix(sum_matrix(matrix))
  end
  def sum_matrix(matrix)
    matrix
        .map{|row| [*row, row.inject(:+)] }
        .transpose
        .map{|row| [*row, row.inject(:+)] }
        .transpose
  end
  def format_matrix(matrix)
    size = matrix.last.max.to_s.length
    matrix.map{|row| format_row(row, size) }.join("\n")
  end
  def format_row(row, size)
    row.map{|col| col.to_s.rjust(size) }.join('|')
  end
  def generate_matrix(col: 4, row: 5, number_range: 1..99)
    Array.new(row){ number_range.to_a.sample(col) }
  end
end
class TestSumMatrix < Minitest::Test
  def test_sum_matrix
    input = [
        [9, 85, 92, 20],
        [68, 25, 80, 55],
        [43, 96, 71, 73],
        [43, 19, 20, 87],
        [95, 66, 73, 62]
    ]
    expected = [
        [9, 85, 92, 20, 206],
        [68, 25, 80, 55, 228],
        [43, 96, 71, 73, 283],
        [43, 19, 20, 87, 169],
        [95, 66, 73, 62, 296],
        [258, 291, 336, 297, 1182]
    ]
    assert_equal expected, SumMatrix.sum_matrix(input)
  end
  def test_format_matrix_max_400
    input = [
        [1, 2, 3, 4],
        [100, 200, 300, 400]
    ]
    expected = <<-TEXT.chomp
  1|  2|  3|  4
100|200|300|400
    TEXT
    assert_equal expected, SumMatrix.format_matrix(input)
  end
  def test_format_matrix_max_40
    input = [
        [1, 2, 3, 4],
        [10, 20, 30, 40]
    ]
    expected = <<-TEXT.chomp
 1| 2| 3| 4
10|20|30|40
    TEXT
    assert_equal expected, SumMatrix.format_matrix(input)
  end
  def test_generate_matrix
    matrix = SumMatrix.generate_matrix(col: 4, row: 5, number_range: 1..99)
    assert_equal 5, matrix.size
    assert matrix.all?{|row| row.size == 4 }
    assert matrix.flatten.all?{|n| n.between?(1, 99) }
    matrix2 = SumMatrix.generate_matrix(col: 4, row: 5, number_range: 1..99)
    assert matrix != matrix2, 'ランダムな結果が得られること'
  end
  # テストしたいというよりも結果が見たいだけ
  def test_generate_sum_matrix
    result = SumMatrix.generate_sum_matrix
    puts result
    assert result.is_a?(String)
  end
end
解説
以下は簡単な解説です。
テストコードを書く
こういう問題の場合は先にテストコードを書いておくのがよいと思いました。
テストコードを書いておけば、あとからどんどんリファクタリングできるからです。
いわゆるテスト駆動開発(TDD)ですね。
また、テストコードを読めば、どういう入力に対してどういう結果が返ってくるのかも他の人にとってわかりやすくなります。
というわけで、class TestSumMatrix < Minitest::Test以下はすべてテストコードになっています。
今回はMinitestを使ってテストコードを書きました。
Minitestを採用した理由は、Ruby標準でgemのインストールがいらないのと、テストコード自体が比較的単純で済むと思ったためです。
テストコードでもっと複雑な条件分けが必要になるのであれば、一番使い慣れているRSpecを採用したと思います。
3段階に処理を分ける
コードの処理は3段階に分かれています。
- ランダムな数字のデータを作成する
- 行単位、列単位に合計値を求める
- 表示形式を整える
それぞれ、以下のメソッドが対応しています。
- generate_matrix
- sum_matrix
- format_matrix
この流れは出題元のブログと同じですね。
最後に、それを一気に実行する便利メソッドとして、generate_sum_matrixを用意しました。
ランダムな数字データを作成する
コードはこちらです。
  def generate_matrix(col: 4, row: 5, number_range: 1..99)
    Array.new(row){ number_range.to_a.sample(col) }
  end
テストコードはこちらです。
  def test_generate_matrix
    matrix = SumMatrix.generate_matrix(col: 4, row: 5, number_range: 1..99)
    assert_equal 5, matrix.size
    assert matrix.all?{|row| row.size == 4 }
    assert matrix.flatten.all?{|n| n.between?(1, 99) }
    
    matrix2 = SumMatrix.generate_matrix(col: 4, row: 5, number_range: 1..99)
    assert matrix != matrix2, 'ランダムな結果が得られること'
  end
パラメータで行と列の大きさと、使用する数字の範囲を指定できるようになっています。
テストコードで検証しているのは以下の4点です。
- 行の大きさ
- 列の大きさ
- 各値の範囲
- ランダムに出力されること
行単位、列単位に合計値を求める
コードはこちらです。
  def sum_matrix(matrix)
    matrix
        .map{|row| [*row, row.inject(:+)] }
        .transpose
        .map{|row| [*row, row.inject(:+)] }
        .transpose
  end
テストコードはこちらです。
  def test_sum_matrix
    input = [
        [9, 85, 92, 20],
        [68, 25, 80, 55],
        [43, 96, 71, 73],
        [43, 19, 20, 87],
        [95, 66, 73, 62]
    ]
    expected = [
        [9, 85, 92, 20, 206],
        [68, 25, 80, 55, 228],
        [43, 96, 71, 73, 283],
        [43, 19, 20, 87, 169],
        [95, 66, 73, 62, 296],
        [258, 291, 336, 297, 1182]
    ]
    assert_equal expected, SumMatrix.sum_matrix(input)
  end
出題元のブログで書かれていたインプットとアウトプットと一致するかどうかを検証しました。
表示形式を整える
コードはこちらです。
  def format_matrix(matrix)
    size = matrix.last.last.max.to_s.length
    matrix.map{|row| format_row(row, size) }.join("\n")
  end
  def format_row(row, size)
    row.map{|col| col.to_s.rjust(size) }.join('|')
  end
テストコードはこちらです。
  def test_format_matrix_max_400
    input = [
        [1, 2, 3, 4],
        [100, 200, 300, 400]
    ]
    expected = <<-TEXT.chomp
  1|  2|  3|  4
100|200|300|400
    TEXT
    assert_equal expected, SumMatrix.format_matrix(input)
  end
  def test_format_matrix_max_40
    input = [
        [1, 2, 3, 4],
        [10, 20, 30, 40]
    ]
    expected = <<-TEXT.chomp
 1| 2| 3| 4
10|20|30|40
    TEXT
    assert_equal expected, SumMatrix.format_matrix(input)
  end
出題元のブログでは4桁固定で右寄せにしていましたが、僕のコードでは一番大きな数字(=一番右下の数字)に合わせて列幅を変えられるようにしています。
テストコードも3桁のケースと2桁のケースを用意してみました。
全部の処理を一気に実行する便利メソッド
コードはこちらです。
  def generate_sum_matrix(col: 4, row: 5, number_range: 1..99)
    matrix = generate_matrix(col: col, row: row, number_range: number_range)
    format_matrix(sum_matrix(matrix))
  end
テストコードはこちらです。
  # テストしたいというよりも結果が見たいだけ
  def test_generate_sum_matrix
    result = SumMatrix.generate_sum_matrix
    puts result
    assert result.is_a?(String)
  end
なんてことはありません、ただ順番にメソッドを呼び出しているだけです。
テストコードはコメントにも書いたとおり、putsで実行結果をコンソールに出したいがために作ったようなものです。
厳密に言えば、いろいろ検証すべき点もあると思いますが、ここでは単に実行結果が文字列として返ってくることだけを検証しています。
ちなみに、実行例はこんな感じになります。
  66|  34|  75|  87| 262
  71|  87|  91|  52| 301
  75|  48|  76|   1| 200
  67|  35|  29|  34| 165
  79|  40|   6|  26| 151
 358| 244| 277| 200|1079
その他
moduleをextendして、def hogeだけでクラスメソッドっぽく呼び出せるようにしています。
メソッド定義
module SumMatrix
  extend self
  
  def generate_sum_matrix(col: 4, row: 5, number_range: 1..99)
    # ...
呼び出し方
result = SumMatrix.generate_sum_matrix
コミットログ
GitHubにコードを置いているので、僕のコミットログを追いかけてみると面白いかもしれません。
ベースとなるコード(テストコードを含む)はだいたい30分ぐらいで作りました。
まとめ
出題元のブログで書かれているコードと比べたり、自分でコードを書いて比べたりするといろいろと勉強になるかもしれません。
興味深いブログを書いてくださった nanapi の hagiyat さんに感謝です!
あわせて読みたい
同じような系統の問題として、先日CodeIQに「ビンゴカード作成問題」というのを出しました。
こちらもよかったら解いてみてください。
2015.04.04追記 RSpecバージョンのテストコード
@toshi0607さんから「RSpec版もほしい」とリクエストされたので作ってみました。
describe SumMatrix do
  example "sum_matrix" do
    input = [
        [9, 85, 92, 20],
        [68, 25, 80, 55],
        [43, 96, 71, 73],
        [43, 19, 20, 87],
        [95, 66, 73, 62]
    ]
    expected = [
        [9, 85, 92, 20, 206],
        [68, 25, 80, 55, 228],
        [43, 96, 71, 73, 283],
        [43, 19, 20, 87, 169],
        [95, 66, 73, 62, 296],
        [258, 291, 336, 297, 1182]
    ]
    expect(SumMatrix.sum_matrix(input)).to eq expected
  end
  example "format_matrix_max_400" do
    input = [
        [1, 2, 3, 4],
        [100, 200, 300, 400]
    ]
    expected = <<-TEXT.chomp
  1|  2|  3|  4
100|200|300|400
    TEXT
    expect(SumMatrix.format_matrix(input)).to eq expected
  end
  example "format_matrix_max_40" do
    input = [
        [1, 2, 3, 4],
        [10, 20, 30, 40]
    ]
    expected = <<-TEXT.chomp
 1| 2| 3| 4
10|20|30|40
    TEXT
    expect(SumMatrix.format_matrix(input)).to eq expected
  end
  example "generate_matrix" do
    matrix = SumMatrix.generate_matrix(col: 4, row: 5, number_range: 1..99)
    expect(matrix.size).to eq 5
    expect(matrix.all?{|row| row.size == 4 }).to be_truthy
    expect(matrix.flatten.all?{|n| n.between?(1, 99) }).to be_truthy
    matrix2 = SumMatrix.generate_matrix(col: 4, row: 5, number_range: 1..99)
    expect(matrix != matrix2).to be_truthy, 'ランダムな結果が得られること'
  end
  # テストしたいというよりも結果が見たいだけ
  example "generate_sum_matrix" do
    result = SumMatrix.generate_sum_matrix
    puts result
    expect(result.is_a?(String)).to be_truthy
  end
end
コンバート方法
minitest_to_rspecっていうgemがあったので試してみましたが、エラーが出るので自分でコンバートしてみました。
コンバート方法は「RubyMine + 正規表現」と手作業です。
def test_xxx を example "xxx" do に変換する
- Find: /def test_(\w+)/
- Replace: example "$1" do
assert_equal xxx, yyy を expect(yyy).to eq xxx に変換する
- Find: /assert_equal (\w+), ([\w.()]+)/
- Replace: expect($2).to eq $1
assert xxx を expect(xxx).to be_truthy に変換する
- Find: /assert (.+)/
- Replace: expect($1).to be_truthy
ただし、以下の部分だけは手作業で変換しました。
# assert matrix != matrix2, 'ランダムな結果が得られること'
expect(matrix != matrix2).to be_truthy, 'ランダムな結果が得られること'
class TestSumMatrix < Minitest::Test を describe SumMatrix do に変換する
ここは1箇所しかないので手作業で変換しました。
(正規表現でもできなくはないが、手作業の方が速い)
# class TestSumMatrix < Minitest::Test
describe SumMatrix do
ファイル名を sum_matrix_spec.rb に変更してrspec実行
動きました。
備考
今回はかなり単純なテストコードだったので、正規表現でほとんどカバーできましたが、この手法で毎回コンバートできるとは限りませんので悪しからず。
正規表現便利!!
こんな感じで正規表現を活用するととても便利です!
正規表現を使いこなせていない人は「フクロウ本」で勉強しましょう。
僕もこれで正規表現を学びました。
RSpecが苦手という方へ
RSpecが苦手な方は以前僕が書いたこちらの入門記事をどうぞ。





