2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-11-16

はじめに

普段は自分のブログサイトで色々投稿しているのですが、
思い切って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が返るように仮置きしてテストを通しました。

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
problem.rb
class Integer
  def to_l
    'one'
  end
end

20までの数字英名変換をcase文で定義

21以降の数字の1の位は1~9で繰り返しですし、
101以降の数字の下2桁は99までの繰り返しなので

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

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

test_problem.rb
assert_equal 19, number_letter_counts(from: 1, to: 5)

number_letter_countsメソッドに対して、
始まりと終わりの2数を引数に持たせることで
求める文字数が返ることを期待します。

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の処理

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メソッドで英名に変換してハイフンでつなぎました。

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

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

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 という形式で終わります。

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になるかどうかで返り値は場合分けをしました。

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

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

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

最終的なコード

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

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?