Help us understand the problem. What is going on with this article?

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

More than 5 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 に変換する

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

  • 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

  • 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実行

Screen Shot 2015-04-04 at 12.31.37.png

動きました。

備考

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

正規表現便利!!

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

詳説 正規表現 第3版

download.jpg

RSpecが苦手という方へ

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

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

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした