Preface (はじめに)
アラビア数字を受け取ってローマ数字を返すmethodかclassを作れという課題が出たので,
Rubyを用いて解いてみます.
Roman numerals
ローマ数字なんていまどきオシャレな時計くらいしか目にしません.
12以上の数字はどう表すのでしょう.
ローマ数字 – Wikipediaを参考にアラビア数字とローマ数字の対応表を以下に示します.
| アラビア数字 | ローマ数字 | 
|---|---|
| 1 | I | 
| 5 | V | 
| 10 | X | 
| 50 | L | 
| 100 | C | 
| 500 | D | 
| 1000 | M | 
それぞれの位の数字は上記の数字の和に分解して大きい順に並べて書きます.
例えば, 3のときはIII, 2020だとMMXXとなります.
ただ, ローマ数字は面倒な規則があり,
一般的に4や99のような数字を表す時に, IIIIやLXXXXVIIIIのように表しません.
このような数字は右から左を減算することを意味するように小さい数を大きい数の左に書く減算則という規則に従います. 具体的には, 4はIVで99はXCIXとなります.
また, これらの規則のためにローマ数字は1から3999までの値しか表現できません.
(古代ローマ人はこの使い方で困らなかったのでしょうか?)
Ruby
これまでの情報からRubyでアラビア数字からローマ数字への変換を実装してみます.
なんの捻りもなしに実装するなら, 条件分岐を大量に使えば一応はできるでしょう.
ただ, そんなプログラムは見づらいでしょうし, 時間が経てばよく分かんないけど動く腫れ物のようなプログラムになるでしょう.
なので, 少し工夫を凝らしたRubyのプログラムを作ろうと思います.
法則
見やすいプログラムを作るには法則を見つける事です.
法則を用いてメソッドを作れば, それを用いて見やすいプログラムを作ることができます.
では, ローマ数字の法則とはどのようなものでしょうか.
大まかな法則
上記の対応表を見ると, ローマ数字の文字が変わるのは各数字の5倍, そして次は2倍の数字の時でそれを交互に繰り返しています. (ただ, 3回が上限ですが)
このことから各桁の数字に対して, 毎回5と2の商と剰余を求めることでどのローマ数字に変換すればいいかが分かります.
以下はアラビア数字が87の時の動作を表しています. (分かりにくい説明と表で申し訳ないです…)
| 除数 | 商 | 剰余 | 変換するローマ数字 | 
|---|---|---|---|
| 5 | 17 | 2 | I | 
| 2 | 8 | 1 | V | 
| 5 | 1 | 3 | X | 
| 2 | 0 | 1 | L | 
| 5 | 0 | 0 | C | 
| 2 | 0 | 0 | D | 
| 0 | M | 
表より, アラビア数字の87は, L1つ, X3つ, V1つ, I2つ つまり, **LXXXVII**と表せばいいことになります.これは正しい表記です.
これをもとに実装してみます
class Integer
  def to_roman
    @@arabic = self
    @@roman = ''
    @@a2r = []
    @@symbol = ['I', 'V', 'X', 'L', 'C', 'D', 'M']
    for i in 1..3 do
      devide(5)
      devide(2)
    end
    for i in 1..6 do
      @@roman = @@symbol[i]*@@a2r[i] + @@roman
    end
    @@roman = @@symbol[-1]*@@arabic + @@roman
  end
  def devide n
    @@a2r << @@arabic % n
    @@arabic /= n
  end
