「Ruby力向上のための基礎トレーニング」をテストコード付きで解いてみた

## はじめに

[Ruby力向上のための基礎トレーニング](http://blog.nanapi.co.jp/tech/2015/04/02/basic_training_of_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 |

ただし、出題元のブログでは以下のような仕様になっていました。

- ランダムに数字を出力する
- 計算結果は以下のようなフォーマットで出力する

````text
   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
````

## 僕の解答例

先に僕の解答例を載せておきます。

```ruby
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段階に分かれています。

1. ランダムな数字のデータを作成する
2. 行単位、列単位に合計値を求める
3. 表示形式を整える

それぞれ、以下のメソッドが対応しています。

1. `generate_matrix`
2. `sum_matrix`
3. `format_matrix`

この流れは出題元のブログと同じですね。

最後に、それを一気に実行する便利メソッドとして、`generate_sum_matrix`を用意しました。

### ランダムな数字データを作成する

コードはこちらです。

```ruby
  def generate_matrix(col: 4, row: 5, number_range: 1..99)
    Array.new(row){ number_range.to_a.sample(col) }
  end
```

テストコードはこちらです。

```ruby
  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点です。

1. 行の大きさ
2. 列の大きさ
3. 各値の範囲
4. ランダムに出力されること

### 行単位、列単位に合計値を求める

コードはこちらです。

```ruby
  def sum_matrix(matrix)
    matrix
        .map{|row| [*row, row.inject(:+)] }
        .transpose
        .map{|row| [*row, row.inject(:+)] }
        .transpose
  end
```

テストコードはこちらです。

```ruby
  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
```

出題元のブログで書かれていたインプットとアウトプットと一致するかどうかを検証しました。

### 表示形式を整える

コードはこちらです。

```ruby
  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
```

テストコードはこちらです。

```ruby
  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桁のケースを用意してみました。

### 全部の処理を一気に実行する便利メソッド


コードはこちらです。

```ruby
  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
```

テストコードはこちらです。

```ruby
  # テストしたいというよりも結果が見たいだけ
  def test_generate_sum_matrix
    result = SumMatrix.generate_sum_matrix
    puts result
    assert result.is_a?(String)
  end
```

なんてことはありません、ただ順番にメソッドを呼び出しているだけです。

テストコードはコメントにも書いたとおり、putsで実行結果をコンソールに出したいがために作ったようなものです。

厳密に言えば、いろいろ検証すべき点もあると思いますが、ここでは単に実行結果が文字列として返ってくることだけを検証しています。

ちなみに、実行例はこんな感じになります。

```text
  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`だけでクラスメソッドっぽく呼び出せるようにしています。

メソッド定義

```ruby
module SumMatrix
  extend self
  
  def generate_sum_matrix(col: 4, row: 5, number_range: 1..99)
    # ...
```

呼び出し方

```ruby
result = SumMatrix.generate_sum_matrix
```

### コミットログ

GitHubにコードを置いているので、僕のコミットログを追いかけてみると面白いかもしれません。

https://github.com/JunichiIto/sum-matrix/commits/master

ベースとなるコード（テストコードを含む）はだいたい30分ぐらいで作りました。

## まとめ

出題元のブログで書かれているコードと比べたり、自分でコードを書いて比べたりするといろいろと勉強になるかもしれません。

興味深いブログを書いてくださった nanapi の hagiyat さんに感謝です！

[Ruby力向上のための基礎トレーニング](http://blog.nanapi.co.jp/tech/2015/04/02/basic_training_of_ruby/)


### あわせて読みたい

同じような系統の問題として、先日CodeIQに「ビンゴカード作成問題」というのを出しました。
こちらもよかったら解いてみてください。

- [Ruby初心者必見！？「ビンゴカード作成問題」のリファクタリング風景をお見せします](http://blog.jnito.com/entry/2015/03/06/090106)
- [「ビンゴカード作成問題」の優秀作品ベスト3を発表します！](http://blog.jnito.com/entry/2015/03/06/085433)

## 2015.04.04追記 RSpecバージョンのテストコード

@toshi0607さんから「[RSpec版もほしい](https://twitter.com/toshi0607/status/584034612487131136)」とリクエストされたので作ってみました。

```ruby
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](https://github.com/jaredbeck/minitest_to_rspec)っていうgemがあったので試してみましたが、エラーが出るので自分でコンバートしてみました。

コンバート方法は「RubyMine + 正規表現」と手作業です。

### def test_xxx を example "xxx" do に変換する

![Screen Shot 2015-04-04 at 12.18.30.png](https://qiita-image-store.s3.amazonaws.com/0/7465/80bbc8e5-43f0-ba02-4ec6-67b2eb190243.png "Screen Shot 2015-04-04 at 12.18.30.png")

- Find: `/def test_(\w+)/`
- Replace: `example "$1" do`

### assert_equal xxx, yyy を expect(yyy).to eq xxx に変換する

![Screen Shot 2015-04-04 at 12.21.50.png](https://qiita-image-store.s3.amazonaws.com/0/7465/fe533eb5-e752-e8b1-9a26-4870dedd89a4.png "Screen Shot 2015-04-04 at 12.21.50.png")

- Find: `/assert_equal (\w+), ([\w.()]+)/`
- Replace: `expect($2).to eq $1`

### assert xxx を expect(xxx).to be_truthy に変換する

![Screen Shot 2015-04-04 at 12.26.04.png](https://qiita-image-store.s3.amazonaws.com/0/7465/7033606b-3007-6f7d-abda-020000568709.png "Screen Shot 2015-04-04 at 12.26.04.png")

- Find: `/assert (.+)/`
- Replace: `expect($1).to be_truthy`

ただし、以下の部分だけは手作業で変換しました。

```ruby
# assert matrix != matrix2, 'ランダムな結果が得られること'
expect(matrix != matrix2).to be_truthy, 'ランダムな結果が得られること'
```

### class TestSumMatrix < Minitest::Test を describe SumMatrix do に変換する

ここは1箇所しかないので手作業で変換しました。
（正規表現でもできなくはないが、手作業の方が速い）

```ruby
# class TestSumMatrix < Minitest::Test
describe SumMatrix do
```

### ファイル名を sum_matrix_spec.rb に変更してrspec実行

![Screen Shot 2015-04-04 at 12.31.37.png](https://qiita-image-store.s3.amazonaws.com/0/7465/e1a83ef9-4dcc-db0e-beb0-3a20e1220eed.png "Screen Shot 2015-04-04 at 12.31.37.png")

動きました。

### 備考

今回はかなり単純なテストコードだったので、正規表現でほとんどカバーできましたが、この手法で毎回コンバートできるとは限りませんので悪しからず。

## 正規表現便利！！

こんな感じで正規表現を活用するととても便利です！
正規表現を使いこなせていない人は「フクロウ本」で勉強しましょう。
僕もこれで正規表現を学びました。

<a href="http://www.amazon.co.jp/gp/product/4873113598/ref=as_li_ss_tl?ie=UTF8&camp=247&creative=7399&creativeASIN=4873113598&linkCode=as2&tag=junic05-22">詳説 正規表現 第3版</a>

![download.jpg](https://qiita-image-store.s3.amazonaws.com/0/7465/6f6afb63-07dc-f2bf-b353-63cf0370e110.jpeg "download.jpg")

## RSpecが苦手という方へ

RSpecが苦手な方は以前僕が書いたこちらの入門記事をどうぞ。

[使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」](http://qiita.com/jnchito/items/42193d066bd61c740612)