はじめに
ソニックガーデンの @regonn 氏が 松江Ruby会議06で出された問題を解いてみた という記事を書いていたので、僕も解いてみました。
ただし、解いたのは「CSVのソート問題」だけです。
問題
以下のような名前とそれぞれの項目の得点を記した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
がメインロジックです。
- 指定されたフィールドの値でレコードをソートする
- 逆順に並び替える
- 各レコードを出力用にフォーマットする
- フォーマットした行データを改行文字で連結する
というのが大まかな処理の流れです。
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原理主義に陥ると逆に効率が悪くなるときがありますが、適材適所で使ってあげると非常に良い効果をもたらすと思います。
というわけで、シンプルでありながら、いろいろ勉強になる良いプログラミング問題だな、と思いました。
みなさんも一度自分で解いてみてください!