数字を英名に変換するメソッドをRubyでテスト駆動開発する

はじめに
---
普段は自分のブログサイトで色々投稿しているのですが、
思い切ってQiitaに初投稿してみました。

Project Eulerの問17を Ruby と Minitest を用いて解いてみたという内容になります。

Project Euler とは
---
Project Euler（プロジェク オイラー）と読みます。
数学の世界で有名なオイラー関数のオイラーさんが語源でしょうか。

その名の通り数学的なプログラムが掲載されている問題集サイトです。
2018年11月16日時点で632問が掲載されています。
会員登録して回答を提出していくとレベルが上っていくきます。

問題を解くために利用するプログラミング言語は何を使っても良いのですが
私はRubyを用いて回答しています。

開発環境
---
```
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
minitest (5.10.3)
```

問題文
---
>If the numbers 1 to 5 are written out in words: one, two, three, four, five, then there are 3 + 3 + 5 + 4 + 4 = 19 letters used in total.

>If all the numbers from 1 to 1000 (one thousand) inclusive were written out in words, how many letters would be used?

>NOTE: Do not count spaces or hyphens. For example, 342 (three hundred and forty-two) contains 23 letters and 115 (one hundred and fifteen) contains 20 letters. The use of "and" when writing out numbers is in compliance with British usage.

日本語訳も引用して掲載します。
>1 から 5 までの数字を英単語で書けば one, two, three, four, five であり, 全部で 3 + 3 + 5 + 4 + 4 = 19 の文字が使われている.

>では 1 から 1000 (one thousand) までの数字をすべて英単語で書けば, 全部で何文字になるか.

>注: 空白文字やハイフンを数えないこと. 例えば, 342 (three hundred and forty-two) は 23 文字, 115 (one hundred and fifteen) は20文字と数える. なお, "and" を使用するのは英国の慣習.

回答
---
### 問題の特徴を整理
- 1 から 1000 までの数字をそれぞれ英名に変換
- その英名にハイフンや半角ペースがあれば除去
- 各英名の文字数をカウントして足し上げる

そもそも数字を英名に変えてくれるメソッドなんてRubyにはないのでは・・・
ということでメソッドを自作して対応しました。

せっかくメソッドを作るならば
テスト駆動開発の練習台としても利用しようということで
ここからはテスト駆動開発です。

### メソッドの仮定義
以下のようにIntegerクラスに対して`to_l`メソッドを定義することで
その英名を返すようにします。

とりあえず中身は`one`が返るように仮置きしてテストを通しました。

```ruby:test_problem.rb
require 'minitest/autorun'
require './problem'

class ProblemTest < Minitest::Test
  def test_problem
    assert_equal 'one', 1.to_l
    assert_equal 3, 1.to_l.size
  end
end
```
```ruby:problem.rb
class Integer
  def to_l
    'one'
  end
end
```

### 20までの数字英名変換をcase文で定義
21以降の数字の1の位は1~9で繰り返しですし、
101以降の数字の下2桁は99までの繰り返しなので

- 20までの英名を定義
- 99まで拡張
- 999まで拡張

の3段階で考えながら開発しました。

```ruby:test_problem.rb
assert_equal 19, number_letter_counts(from: 1, to: 5)
```
`number_letter_counts`メソッドに対して、
始まりと終わりの2数を引数に持たせることで
求める文字数が返ることを期待します。

```ruby:problem.rb
class Integer
  def to_l
    case self
    when 1
      'one'
    when 2
      'two'
    when 3
      'three'
    when 4
      'four'
    when 5
      'five'
    when 6
      'six'
    when 7
      'seven'
    when 8
      'eight'
    when 9
      'nine'
    when 10
      'ten'
    when 11
      'eleven'
    when 12
      'twelve'
    when 13
      'thirteen'
    when 14
      'fourteen'
    when 15
      'fifteen'
    when 16
      'sixteen'
    when 17
      'seventeen'
    when 18
      'eighteen'
    when 19
      'nineteen'
    when 20
      'twenty'
    end
  end
end

def number_letter_counts(from:, to:)
  (from..to).map(&:to_l).map(&:size).inject(&:+)
end
```
20まではcase文でそれぞれ定義しているだけです。

`number_letter_counts`メソッドの中身は
2つの引数を始点・終点とした範囲オブジェクトを`map`メソッドで英名に変換した配列にし
さらに英名文字数をそれぞれ`size`メソッドでカウントし
`inject`メソッドで足し上げる処理としました。