end
細かな法則
ただ上記の法則をそのまま取り入れると, 4のときIIIIを返してしまいます.
なので, 減算則に対応した法則も見つけないといけません.
まずは, 具体的な数字で考えます.
一桁の数字だと考えやすいので, 4と9のローマ数字変換について考えます.
変換対象のアラビア数字が4だとIV, 9だとIXとなります.
両者の共通点として最初のローマ数字が**Iであることに気づきましたか?
また, 両者の違いは後ろのローマ数字, 4では5を表すV, 9では10を表すX**が違います.
法則を見つけやすくするために数字で扱ってみましょう.
数字での共通点は, 4と9はどちらも5の剰余が4となり, 違いは5の商が4だと0, 9だと1であるという点です.
これは先ほどの大まかな法則に組み込むことができそうです.
より一般化すると, 各桁に対する5の剰余が4の場合,
- 次の2の剰余が0だと, 右のローマ数字は左のローマ数字の1つ先のローマ数字 (e.g. I→V)
- 次の2の剰余が1だと, 右のローマ数字は左のローマ数字の1つ先のローマ数字 (e.g. I→X)
となります.
では, この細かな法則も組み込んで実装してみます.
Code
class Integer
  def to_roman
    @@arabic = self
    @@roman = ''
    @@a2r = []
    @@symbol = ['I', 'V', 'X', 'L', 'C', 'D', 'M']
    for i in 0..2 do
      devide(5)
      devide(2)
      k = 2*i
      if @@a2r[k] == 4
        tmp = @@a2r[k+1]
        @@roman = @@symbol[k] + @@symbol[k+1+tmp] + @@roman
      else
        @@roman = @@symbol[k+1]*@@a2r[k+1] + @@symbol[k]*@@a2r[k] + @@roman
      end
    end
    @@roman = @@symbol[-1]*@@arabic + @@roman
  end
  def devide n
    @@a2r << @@arabic % n
    @@arabic /= n
  end
end
予想より長くなってしまいましたが, なんとか実装できました.
Test
手動で確かめるのも面倒なので, この記事で作成したassert_equal関数を流用してテストをしたいと思います.
以下はそのテストコードです
require './roman_number'
require './assert_equal'
[
  ['I', 1],
  ['II', 2],
  ['III', 3],
  ['IV', 4],
  ['V', 5],
  ['VI', 6],
  ['IX', 9],
  ['X', 10],
  ['XI', 11],
  ['XIV', 14],
  ['XV', 15],
  ['XIX', 19],
  ['XXXVIII', 38],
  ['XLII', 42],
  ['XLIX', 49],
  ['LI', 51],
  ['XCVII', 97],
  ['XCIX', 99],
  ['CDXXXIX', 439],
  ['CDLXXXIII', 483],
  ['CDXCIX', 499],
  ['DCCXXXII', 732],
  ['CMLXI', 961],
  ['CMXCIX', 999],
  ['MCMXCIX', 1999]
].each do |expected, index|
  assert_equal(expected, index.to_roman)
end
では, テストを行ってみましょう.
> ruby test_rn.rb
さあ, 出力は…
expected :: I
result   :: I
succeeded in assert_equal.
expected :: II
result   :: II
succeeded in assert_equal.
expected :: III
result   :: III
succeeded in assert_equal.
expected :: IV
result   :: IV
succeeded in assert_equal.
expected :: V
result   :: V
succeeded in assert_equal.
expected :: VI
result   :: VI
succeeded in assert_equal.
expected :: IX
result   :: IX
succeeded in assert_equal.
expected :: X
result   :: X
succeeded in assert_equal.
expected :: XI
result   :: XI
succeeded in assert_equal.
expected :: XIV
result   :: XIV
succeeded in assert_equal.
expected :: XV
result   :: XV
succeeded in assert_equal.
expected :: XIX
result   :: XIX
succeeded in assert_equal.
expected :: XXXVIII
result   :: XXXVIII
succeeded in assert_equal.
expected :: XLII
result   :: XLII
succeeded in assert_equal.
expected :: XLIX
result   :: XLIX
succeeded in assert_equal.
expected :: LI
result   :: LI
succeeded in assert_equal.
expected :: XCVII
result   :: XCVII
succeeded in assert_equal.
expected :: XCIX
result   :: XCIX
succeeded in assert_equal.
expected :: CDXXXIX
result   :: CDXXXIX
succeeded in assert_equal.
expected :: CDLXXXIII
result   :: CDLXXXIII
succeeded in assert_equal.
expected :: CDXCIX
result   :: CDXCIX
succeeded in assert_equal.
expected :: DCCXXXII
result   :: DCCXXXII
succeeded in assert_equal.
expected :: CMLXI
result   :: CMLXI
succeeded in assert_equal.
expected :: CMXCIX
result   :: CMXCIX
succeeded in assert_equal.
expected :: MCMXCIX
result   :: MCMXCIX
succeeded in assert_equal.
すべて成功!
期待通りの結果を返してくれましたね.