はじめに
Delphi Advent Calenar 2019 の 12/2 の DEKO さんの記事「整数をローマ数字 (文字列) に変換するコードを書いてみる」に
私はこの問題を Delphi 10.3 Rio で解くことにしました。この辺で一旦読むのをやめて、各自お好きな言語でこの課題にチャレンジしてみるのもいいかと思います。
とあったので、僕も作ってみました。
同じ Delphi 10.3.3 で作ったのでちょっと趣旨が違いますが解き方が違うので大目に見て…
考え方
剰余
4 と 9 がネックになる事は判っていました。
この2つだけ次の桁の数字から1つ引くという特殊な書き方になるからです。
ただ、どちらの数も 5 との剰余は 4 になります。
N mod 5 = 4 // 剰余が 4 の場合は N は 4 or 9
文字列化
ローマ数字はその書き方の制約上、3999 より大きい数を表せません。
つまり、最大の大きさでもわずか4桁の数に過ぎません。
ですので、これを数として計算せずに文字列化してもたった4文字。処理上大きな負担にはならないはずです。
今回は上の剰余を使いたいと考えたので、文字列化しました。
まとめると
- 剰余が 4 の場合は次の数から今の数を引く書き方にする
- 数を4文字の文字列化して先頭から処理する
として作りました。
アラビア数字→ローマ数字
では実際のソースです。
function ToRoman(const iArabic: Integer): String;
const // S5 は 5 * 10^n を表すシンボル, S1 は 1 * 10^n を表すシンボル
ROMAN_SYMBOLS: array [-1.. 3] of record S5, S1: Char end = (
(S5: ' '; S1: ' '), // 範囲チェックを横着するためのダミー
(S5: ' '; S1: 'M'),
(S5: 'D'; S1: 'C'),
(S5: 'L'; S1: 'X'),
(S5: 'V'; S1: 'I')
);
begin
if (iArabic > 3999) or (iArabic < 1) then
Exit('');
var V := Int32(iArabic).ToString;
V := (StringOfChar('0', 4) + V).SubString(V.Length, 4); // 必ず4文字の文字列にする
var SB := TStringBuilder.Create;
try
for var i := 0 to 3 do // 4文字しかないので 0~3 で OK
begin
var C := ROMAN_SYMBOLS[i];
var P := ROMAN_SYMBOLS[i - 1]; // 現在の桁数より大きい桁のシンボル
var N := StrToIntDef(V.Chars[i], 0);
var F := N > 4;
var R := N mod 5;
if (R = 4) then // 剰余が 4 か?
begin
SB.Append(C.S1);
SB.Append(IfThen(F, P.S1, C.S5))
end
else
begin
if F then
SB.Append(C.S5);
SB.Append(C.S1, R);
end;
end;
Result := SB.ToString;
finally
SB.DisposeOf;
end;
end;
0:
12: XII
24: XXIV
42: XLII
49: XLIX
89: LXXXIX
299: CCXCIX
493: CDXCIII
1960: MCMLX
3999: MMMCMXCIX
ローマ数字→アラビア数字(おまけ)
ローマ数字化とこのアラビア数字化で、同じシンボルを別々の構造体に持っているのがカッコ悪いですね…
function ToAlabic(const iRomanNum: String): Integer;
const
ROMANS: array [-1.. 6] of record Number: Integer; Symbol: Char; end = (
(Number: 0; Symbol: ' '),
(Number: 1000; Symbol: 'M'),
(Number: 500; Symbol: 'D'),
(Number: 100; Symbol: 'C'),
(Number: 50; Symbol: 'L'),
(Number: 10; Symbol: 'X'),
(Number: 5; Symbol: 'V'),
(Number: 1; Symbol: 'I')
);
begin
Result := 0;
var i := 0;
var j := 0;
var Len := iRomanNum.Length;
while i < Len do
begin
var C := iRomanNum.Chars[i];
var N := iRomanNum.Chars[i + 1];
var CR := ROMANS[j];
var NR := ROMANS[j - 1];
if N = NR.Symbol then
begin
var D := 1;
while D < NR.Number do
D := D * 10;
Inc(Result, NR.Number - D div 10);
Inc(i, 2);
end
else
begin
if C = CR.Symbol then
begin
Inc(Result, CR.Number);
Inc(i);
end
else
Inc(j);
end
end;
end;
: 0
XII: 12
XXIV: 24
XLII: 42
XLIX: 49
LXXXIX: 89
CCXCIX: 299
CDXCIII: 493
MCMLX: 1960
MMMCMXCIX: 3999
上記の出力結果を出すルーチン
標準手続き Write で使える桁数指定を使っているので、珍しいかもしれないなと思ってこちらも掲載。
program RomanNum;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
uRomanNumHelper in 'uRomanNumHelper.pas';
const
NUMS: array[0.. 9] of Integer = (
0, 12, 24, 42, 49, 89, 299, 493, 1960, 3999
);
begin
// アラビア数字→ローマ数字
Writeln('Alabic to Roman');
for var N in NUMS do
Writeln(N:4, ': ', ToRoman(N));
Writeln;
// ローマ数字→アラビア数字
Writeln('Roman to Alabic');
for var N in NUMS do
begin
var R := ToRoman(N);
Writeln(R:9, ': ', ToAlabic(R):4);
end;
Readln;
end.
最後に
DEKO さんの解き方の方がエレガント!!
そして、この課題、大学の情報の実習で解かされた気がする…