とりあえずこの段階でテストは通ります。

### 21~99の処理
```ruby:test_problem.rb
assert_equal 9, number_letter_counts(from: 21, to: 21)
```
21の英名は`twenty-one`ですが、
ハイフンは数えないとのことなので
その文字数は 9 となります。

このままではテストが通らないのでメソッドを変更します。

30, 40, 50, 60, 70, 80, 90はこれまで同様に定義した上でその他の数字については
レシーバを引数で割った時の商と余りを配列で返してくれる`divmod`メソッドを多重代入で利用しました。

引数を10とする`divmod`メソッドによって**商が10の位の数字、余りが1の位の数字**になるので、
それぞれを自作した`to_l`メソッドで英名に変換してハイフンでつなぎました。

```ruby:problem.rb
when (21..99)
  quotient, remainder = divmod(10)
  tens_place = quotient * 10
  tens_place.to_l + '-' + remainder.to_l
```

ハイフンを削除出来ておらずテスト結果は 10 となり落ちてしまうので
文字数カウントする前にハイフンがあれば削除するようにメソッドを変更しました。

```ruby:problem.rb
def number_letter_counts(from:, to:)
  (from..to).map(&:to_l).map { |i| i.delete('-') }.map(&:size).inject(&:+)
end
```

### 100~999の処理
基本的には繰り返しです。

101 であれば one hundred and one となります。
ただし、200 など ●00 という数字については two hundred という形式で終わります。

```ruby:test_problem.rb
assert_equal 'one hundred and one', 101.to_l
assert_equal 'two hundred', 200.to_l
assert_equal 16, number_letter_counts(from: 101, to: 101)
```

今回は`divmod`メソッドの引数を 100 として
余りが0になるかどうかで返り値は場合分けをしました。

```ruby:problem.rb
when (100..999)
  quotient, remainder = divmod(100)
  if remainder.zero?
    quotient.to_l + ' hundred'
  else
    quotient.to_l + ' hundred and ' + remainder.to_l
  end
```

文字数カウントする前に、**ハイフンまたは半角スペースを削除**しないといけないので、
`delete`メソッドを**正規表現を用いた`gsub`メソッド**に変更しています。

```ruby:problem.rb
def number_letter_counts(from:, to:)
  (from..to).map(&:to_l).map { |i| i.gsub(/-|\s/, '') }.map(&:size).inject(&:+)
end
```

これでテストが通るので
最後に1000を個別に定義して完了としました。

### 最終的なコード
```ruby:problem.rb
class Integer
  def to_l
    case self
    when 1
      'one'
    when 2
      'two'
    when 3
      'three'
    when 4
      'four'
    when 5
      'five'
    when 6
      'six'
    when 7
      'seven'
    when 8
      'eight'
    when 9
      'nine'
    when 10
      'ten'
    when 11
      'eleven'
    when 12
      'twelve'
    when 13
      'thirteen'
    when 14
      'fourteen'
    when 15
      'fifteen'
    when 16
      'sixteen'
    when 17
      'seventeen'
    when 18
      'eighteen'
    when 19
      'nineteen'
    when 20
      'twenty'
    when 30
      'thirty'
    when 40
      'forty'
    when 50
      'fifty'
    when 60
      'sixty'
    when 70
      'seventy'
    when 80
      'eighty'
    when 90
      'ninety'
    when (21..99)
      quotient, remainder = divmod(10)
      tens_place = quotient * 10
      tens_place.to_l + '-' + remainder.to_l
    when (100..999)
      quotient, remainder = divmod(100)
      if remainder.zero?
        quotient.to_l + ' hundred'
      else
        quotient.to_l + ' hundred and ' + remainder.to_l
      end
    when 1000
      'one thousand'
    end
  end
end

def number_letter_counts(from:, to:)
  (from..to).map(&:to_l).map { |i| i.gsub(/-|\s/, '') }.map(&:size).inject(&:+)
end

puts number_letter_counts(from: 1, to: 1000)
#=> 21124
```

このような形でQiitaにも投稿していこうと思います。

参考文献
---
[1] Project Euler：https://projecteuler.net/problem=17
[2] 日本語訳：http://odz.sakura.ne.jp/projecteuler/index.php?cmd=read&page=Problem%2017
[3] divmod：https://docs.ruby-lang.org/ja/latest/method/Numeric/i/divmod.html