Roman Numeral Converter — Java source
Convert integers up to 3,999,999 to Roman numerals and back. Vinculum overline above 3,999, canonical-form validation, a step-by-step greedy breakdown, and 14 language sources. Runs entirely in your browser.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// roman-numeral-converter - Roman <-> Arabic (vinculum, 1..3,999,999).
//
// Language: Java (17+, standard library only)
// Source: CosmoDev polyglot showcase port of the Roman Numeral Converter tool,
// ported from src/lib/roman-numeral.ts (the canonical TypeScript
// implementation); kept in lock-step with the Go twin at
// cli/roman-numeral-converter/roman-numeral-converter.go.
// License: display source - part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never throws on bad input (returns "" / null).
// - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
// - Self-contained: stdlib only.
//
// Algorithm: one ordered (value, symbol) table for 1..3,999 drives both
// directions. toRoman greedily subtracts the largest fitting symbol; above
// 3,999 the thousands part is rendered with the same table and each glyph
// gains a combining overline (U+0305) meaning x 1,000. fromRoman scans
// left-to-right where a smaller letter before a larger one subtracts
// (IV = 4, CM = 900), then RE-RENDERS the parsed total and rejects anything
// that doesn't round-trip - that one check enforces canonical form
// (rejecting "IIII", "VV", "IC", plain "MMMM" for 4,000).
import java.util.Locale;
public final class RomanNumeral {
static final String OVERLINE = "\u0305"; // vinculum: value x 1,000
static final String MACRON = "\u0304"; // accepted on input, normalized
static final long MAX_ROMAN = 3_999_999L;
static final long[] BASE_VAL = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
static final String[] BASE_SYM = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
private RomanNumeral() {}
static long letterValue(char c) {
switch (c) {
case 'I': return 1;
case 'V': return 5;
case 'X': return 10;
case 'L': return 50;
case 'C': return 100;
case 'D': return 500;
case 'M': return 1000;
default: return 0;
}
}
static String overline(String s) {
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
out.append(s.charAt(i)).append(OVERLINE);
}
return out.toString();
}
/** Greedy render of 1..3,999. */
static String toRomanBase(long v) {
StringBuilder out = new StringBuilder();
for (int i = 0; i < BASE_VAL.length; i++) {
while (v >= BASE_VAL[i]) {
out.append(BASE_SYM[i]);
v -= BASE_VAL[i];
}
}
return out.toString();
}
/** Converts 1..3,999,999 ("" when out of range). Above 3,999 the thousands
* part carries a combining overline per glyph. */
public static String toRoman(long n) {
if (n < 1 || n > MAX_ROMAN) return "";
if (n <= 3999) return toRomanBase(n);
String out = overline(toRomanBase(n / 1000));
if (n % 1000 > 0) out += toRomanBase(n % 1000);
return out;
}
/** One left-to-right pass where a smaller letter before a larger one
* subtracts. Returns junk for non-canonical strings - the round-trip in
* fromRoman is the canonicality gate. */
static long scanValue(String s) {
long total = 0;
for (int i = 0; i < s.length(); i++) {
long v = letterValue(s.charAt(i));
long next = i + 1 < s.length() ? letterValue(s.charAt(i + 1)) : 0;
total += next > v ? -v : v;
}
return total;
}
/** Parses a canonical numeral (plain or vinculum), or null. Trimmed and
* uppercased first; a pasted macron counts as the overline mark. */
public static Long fromRoman(String s) {
String input = s.trim().toUpperCase(Locale.ROOT).replace(MACRON, OVERLINE);
// Split into overlined glyphs (letter + mark) and plain letters.
StringBuilder over = new StringBuilder();
StringBuilder plain = new StringBuilder();
int i = 0;
while (i < input.length()) {
char c = input.charAt(i);
if (letterValue(c) == 0) return null;
if (i + 1 < input.length() && input.charAt(i + 1) == '\u0305') {
over.append(c);
i += 2;
} else {
plain.append(c);
i++;
}
}
long total = 0;
if (over.length() > 0) total += scanValue(over.toString()) * 1000;
if (plain.length() > 0) total += scanValue(plain.toString());
if (total < 1 || total > MAX_ROMAN) return null;
return toRoman(total).equals(input) ? total : null;
}
// ---------- showcase (run: java RomanNumeral.java) ----------
public static void main(String[] args) {
// toRoman - known values, both scales
assert toRoman(1L).equals("I");
assert toRoman(1994L).equals("MCMXCIV");
assert toRoman(3999L).equals("MMMCMXCIX");
assert toRoman(4000L).equals("I\u0305V\u0305");
assert toRoman(4001L).equals("I\u0305V\u0305I");
assert toRoman(3_999_999L).equals("M\u0305M\u0305M\u0305C\u0305M\u0305X\u0305C\u0305I\u0305X\u0305CMXCIX");
// toRoman - out of range
assert toRoman(0L).isEmpty();
assert toRoman(4_000_000L).isEmpty();
// fromRoman - canonical, with case/whitespace/macron tolerance
assert fromRoman("MCMXCIV") == 1994;
assert fromRoman(" mcmxciv ") == 1994;
assert fromRoman("I\u0305V\u0305") == 4000;
assert fromRoman("I\u0304V\u0304") == 4000; // macron
// fromRoman - non-canonical / invalid
assert fromRoman("IIII") == null;
assert fromRoman("VV") == null;
assert fromRoman("IC") == null;
assert fromRoman("MMMM") == null; // 4,000 must be vinculum
assert fromRoman("ABC") == null;
assert fromRoman("") == null;
System.out.println("all showcase assertions passed");
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →