オイラープロジェクトを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にコメントを付ければ十分だ。