Edited at

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

More than 3 years have passed since last update.


はじめに

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


  1. ランダムな数字のデータを作成する

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

  3. 表示形式を整える

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


  1. generate_matrix

  2. sum_matrix

  3. 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点です。


  1. 行の大きさ

  2. 列の大きさ

  3. 各値の範囲

  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にコードを置いているので、僕のコミットログを追いかけてみると面白いかもしれません。

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

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


まとめ

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

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

Ruby力向上のための基礎トレーニング


あわせて読みたい

同じような系統の問題として、先日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実行

動きました。


備考

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


正規表現便利!!

こんな感じで正規表現を活用するととても便利です!

正規表現を使いこなせていない人は「フクロウ本」で勉強しましょう。

僕もこれで正規表現を学びました。

詳説 正規表現 第3版


RSpecが苦手という方へ

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

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」