summer_tree_home

Check iOでPython3をマスターするぜっ

Roman numerals (Check iO > Home) 数字をローマ数字に変換する

いよいよ、あと2問で Home Station(最初の島)が終わる。いつの間にか、周辺の島がアンロックされていて、Homeの次はどこに行こうか楽しみだ。

どんな問題?

Roman numerals
http://www.checkio.org/mission/roman-numerals/

数字をローマ数字(I,II,III,IV...)に変換するという問題。

ローマ数字って腕時計に付いてる1から12ぐらいまでしか知らないんだよなー。
問題文でも詳しい説明はなく、詳細は Wikipedia を見ろって書いてあるだけ。幸い、日本語版があったので、そっちを読んで簡単にまとめてみた。

3分でわかるローマ数字

使う文字は7種類

1 5 10 50 100 500 1000
I V X L C D M

文字を並べるルール

  • できるだけ使う文字数が少なくなるようにする。IIIII→V
  • 左から桁の大きい順に並べる。100の桁→10の桁→1の桁
  • 同じ文字を4つ並べてはいけない。IIII→IV や VIIII→IX など。これを減算則という。
  • 減算則で認められるのは、IV(4), IX(9), XL(40), XC(90), CD(400), CM(900) の6通りだけ。
  • V(5),L(50),D(500)は1回しか使えない。
  • 0は無い。
  • 1から3999までしか表現できない。

おまけ情報

  • Excelには、ローマ数字を作成する ROMAN 関数がある。
  • 時計の文字盤では、4をIIIIと書くのは間違いじゃない。

すっかり読みふけってしまった。Wikipediaって恐ろしい。

どうやって解く?

思っていたより簡単そうだ。いろいろルールはあるものの、結局は、数字を各桁ごとにローマ数字に変換して、結合すればいいだけだ。
Wikipediaに、各桁の一覧が載っていたので、これを参考にして作ってみる。

1の桁

1 2 3 4 5 6 7 8 9
I II III IV V VI VII VIII IX

10の桁

10 20 30 40 50 60 70 80 90
X XX XXX XL L LX LXX LXXX XC

100の桁

100 200 300 400 500 600 700 800 900
C CC CCC CD D DC DCC DCCC CM

1000の桁

1000 2000 3000
M MM MMM


コードを書いてみた。

def checkio(number):
    def make_roman_nums(i, v, x):
        return ['', i, i * 2, i * 3, i + v, v, v + i, v + i * 2, v + i * 3, i + x]

    # 変換表を作成
    roman_nums = [make_roman_nums('I', 'V', 'X'),  # ones
                  make_roman_nums('X', 'L', 'C'),  # tens
                  make_roman_nums('C', 'D', 'M'),  # hundreds
                  make_roman_nums('M', '', '')[:4]]  # thousands

    # 数字を各桁に分解する
    digits = [int(c) for c in reversed(str(number))]  # 123 -> [3, 2, 1]

    # ローマ数字に直して結合
    return ''.join(reversed([roman_nums[i][digits[i]] for i in range(len(digits))]))

roman_nums が変換表だ。内容は下記の通り。直接、下のように書いてもよかったが、さすがに芸がないので上のようにしておいた。

roman_nums = [['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
              ['', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
              ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM'],
              ['', 'M', 'MM', 'MMM']]

あとは、この変換表を使って各桁の数字をローマ数字に変換する。
roman_nums[0] が 1の桁
roman_nums[1] が 10の桁
roman_nums[2] が 100の桁
roman_nums[3] が 1000の桁
となっているので、
2014なら、

roman_nums[3][2] + roman_nums[2][0] + roman_nums[1][1] + roman_nums[0][4]

となる。

最後の2行では、数字を分解して、ローマ数字に変換して、結合する。

    digits = [int(c) for c in reversed(str(number))]  # 123 -> [3, 2, 1]
    return ''.join(reversed([roman_nums[i][digits[i]] for i in range(len(digits))]))

数字を各桁に分解するところは、数字を文字列にして反転させ、またintに変換している。
digits[0] が 1の桁
digits[1] が 10の桁
digits[2] が 100の桁
digits[3] が 1000の桁
となる。
次の行で、1の桁から順番に変換表にあてはめ、反転してから、結合する。

ただ、読み直してみると、やっぱりこの2行がわかりにくいかなと思う。reversedが2回出てくるのも、あまり綺麗じゃない。

digitsを (桁,数字) のリストにしたらどうだろう?

    # 852 -> [(3, 0), (2, 8), (1, 5), (0, 2)]
    digits = [(place, number // (10 ** place) % 10) for place in range(3, -1, -1)]
    return ''.join(roman_nums[place][value] for place, value in digits)

この方がいいかも。

あとは、checkio()が呼ばれるたびに、変換表を作り直すのも無駄だなぁ、ということで、roman_nums は関数の外に出した。

def _make_roman_nums(i, v, x):
    return ['', i, i * 2, i * 3, i + v, v, v + i, v + i * 2, v + i * 3, i + x]

roman_nums = [_make_roman_nums('I', 'V', 'X'),  # ones
              _make_roman_nums('X', 'L', 'C'),  # tens
              _make_roman_nums('C', 'D', 'M'),  # hundreds
              _make_roman_nums('M', '', '')[:4]]  # thousands

def checkio(number):
    # 852 -> (3, 0), (2, 8), (1, 5), (0, 2)
    digits = [(place, number // (10 ** place) % 10) for place in range(3, -1, -1)]
    return ''.join(roman_nums[place][value] for place, value in digits)

よし、これで公開!