LoginSignup
0
0

More than 1 year has passed since last update.

行単位、列単位の合計値算出プログラムを実装する

Last updated at Posted at 2022-01-11

はじめに

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)のように範囲オブジェクトを使用するべきでした。

54等の決まった値は定数化が可能でした。

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_arraysizeを使用していますが、事前に定数化しておくと可読性が上がったと思います。

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メソッドはあまり使わない。
    • mapeachで出来ないか、処理を見返してみる。
  • 空配列を定義することはあまりない。
    • mapで出来ないか、処理を見返してみる。

まとめ

Rubyには便利なメソッドが多くあるので、今後はメソッドの使いどころを意識して、適切なタイミングで活用できるようになりたいと思います。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0