LoginSignup
10
6

More than 5 years have passed since last update.

僕も松江Ruby会議06で出された問題を解いてみた。

Posted at

はじめに

ソニックガーデンの @regonn 氏が 松江Ruby会議06で出された問題を解いてみた という記事を書いていたので、僕も解いてみました。
ただし、解いたのは「CSVのソート問題」だけです。

問題

以下のような名前とそれぞれの項目の得点を記したCSVファイルがあります。

ruby_1.csv
name,ruby,php,python,perl
一郎,100,40,70,80
二郎,60,90,80,10
三郎,90,60,60,60
四郎,80,70,70,80

rubyの得点を基準に降順に並べ替え、名前とrubyの得点を出力しなさい。

一郎,100
三郎,90
四郎,80
二郎,60

また、平均点を計算し降順に並べ替え、名前と平均点を出力しなさい。
(平均点が同じ場合はrubyの点数が高い方を先に表示すること)

四郎,75.0
一郎,72.5
三郎,67.5
二郎,60.0

僕の回答

元の問題ではネット上からCSVを取ってきたり、RubyのCSVモジュールを使ったりしていますが、僕の回答はCSVを単純な文字列として扱うことにしました。

また、独自のクラスを定義してオブジェクト指向っぽく解いています。

・・・あ、自分で解こうと思っている人はまだ僕のコードを見ない方がいいかも!?
僕のコードを見ても問題ない人だけ、先に進んでください↓

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
はい、それでは以下が僕の回答です!

def sort_by_ruby(csv)
  RecordSorter.new(csv, :ruby).sort
end

def sort_by_avg(csv)
  RecordSorter.new(csv, :average, :ruby).sort
end

class Record
  attr_reader :name, :points

  def self.parse_csv_row(text)
    name, *points = text.split(',')
    self.new(name, points.map(&:to_i))
  end

  def initialize(name, points)
    @name = name
    @points = points
  end

  def ruby
    points[0]
  end

  def sum
    points.inject(:+)
  end

  def average
    sum / points.size.to_f
  end
end

class RecordSorter
  attr_reader :csv, :sort_fields

  def initialize(csv, *sort_fields)
    @csv = csv
    @sort_fields = sort_fields
  end

  def sort
    records
        .sort_by { |record| values_for_sort(record) }
        .reverse
        .map { |record| to_row_text(record) }
        .join("\n")
  end

  private

  def records
    csv_body.map { |line| Record.parse_csv_row(line) }
  end

  def csv_body
    csv.lines[1..-1]
  end

  def values_for_sort(record)
    sort_fields.map { |f| record.send(f) }
  end

  def to_row_text(record)
    [record.name, record.send(point_for_display)].join(',')
  end

  def point_for_display
    sort_fields[0]
  end
end

テストコード

この問題はTDDで解きました。
以下が今回使ったテストコードです。
インプットとアウトプットが明確なプログラミング問題はTDDで解いた方が効率が良いです。

require 'minitest/autorun'

class CsvSortTest < Minitest::Test
  def original_csv
    <<-CSV
name,ruby,php,python,perl
一郎,100,40,70,80
二郎,60,90,80,10
三郎,90,60,60,60
四郎,80,70,70,80
    CSV
  end

  # Rubyの得点順でソートするテスト =======================

  def csv_sort_by_ruby
    <<-CSV
一郎,100
三郎,90
四郎,80
二郎,60
    CSV
  end

  def test_sort_by_ruby
    actual = sort_by_ruby(original_csv)
    assert_equal csv_sort_by_ruby.chomp, actual
  end

  # 平均点順でソートするテスト =======================

  def csv_sort_by_avg
    <<-CSV
四郎,75.0
一郎,72.5
三郎,67.5
二郎,60.0
    CSV
  end

  def test_sort_by_avg
    actual = sort_by_avg(original_csv)
    assert_equal csv_sort_by_avg.chomp, actual
  end

  # 平均点 + Rubyの得点順でソートするテスト =======================

  def original_csv_with_same_avg
    <<-CSV
name,ruby,php,python,perl
一郎,80,50,70,80
二郎,60,90,80,10
三郎,90,60,60,60
四郎,90,40,70,80
    CSV
  end

  def csv_sort_by_avg_and_ruby
    <<-CSV
四郎,70.0
一郎,70.0
三郎,67.5
二郎,60.0
    CSV
  end

  def test_sort_by_avg_and_ruby
    actual = sort_by_avg(original_csv_with_same_avg)
    assert_equal csv_sort_by_avg_and_ruby.chomp, actual
  end
end

回答コードの簡単な説明

回答のコードではRecordクラスとRecordSorterクラスの二つのクラスを定義しました。

RecordクラスはCSV一行分のデータを格納します。
またRubyの点数や平均点を返すインスタンスメソッドを用意しています。

RecordSorterクラスはRecordのソート処理を実行するクラスです。
インスタンス作成時に、CSVデータとソートで使うRecordクラスのフィールドを渡します。
また、CSVの各行は Record.parse_csv_row に渡されて、Recordクラスのインスタンスに変換されます。

RecordSorter#sortがメインロジックです。

  1. 指定されたフィールドの値でレコードをソートする
  2. 逆順に並び替える
  3. 各レコードを出力用にフォーマットする
  4. フォーマットした行データを改行文字で連結する

というのが大まかな処理の流れです。
sortメソッドの実装を見ると、上の流れがそのままの形で実装されています。

  def sort
    records
        .sort_by { |record| values_for_sort(record) }
        .reverse
        .map { |record| to_row_text(record) }
        .join("\n")
  end

ちなみに、ソートで使うフィールドの値や出力する点数はメタプログラミング(send)を使って取得しています。

  def values_for_sort(record)
    sort_fields.map { |f| record.send(f) }
  end

  def to_row_text(record)
    [record.name, record.send(point_for_display)].join(',')
  end

まとめ

メソッドを細かく分けてるので、全体的な行数は長くなっていますが、RecordSorter#sort以外はどれも1~2行しかないので、各メソッドで何をやっているのかは比較的理解しやすいはずです。

オブジェクト指向プログラミングを勉強したいと思っている人は、今回書いたコードをじっくり読むとオブジェクト指向らしいクラス設計の参考になるかもしれません。

それから、僕はいきなり最初からこのコードを書き上げたわけではなくて、最初は雑な実装でテストをグリーンにして、ちょっとずつリファクタリングを重ねました。
これぞTDDの「レッド・グリーン・リファクタ」のサイクルです。

「どんなときでもTDDで実装するべし」というTDD原理主義に陥ると逆に効率が悪くなるときがありますが、適材適所で使ってあげると非常に良い効果をもたらすと思います。

というわけで、シンプルでありながら、いろいろ勉強になる良いプログラミング問題だな、と思いました。
みなさんも一度自分で解いてみてください!

10
6
0

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
10
6