数値を英語表記に変換するライブラリ

始まりは、Project Euler - Problem 17でした。

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" を使用するのは英国の慣習。
Problem 17 - PukiWiki

なんじゃこりゃ。変換云々の前に、数値を英語で書いたことなんてほとんどないんですけど・・。


というわけで、英語の勉強からスタート。


英語の数字の読み方・書き方 基数・序数編 [ビジネス英会話] All About

“40はforty”。ついuを付けてしまいそうになります。


英語で10000は何と読む?桁の大きい数字の読み方 [ビジネス英会話] All About

Trillionという単語の存在を初めて知りました・・。


.
.


で、いよいよコーディングです。せっかくなのでライブラリとしてNumericクラスを拡張することにしました。


まずはテストから。テスティングフレームワークRSpec(gem install rspec)です。

require "numeric"

describe Numeric, "#to_en" do
  it "1 to one" do
    1.to_en.should == "one"
  end

  it "13 to thirteen" do
    13.to_en.should == "thirteen"
  end

  it "24 to twenty-four" do
    24.to_en.should == "twenty-four"
  end

  it "100 to one hundred" do
    100.to_en.should == "one hundred"
  end
end

とりあえずここまで。実際に書くときは1つずつで問題ありません。

spec -cfs spec/numeric_spec.rb

を実行すると、真っ赤っかになると思います。(真っ白けかも)


ちなみに、ディレクトリ構成を知りたい方はgithubを見てください。(始めたばかりなので、あまり参考にはしない方がいいと思います)

GitHub - myokoym/mylib: My Ruby Library.


で、プロダクトコードはこのように。

class Numeric
  def to_en
    return self.to_s unless self.is_a?(Integer)
    case self
    when 1
      "one"
    when 2
      "two"

    #(中略)

    when 19
      "nineteen"
    when 20
      "twenty"
    when 30
      "thirty"
    when 40
      "forty"

    #(中略)

    when 90
      "ninety"
    when 21 .. 99
      x_one = self % 10
      x_ten = self - x_one
      x_ten.to_en + "-" + x_one.to_en
    when 100 .. 999
      "one hundred"  #仮実装
    end
  end
end


まず、冒頭の

return self.to_s unless self.is_a?(Integer)

で、今回は整数のみの対応とするため、Integer型を継承していない場合(浮動小数点数など)は単にto_sして返します。Rubyのメソッドは戻り値の型を合わせる必要はありませんが、念のため文字列型で統一します。


ある程度直書きする必要はあるのですが、Case文に何百個も何千個も書いていたら日が暮れてしまうので、21など複数の単語で表す数値は少し工夫しました。

    when 21 .. 99
      x_one = self % 10
      x_ten = self - x_one
      x_ten.to_en + "-" + x_one.to_en

一の位と十の位に分けて、再帰っぽく自分自身を呼び出しています。


では、とりあえず仮実装しておいた100〜999の場合も、同じように修正します。

    when 100 .. 999
      front_num = self / 100
      rear_num = self % 100
      if rear_num == 0
        front_num.to_en + " hundred"
      else
        front_num.to_en + " hundred and " + rear_num.to_en
      end

100で割りきれる場合はandは付かないので、場合分けしています。もう少しきれいに書けるやもしれません。


あとは、まったく同じロジックで1000以上も攻略できます。

完成したコードがこちら。

class Numeric
  def to_en
    return self.to_s unless self.is_a?(Integer)
    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
      x_one = self % 10
      x_ten = self - x_one
      x_ten.to_en + "-" + x_one.to_en
    when 100 .. 999
      front_num = self / 100
      rear_num = self % 100
      if rear_num == 0
        front_num.to_en + " hundred"
      else
        front_num.to_en + " hundred and " + rear_num.to_en
      end
    when 1e3 .. 999999
      front_num = self / 1000
      rear_num = self % 1000
      if rear_num == 0
        front_num.to_en + " thousand"
      else
        front_num.to_en + " thousand and " + rear_num.to_en
      end
    when 1e6 .. 999999999
      front_num = self / 1000000
      rear_num = self % 1000000
      if rear_num == 0
        front_num.to_en + " million"
      else
        front_num.to_en + " million and " + rear_num.to_en
      end
    when 1e9 .. 999999999999
      front_num = self / 1000000000
      rear_num = self % 1000000000
      if rear_num == 0
        front_num.to_en + " billion"
      else
        front_num.to_en + " billion and " + rear_num.to_en
      end
    when 1e12 .. 999999999999999
      front_num = self / 1000000000000
      rear_num = self % 1000000000000
      if rear_num == 0
        front_num.to_en + " trillion"
      else
        front_num.to_en + " trillion and " + rear_num.to_en
      end
    else
      self.to_s
    end
  end
end


これを使って、Problem17も無事に解くことができました。まさか、数学だけでなく英語方面からも攻めてくるとは・・。先が思いやられます。