数字を英名に変換するメソッドをテスト駆動開発で開発する(Project Euler 問17)

スポンサーリンク

今回の問題

Number letter counts

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にはないので骨が折れました。
練習にもなると思ったのでテスト駆動開発も行ってみました。

仮メソッドの定義

class Integer
  def to_l
    'one'
  end
end
 puts 1.to_l
require 'minitest/autorun'
require './no017/problem'
 class ProblemTest < Minitest::Test
  def test_problem
    assert_equal 'one', 1.to_l
    assert_equal 3, 1.to_l.size
  end
end

Integerクラスに to_l メソッドを拡張させました。

とりあえず中身は one と仮置きしてテストを通しています。
テスティングフレームワームはRuby標準搭載の Minitest を利用しています。

20まではcase文で定義

21以降の1の位は1~9で繰り返しですし、101以降の10の位は99までの繰り返しなので

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

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

20まではcase文でそれぞれ定義しているだけです。

number_letter_countsメソッドに始まりと終わりの2数を引数に持たせ、
mapメソッドとinjectメソッドで英名化・文字数カウント・足し上げました。

問題文の例である1〜5までの計算結果も一致することをテストで確認しています。

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
assert_equal 19, number_letter_counts(from: 1, to: 5)

21~99の処理

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

21~99については divmod を用いて 10 で割れば、商が10の位の数字・余りが1の位の数字になるので、それぞれを自作した to_l メソッドで英名に変換してハイフンでつないでいます。

when (21..99)
  quotient, remainder = divmod(10)
  tens_place = quotient * 10
  tens_place.to_l + '-' + remainder.to_l

文字数としてカウントするときにハイフンは数えないように予め削除しています。

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

テストも通ることを確認しました。

assert_equal 9, number_letter_counts(from: 21, to: 21)

100~999の処理

基本的には繰り返しです。今度は divmod を用いて 100 で割っています。

567 であれば five hundred and sixty-seven となるようにしています。

ただし、200 など ●00 という数字については two hundred という形式で終わるので、余りが0にななるかどうかで場合分けをしています。

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メソッドに変更しています。

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

あとは 1000 を個別に定義して終わりでした。
最終的なコードとプルリクエストのリンクを張っておきます。

最終的なコード

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)
require 'minitest/autorun'
require './no017/problem'

require 'minitest/reporters'
Minitest::Reporters.use!

class ProblemTest < Minitest::Test
  def test_problem
    assert_equal 'one', 1.to_l
    assert_equal 3, 1.to_l.size
    assert_equal 'three', 3.to_l
    assert_equal 5, 3.to_l.size
    assert_equal 19, number_letter_counts(from: 1, to: 5)
    assert_equal 112, number_letter_counts(from: 1, to: 20)
    assert_equal Integer, number_letter_counts(from: 1, to: 99).class
    assert_equal 'twenty-one', 21.to_l
    assert_equal 'ninety-nine', 99.to_l
    assert_equal 9, number_letter_counts(from: 21, to: 21)
    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)
  end
end

※問17ブランチのプルリクエスト

このくらいになってくると1問1問が軽くはなくなりますが、やりごたえが出てきますね!
今回は以上です。

この記事の内容が役に立ったと思いました、SNSで記事を共有していただけますと幸いです。