はじめに
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が苦手な方は以前僕が書いたこちらの入門記事をどうぞ。