オイラープロジェクトをRSpecを使ったテスト駆動っぽく
オイラープロジェクト17問目。1〜1000までの数字について、「345 -> three hundred and forty-five」のように文字に開いたとき、使われているアルファベットの文字数の合計を求めよという問題。
RSpecでやってみた。イテレーションが短いのが好きなので、最初は1桁だけで、1 -> one、2 -> two をやり、次に 11 -> eleven という風に桁をあげていく感じでテストコードと本体を膨らましていったら、非常に見通しの悪いコードになった。
class Fixnum single_digit = %w(one two three four five six seven eight nine) teen_numbers = %w(ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen) double_digit = %w(twenty thirty forty fifty sixty seventy eighty ninety) define_method(:double_digit_to_words) do case when self < 10 single_digit[self - 1] when self >= 10 && self < 20 teen_numbers[self - 10] when self >= 20 && self < 100 if self % 10 == 0 double_digit[self / 10 - 2] else [double_digit[self / 10 - 2], single_digit[(self % 10)- 1]].join("-") end end end def to_words if self < 100 self.double_digit_to_words elsif self >=100 && self < 1000 if (self % 100 == 0) (self / 100).double_digit_to_words + " hundred" else (self / 100).double_digit_to_words + " hundred and " + (self % 100).double_digit_to_words end elsif self == 1000 "one thousand" end end end if __FILE__ == $0 sum = 0 (1..1000).each do |i| puts i.to_words sum += i.to_words.gsub(/[ \-]/, "").size end puts sum end
20とか100という境界上の怪しそうなケースについて最初はエラーが出ていたのをテストケースに追加しつつ修正した。いちいちテストを書くのとか超めんどくさいし、はっきり言って画面に出力される文字列を眺めているほうがよっぽどエラーに気付くのも早いと思った。
だけど、いったんテストケースを並べて動くコードができてしまうと、構造を変える変更は確かに楽かもなと思った。上のコードを書き終わってから、重複が多いなと思って再帰で書き直した。
class Fixnum single_digit = %w(one two three four five six seven eight nine) teen_numbers = %w(ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen) double_digit = %w(twenty thirty forty fifty sixty seventy eighty ninety) define_method(:to_words) do case when self > 1000 raise "too big" when self == 1000 "one thousand" when 100 <= self && self < 1000 hundred = self / 100 lower = self % 100 hundred.to_words + if lower == 0 " hundred" else " hundred and " + lower.to_words end when 20 <= self && self < 100 upper = self / 10 lower = self % 10 double_digit[upper - 2] + if lower == 0 "" else "-" + lower.to_words end when 10 <= self && self < 20 teen_numbers[self - 10] when 0 < self && self < 10 single_digit[self - 1] end end end if __FILE__ == $0 sum = (1..1000).inject(0) do |acc, i| acc += i.to_words.gsub(/[ \-]/, "").size end puts sum end
テストが真っ赤になっても、ビミョウなtypoを修正すると、一気にグリーンになるのは気分がいい。で、エッジケースだけ再び赤くなったりしていて、「ああ、そうそう不等号に漏れがあった」という感じで修正できる。細かくリファクタリングする場合も、安心感があって、なるほどテスト駆動開発ってそういうことなのかなと思った。
RSpecで書き下したテストケースは、
require './p17.rb' describe Fixnum, "#to_words" do describe "single digit" do it "return one for 1" do 1.to_words.should == "one" end it "return two for 2" do 2.to_words.should == "two" end it "return nine for 9" do 9.to_words.should == "nine" end end describe "double digit" do it "return eleven for 11" do 11.to_words.should == "eleven" end it "return twenty" do 20.to_words.should == "twenty" end it "return twenty-two" do 22.to_words.should == "twenty-two" end it "return thirty-four" do 34.to_words.should == "thirty-four" end it "return ninety-eight" do 98.to_words.should == "ninety-eight" end end describe "more than triple digit" do it "return one hundred and twenty-three for 123" do 123.to_words.should == "one hundred and twenty-three" end it "return one hundred and twenty-three for 100" do 100.to_words.should == "one hundred" end it "return three hundred and forty-five for 345" do 345.to_words.should == "three hundred and forty-five" end it "return one thousand for 1000" do 1000.to_words.should == "one thousand" end end describe "check the given examples" do it "retrun 23" do 342.to_words.gsub(/[ \-]/, "").size == 23 end it "retrun 20" do 115.to_words.gsub(/[ \-]/, "").size == 20 end end end
RSpecじゃなくてもいいような気がする。というか、describeとか打つのめんどくさい。この例だとTest::Unitにコメントを付ければ十分だ。