アラビア数字・ローマ数字変換

お題:アラビア数字・ローマ数字変換 - No Programming, No LifeErlangでやってみました。やってみたのはけっこう前ですが放置していました。

まずarabic_to_roman。ローマ数字には位取りの0がないのでちょっと表現が難しいですが、1、10、100の位について、それぞれ1、5、10の重みをもつ文字が対応していると考えてみました。1の位であれば、I、V、Xだし、10の位であれば、X、L、Cですね。

-module(roman).
-export([arabic_to_roman/1, roman_to_arabic/1]).
-export([test/0]).

roman_digit(N, {D1, D5, D10}) when N >= 1 andalso N =< 9 ->
    case N of
	1 -> [D1];
	2 -> [D1, D1];
	3 -> [D1, D1, D1];
	4 -> [D1, D5];
	5 -> [D5];
	6 -> [D5, D1];
	7 -> [D5, D1, D1];
	8 -> [D5, D1, D1, D1];
	9 -> [D1, D10]
    end;
roman_digit(0, _D) ->
    [].

これを使って変換してみましょう...

arabic_to_roman(N) when N >= 1 andalso N =< 3999 ->
    D1000 = N div 1000,
    D100 = (N rem 1000) div 100,
    D10 = (N rem 100) div 10,
    D1 = (N rem 10),
    DL = [{"M", void, void}, {"C", "D", "M"}, {"X", "L", "C"}, {"I", "V", "X"}],
    lists:flatten(lists:map(fun(({X, D})) -> roman_digit(X, D) end,
	      lists:zip([D1000, D100, D10, D1], DL))).

1000の位の扱いと、あと0の扱いがやはり若干苦しいですね。

10> roman:arabic_to_roman(1234).
"MCCXXXIV"
11> 

ふむふむ。では次はroman_to_arabicです。こちらは、各桁の1の重みを持つ文字が、場所によって+1なのか-1なのかが変わるというのが厄介です。状態を持たせて、とか、先読をして...といったやりかたも考えられますが、文字列のパターンマッチングを使った方が話が早そうです。こんな感じになるでしょうか...

roman_to_arabic(S) ->
    roman_to_arabic(string:to_upper(S), 0).
roman_to_arabic("", Acc) ->
    Acc;
roman_to_arabic("IV" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 4);
roman_to_arabic("IX" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 9);
roman_to_arabic("XL" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 40);
roman_to_arabic("XC" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 90);
roman_to_arabic("CD" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 400);
roman_to_arabic("CM" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 900);
roman_to_arabic("I" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 1);
roman_to_arabic("V" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 5);
roman_to_arabic("X" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 10);
roman_to_arabic("L" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 50);
roman_to_arabic("C" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 100);
roman_to_arabic("D" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 500);
roman_to_arabic("M" ++ Rem, Acc) ->
    roman_to_arabic(Rem, Acc + 1000).

ローマ数字は3999までしかないらしいのでこれでもいいような気がしないでもないですが、もうちょっと考えてみたいので没にしました。作りなおしたのがこちらです:

roman_to_arabic(S) ->
    roman_to_arabic(string:to_upper(S), 0).
roman_to_arabic("", Res) ->
    Res;
roman_to_arabic(S, Res) ->
    D = [{"M", 1000}, {"CM", 900}, {"D", 500},
	 {"CD", 400}, {"C", 100}, {"XC", 90}, 
	 {"L", 50}, {"XL", 40}, {"X", 10}, 
	 {"IX", 9}, {"V", 5}, {"IV", 4},
	 {"I", 1}],
    case lists:foldl(fun({DD, DN} ,Acc)->
			case Acc of
			    [] ->
				case string:str(S, DD) of
				    1 ->
					{DN, string:len(DD)};
				    _Else ->
					Acc
				end;
			    Acc-> Acc
			end end, [], D) of
	{X, Len} ->
	    roman_to_arabic(string:substr(S, Len + 1), Res + X);
	[] ->
	    error
    end.			

やってることは一緒のつもりですがややこしいですね。最初に見つかったところで止まるループを作りたかったのですが、やりかたが思いつかず上記のような苦しい表現になってしまいまいた。いずれにしても没バージョンに比べて著しく優れているとも思えませんね...

19> roman:roman_to_arabic("xvi").
16
20>

XVIは16MHzなので正しいようですね(ひどい覚えかただ)。最後にテストを:

test() ->
    assert(arabic_to_roman(11), "XI"),
    assert(roman_to_arabic("XI"), 11),
    assert(arabic_to_roman(12), "XII"),
    assert(roman_to_arabic("XII"), 12),
    assert(arabic_to_roman(14), "XIV"),
    assert(roman_to_arabic("XIV"), 14),
    assert(arabic_to_roman(18), "XVIII"),
    assert(roman_to_arabic("XVIII"), 18),
    assert(arabic_to_roman(24), "XXIV"),
    assert(roman_to_arabic("XXIV"), 24),
    assert(arabic_to_roman(43), "XLIII"),
    assert(roman_to_arabic("XLIII"), 43),
    assert(arabic_to_roman(99), "XCIX"),
    assert(roman_to_arabic("XCIX"), 99),
    assert(arabic_to_roman(495), "CDXCV"),
    assert(roman_to_arabic("CDXCV"), 495),
    assert(arabic_to_roman(1888), "MDCCCLXXXVIII"),
    assert(roman_to_arabic("MDCCCLXXXVIII"), 1888),
    assert(arabic_to_roman(1945), "MCMXLV"),
    assert(roman_to_arabic("MCMXLV"), 1945),
    assert(arabic_to_roman(3999), "MMMCMXCIX"),
    assert(roman_to_arabic("MMMCMXCIX"), 3999).	   
assert(X,X) -> ok.

実行してみると...

20> roman:test().
ok
21> 

OKのようです。