はじめに
Ruby初心者向けのプログラミング問題を集めてみたの問題を解いてみました。
今回は、Excelのように、行・列の格子状になるランダムな数値を出力し、行・列単位で合計値を算出し、出力するプログラムにチャレンジしました。
最終的には、以下のような形式にフォーマットしたいです。
9| 75| 83| 74| 241
1| 27| 32| 48| 108
51| 66| 76| 3| 196
2| 37| 69| 85| 193
55| 40| 25| 88| 208
118| 245| 285| 298| 945
要求仕様
- 計算のベースとなる配列を作成する
- フォーマットし出力した際に、5 × 4 となるような配列であること
- 配列の値は
1〜99
のランダムな値であること
- ベースとなる配列を元に、行・列で合計値を算出し、例の通りフォーマットされた表を出力する
- 文字列で出力すること
自分が書いたソースコード
class SumMatrix
def call
srand(5)
sum_rows_array = []
sum_columns_array = []
result = ''
calculate_row(sum_rows_array)
calculate_column(sum_rows_array, sum_columns_array)
format_matrix(sum_rows_array << sum_columns_array, result)
result
end
private
def calculate_row(sum_rows_array)
5.times do
base_array = []
4.times do
base_array << rand(100)
end
base_array << base_array.sum
sum_rows_array << base_array
end
end
def calculate_column(sum_rows_array, sum_columns_array)
sum_rows_array.size.times do |number|
base_array = []
sum_rows_array.each do |array|
base_array << array[number]
end
sum_columns_array << base_array.sum
end
end
def format_matrix(matrix_array, result)
matrix_array.each do |array|
array.each_with_index do |number, index|
string = if index == array.size - 1
"#{number}\n"
else
"#{number}|"
end.rjust(5, ' ')
result << string
end
end
end
end
1. 行・列のランダムな数値を作成
def calculate_row(sum_rows_array)
5.times do
base_array = []
4.times do
base_array << rand(100)
end
base_array << base_array.sum
sum_rows_array << base_array
end
end
処理について
calculate_row
メソッドで、ランダムな整数からなる5 × 4の配列を作成し、行の合計値を算出しています。
times
メソッドを使用して5 × 4の配列を作成しています。
改善点
まず、メソッド呼び出しで引数の状態が変わることはあまり良くないことですが、引数のsum_rows_array
に対して、破壊的に変更を加えてしまっています。
また、base_array
で空配列を定義していますが、mapを使うとbase_array
は不要になりますね。
引数でsum_rows_array
の空配列を受け取っていますが、こちらもmapを使用すると引数は不要になります。
rand(100)
としていますが、この場合だと0~99
のランダム整数が生成されてしまうため、要件(1~99
)と異なってしまっています。
要件を満たすためには、rand(1..99)
のように範囲オブジェクトを使用するべきでした。
5
や4
等の決まった値は定数化が可能でした。
2. 行・列で合計値を算出する
def calculate_column(sum_rows_array, sum_columns_array)
sum_rows_array.size.times do |number|
base_array = []
sum_rows_array.each do |array|
base_array << array[number]
end
sum_columns_array << base_array.sum
end
end
処理について
calculate_column
メソッドで、列の合計値、また行・列の合計値を算出しています。
改善点
こちらもcalculate_row
メソッドと同様で、引数に対して破壊的に変更を加えてしまっています。
また、times
メソッドではなく、each
でループを回すだけでよかったと思います。
また、sum_rows_array
のsize
を使用していますが、事前に定数化しておくと可読性が上がったと思います。
3. フォーマットする
def format_matrix(matrix_array, result)
matrix_array.each do |array|
array.each_with_index do |number, index|
string = if index == array.size - 1
"#{number}\n"
else
"#{number}|"
end.rjust(5, ' ')
result << string
end
end
end
処理について
作成した配列を、指定の形式の文字列にフォーマットします。
改善点
each_with_index
に引数を設定していませんが、each.with_index(1)
のように、with_index
で初期値を1
で設定するとarray.size - 1
しなくても良くなるため、こちらも可読性が上がったと思います。
回答例
先輩にいただいた回答例です。
class SumMatrix
NUMBER_RANGE = 1..99
ROW_LENGTH = 5
COL_LENGTH = 4
SEPARATE_STRING = '|'.freeze
def generate_matrix
Array.new(ROW_LENGTH) { NUMBER_RANGE.to_a.sample(COL_LENGTH) }
end
def sum_matrix(matrix)
row_total_array = matrix.map { |row| row + [row.sum] }
col_total_array = row_total_array.transpose.map(&:sum)
row_total_array + [col_total_array]
end
def format_matrix(matrix)
matrix.map(&method(:format_row)).join("\n")
end
private
def format_row(row)
row.map(&method(:format_number)).join(SEPARATE_STRING)
end
def format_number(number)
format('%4d', number)
end
end
1. 定数化する
NUMBER_RANGE = 1..99
ROW_LENGTH = 5
COL_LENGTH = 4
SEPARATE_STRING = '|'.freeze
まず、決まった値は定数化されています。
定数化するメリットは以下になります。
- 定数名が付くので、その数字が何を意味しているのかが分かり易い。
- (複数箇所で利用している場合)改修のコストが下がる。(修正漏れによるデグレ回避が可能)
2. 行・列のランダムな数値を作成
def generate_matrix
Array.new(ROW_LENGTH) { NUMBER_RANGE.to_a.sample(COL_LENGTH) }
end
1行で5 × 4の配列が作成されています。
配列の長さに指定がある場合は、Array.new
を使用すると良いことが分かりました。
-
Array.new(size) {|index| ... }
- 長さ
size
の配列を生成し、各要素のindex
を引数としてブロックを実行し、各要素の値をブロックの評価結果に設定します。
- 長さ
ブロック内では、to_a
で配列に変換し、sample
メソッドを使用されています。
-
sample(n)
- 配列から
n
個の要素をランダムに選んで返します。 - 重複した
index
は選択されません。
- 配列から
3. 行・列で合計値を算出する
def sum_matrix(matrix)
row_total_array = matrix.map { |row| row + [row.sum] }
col_total_array = row_total_array.transpose.map(&:sum)
row_total_array + [col_total_array]
end
sum_matrix
の引数に5 × 4の配列を渡すと、行・列の合計値が算出されます。
row_total_array
に対してtranspose
し、それぞれの配列で合計値を出しているところがポイントと思いました。
-
transpose
- ネストされた配列に対して、行と列の入れ換えを行います。
(transpose
の使用例)
p [[1,2],
[3,4],
[5,6]].transpose
# => [[1, 3, 5], [2, 4, 6]]
また、sum_matrix
の別の方法もメモしておきます。
(伊藤淳一さんの回答をお借りしました。)
def sum_matrix(matrix)
matrix
.map{|row| [*row, row.inject(:+)] }
.transpose
.map{|row| [*row, row.inject(:+)] }
.transpose
end
こちらは、配列展開を使用されています。
*row
で配列(row
)を展開し、inject
メソッドが実行されています。
4. フォーマットする
def format_matrix(matrix)
matrix.map(&method(:format_row)).join("\n")
end
private
def format_row(row)
row.map(&method(:format_number)).join(SEPARATE_STRING)
end
def format_number(number)
format('%4d', number)
end
format_matrix
の引数に完成した配列を渡すと、フォーマットされた文字列が返ります。
&method(:メソッド名)
で、配列の要素1つ1つに対して、method
で与えられたメソッドを呼んでいます。
&method
を使わずに書くと以下のようになります。
row.map { |number| format_number(number) }
&method
を使用すると、proc
が省略できるため、簡潔に書けて良いですね。
format
メソッドについてもメモしておきます。
-
format(format, *arg)
-
format
: フォーマット文字列(変換したい書式を指定する。) -
arg
: フォーマットされる値
-
今回は、%4d
で4桁に桁数が揃えられています。
学んだこと
- 要件を意識する。
-
srand(5)
で結果が固定値になっていたり、ランダムな数値の範囲が異なってしまっていたこと等、要件を見たせていないことがあったため、要件を理解し、満たすようにする。
-
- テストファーストを心がける。
- RSpecを先に書くことで、要件を満たすプログラムを作成することが出来る。
- 変数や引数に渡したものに対して、破壊的メソッドは(基本的に)使わない。
- 本当に破壊的メソッドを使うしかないのか、処理を見返してみる。
- 決まった値は、定数化する。
- 1箇所で記載されているため、改修コストが下がる。
-
times
メソッドはあまり使わない。-
map
やeach
で出来ないか、処理を見返してみる。
-
- 空配列を定義することはあまりない。
-
map
で出来ないか、処理を見返してみる。
-
まとめ
Rubyには便利なメソッドが多くあるので、今後はメソッドの使いどころを意識して、適切なタイミングで活用できるようになりたいと思います。