Reactで、数字をローマ数字に変換する方法
数字をローマ数字にするには、「M = 1000」「CM = 900」…「I = 1」の組を大きい順に並べた表を用意し、大きい記号から順に引いていきます。逆にローマ数字を数にするときは、先に正規表現で正しい並びかどうかを確かめてから、同じ表で足し合わせます。
ここでは、変換の関数を roman.ts にまとめ、数字を打てばローマ数字が、ローマ数字を打てば数字が出る RomanNumeralConverter というコンポーネントを作りました。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。関数の結果は Node.js で確かめ、tsc で型エラーがないことを確かめました。コンポーネントは StrictMode で包んだ開発用のビルドを、幅 375px の Chromium で Playwright から操作しました。日本語入力(IME)で打ったときの動き、Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げは試していません。

数字をローマ数字にする関数
ここでは、CSS の仕様にある upper-roman の定義と同じく、4 を「IV」、9 を「IX」と書く形にそろえます。同じように、引き算で書く6つの組(IV、IX、XL、XC、CD、CM)も、ふつうの記号と同じ表に入れておきます。そうすると、表の上から順に「引けるだけ引く」をくり返すだけで変換できます。
// 数字とローマ数字を相互に変換する
// 大きい順に並べた、記号と値の組。4 や 9 を表す「IV」「IX」なども入れておく
const TABLE: [string, number][] = [
['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],
];
export const MIN = 1;
export const MAX = 3999;
// 1〜3999 の整数をローマ数字にする。範囲の外なら null を返す
export function toRoman(n: number): string | null {
if (!Number.isInteger(n) || n < MIN || n > MAX) return null;
let rest = n;
let result = '';
for (const [symbol, value] of TABLE) {
while (rest >= value) {
result += symbol;
rest -= value;
}
}
return result;
}
// 正しい並びのローマ数字だけに一致する正規表現(千・百・十・一の位の順)
const PATTERN = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/;
// ローマ数字を数にする。正しくない並びなら null を返す
export function fromRoman(input: string): number | null {
// 全角の「XII」や1文字の「Ⅻ」を「XII」にし、小文字は大文字にそろえる
const s = input.normalize('NFKC').trim().toUpperCase();
if (s === '' || !PATTERN.test(s)) return null;
let result = 0;
let i = 0;
for (const [symbol, value] of TABLE) {
while (s.startsWith(symbol, i)) {
result += value;
i += symbol.length;
}
}
return result;
}
たとえば 1999 なら、M(1000)を引いて残り 999、CM(900)を引いて 99、XC(90)を引いて 9、IX(9)を引いて 0 になり、「MCMXCIX」ができます。検証環境の Node.js で toRoman() を実行した結果の一部は、次のとおりです。
| 渡した数 | 結果 |
|---|---|
4 |
IV |
944 |
CMXLIV |
2026 |
MMXXVI |
3888 |
MMMDCCCLXXXVIII |
3999 |
MMMCMXCIX |
0、4000、-1、1.5、NaN |
null |
この関数では M を3つまでにしているので、書けるのは 3999(MMMCMXCIX)までです。CSS の仕様でも、upper-roman の定義は CSS Counter Styles Level 3 の定義済みのカウンタースタイルで range: 1 3999 になっています。範囲の外(0 や負の数を含む)と整数でない数には null を返し、呼び出し側でメッセージを出します。
1〜3999 のすべての数を toRoman() でローマ数字にし、それを fromRoman() で数に戻したところ、3999個すべてが元の数に戻りました。できた文字列に重複はなく、いちばん長いのは 3888 の「MMMDCCCLXXXVIII」(15文字)でした。
ローマ数字を数にするときは、先に並びを確かめる
文字を左から読んで足すだけでは、「IIII」のような正しくない並びも、そのまま足されて数になります。そこで fromRoman() では、足す前に正規表現 PATTERN で並びを確かめています。千の位は M を0〜3個、百の位は「CM」「CD」か「D のあとに C を0〜3個」、十の位と一の位も同じ形です。この形に合わない文字列は null になります。
| 渡した文字列 | 結果 | 補足 |
|---|---|---|
MMXXVI |
2026 |
正しい並び |
mmxxvi |
2026 |
大文字にそろえてから読む |
IIII |
null |
I・X・C は3つまで |
VV |
null |
V・L・D は1つまで |
IC |
null |
I を前に置けるのは V と X だけ |
XM |
null |
X を前に置けるのは L と C だけ |
VX |
null |
V を前に置く組がない |
MMMM |
null |
M は3つまで |
1文字の「Ⅻ」や全角の文字も読む
Unicode には、ローマ数字を1文字で表す「Ⅰ」(U+2160)から「Ⅻ」(U+216B)などの文字があります。検証環境の Python の unicodedata で Ⅰ、Ⅳ、Ⅻ、Ⅿ、ⅰ を調べると、どれにも互換分解(<compat>)が定められていて、「Ⅻ」は X・I・I の3文字に当たります。
そのため、MDN の normalize() の説明にある normalize('NFKC') をかけると、ふつうの英字になります。検証環境では、Ⅻ は XII に、小文字の ⅰ(U+2170)は i になりました。fromRoman() は最初に NFKC をかけているので、fromRoman('Ⅻ') は 12、全角の XIV は 14、小文字の1文字のローマ数字で書いた ⅿⅿⅹⅹⅵ は 2026 になりました。
ただし、「ↀ」(U+2180、ROMAN NUMERAL ONE THOUSAND C D)のように分解が定められていない文字は、NFKC をかけてもそのまま残ります。fromRoman() に渡すと、正規表現に合わずに null になります。
打つたびに変換する入力欄
RomanNumeralConverter は、欄に数字が入っていればローマ数字に、それ以外ならローマ数字として読んで数字にします。変換の結果は state に持たず、描画のたびに欄の文字から計算しています。
import { useId, useState } from 'react';
import { MAX, MIN, fromRoman, toRoman } from './roman';
export function RomanNumeralConverter() {
const id = useId();
const [text, setText] = useState('2026');
const trimmed = text.trim();
let result = '';
let error = '';
if (trimmed === '') {
result = '';
} else if (/^[0-90-9]+$/.test(trimmed)) {
// 数字が入っていたらローマ数字にする
const n = Number(trimmed.normalize('NFKC'));
const roman = toRoman(n);
if (roman === null) error = `${MIN}から${MAX}までの数を入れてください`;
else result = roman;
} else {
// それ以外はローマ数字として読む
const n = fromRoman(trimmed);
if (n === null) error = 'ローマ数字として読めませんでした';
else result = String(n);
}
return (
<div className="roman-converter">
<label htmlFor={id}>数字かローマ数字</label>
<input
id={id}
value={text}
autoComplete="off"
onChange={(e) => setText(e.target.value)}
aria-describedby={`${id}-result`}
/>
<output id={`${id}-result`} htmlFor={id} className={error ? 'error' : ''}>
{error || result}
</output>
</div>
);
}
.roman-converter { display: grid; gap: 8px; max-width: 480px; }
.roman-converter input { font-size: 16px; padding: 6px 8px; }
.roman-converter output { min-height: 1.5em; font-size: 28px; color: #067a98; overflow-wrap: anywhere; }
.roman-converter output.error { font-size: 16px; color: #d85878; }
結果は <output> 要素に出しています。MDN の output 要素の説明にあるとおり、<output> の暗黙の ARIA ロールは status です。検証環境の Chromium でもアクセシビリティツリー上のロールは status になっていたので、role="status" は付けていません。
Playwright で欄を空にして1文字ずつ打ち、表示を確かめた結果は次のとおりです。
| 打った文字 | 表示 |
|---|---|
3888 |
MMMDCCCLXXXVIII |
4000 |
1から3999までの数を入れてください |
0 |
1から3999までの数を入れてください |
2026(全角) |
MMXXVI |
mcmxcix |
1999 |
IIII |
ローマ数字として読めませんでした |
Ⅻ |
12 |
「MCMXCIX」を1文字ずつ打つと、表示は 1000、1100、1900、1910、1990、1991、1999 と変わりました。途中の「MC」や「MCM」も正しいローマ数字なので、エラーにならずにその時点の数が出ます。幅 375px の画面で、横にはみ出す表示はありませんでした。
Intl.NumberFormat と CSS でできること
MDN の Intl.supportedValuesOf() の説明には、数字の書き方(numberingSystem)の値として roman(大文字のローマ数字)と romanlow(小文字のローマ数字)が「アルゴリズム」の種類として載っています。しかし、検証環境の Node.js と Chromium で new Intl.NumberFormat('en', { numberingSystem: 'roman' }) を作ると、resolvedOptions().numberingSystem は latn になり、format(2026) は 2,026 でした。Intl.supportedValuesOf('numberingSystem') にも roman は入っていませんでした。検証環境では Intl.NumberFormat でローマ数字の文字列を作れなかったので、上のような関数を使います。
画面の上で番号として見せるだけなら、CSS でもできます。MDN の list-style-type の説明にある upper-roman を <ol> に指定すると、項目の番号が大文字のローマ数字になります。
.css-roman ol { list-style-type: upper-roman; list-style-position: inside; padding-left: 0; margin: 0; font-size: 18px; }

<ol start="3998"> に4つの項目を入れたところ、3998 と 3999 はローマ数字になり、4000 と 4001 は普通の数字になりました。start="0" の項目も「0.」です。CSS Counter Styles の仕様では、範囲(range)の外の数は代わりのスタイル(fallback)で書くと定められていて、fallback の初期値は decimal です。上の結果はこの定めのとおりです。
CSS でできるのは、リストやカウンターの番号の見た目を変えるところまでです。入力欄で数字とローマ数字を行き来させたり、変換した文字列を JavaScript で使ったりするときは、roman.ts のような関数を使います。





