はじめに
普段は自分のブログサイトで色々投稿しているのですが、
思い切って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
が返るように仮置きしてテストを通しました。
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
class Integer
def to_l
'one'
end
end
20までの数字英名変換をcase文で定義
21以降の数字の1の位は1~9で繰り返しですし、
101以降の数字の下2桁は99までの繰り返しなので
- 20までの英名を定義
- 99まで拡張
- 999まで拡張
の3段階で考えながら開発しました。
assert_equal 19, number_letter_counts(from: 1, to: 5)
number_letter_counts
メソッドに対して、
始まりと終わりの2数を引数に持たせることで
求める文字数が返ることを期待します。
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の処理
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
メソッドで英名に変換してハイフンでつなぎました。
when (21..99)
quotient, remainder = divmod(10)
tens_place = quotient * 10
tens_place.to_l + '-' + remainder.to_l
ハイフンを削除出来ておらずテスト結果は 10 となり落ちてしまうので
文字数カウントする前にハイフンがあれば削除するようにメソッドを変更しました。
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 という形式で終わります。
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になるかどうかで返り値は場合分けをしました。
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)
#=> 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