1. 概述

罗马数字是古罗马人发明的一种计数系统,用特定字母代表不同数值。虽然现在日常使用不多,但在钟表、书籍章节编号、电影出品年份等场景仍能看到它的身影。

本文将带你实现一个简单高效的双向转换器,支持罗马数字 ↔ 阿拉伯数字之间的互转。代码简洁、逻辑清晰,适合集合备用。


2. 罗马数字基础

罗马数字由 7 个基本符号构成,每个符号对应一个固定值:

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

早期人们用 IIII 表示 4,XXXX 表示 40,但这种写法容易误读(比如看成 III)。为提升可读性,后来引入了减法规则(subtractive notation)

  • IIII → ✅ IV(表示 5 - 1)
  • XXXX → ✅ XL(表示 50 - 10)
  • CCCC → ✅ CD(表示 500 - 100)

⚠️ 这个细节很关键!意味着我们在解析时不能简单地逐字符相加,必须向前看一位,判断是否构成“减法组合”。


3. 数据模型设计

为了方便处理减法情况,我们直接在枚举中预定义所有合法符号组合,包括 IV, IX, XL, XC, CD, CM

enum RomanNumeral {
    I(1), IV(4), V(5), IX(9), X(10), 
    XL(40), L(50), XC(90), C(100), 
    CD(400), D(500), CM(900), M(1000);

    private int value;

    RomanNumeral(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
    
    public static List<RomanNumeral> getReverseSortedValues() {
        return Arrays.stream(values())
          .sorted(Comparator.comparing((RomanNumeral e) -> e.value).reversed())
          .collect(Collectors.toList());
    }
}

📌 关键点说明:

  • getReverseSortedValues() 返回按数值从大到小排序的符号列表
  • ✅ 转换时优先匹配大值符号,简单粗暴且高效
  • ✅ 把 IV, IX 等作为独立枚举项,避免手动判断减法规则,减少出错概率

4. 罗马数字转阿拉伯数字

罗马数字表示范围通常是 1 ~ 3999(极少数扩展到4000),我们这里也限制在这个区间。

4.1 实现思路

核心算法:贪心匹配 + 字符串前缀判断

输入:一个罗马数字字符串
初始化结果为 0
从最大符号开始遍历(M → I)
  如果当前字符串以该符号命名开头:
    将其对应数值加到结果
    去掉字符串开头这部分
  否则:
    尝试下一个更小的符号
直到字符串为空或遍历完所有符号

4.2 Java 实现

public static int romanToArabic(String input) {
    String romanNumeral = input.toUpperCase();
    int result = 0;
        
    List<RomanNumeral> romanNumerals = RomanNumeral.getReverseSortedValues();

    int i = 0;

    while ((romanNumeral.length() > 0) && (i < romanNumerals.size())) {
        RomanNumeral symbol = romanNumerals.get(i);
        if (romanNumeral.startsWith(symbol.name())) {
            result += symbol.getValue();
            romanNumeral = romanNumeral.substring(symbol.name().length());
        } else {
            i++;
        }
    }

    if (romanNumeral.length() > 0) {
        throw new IllegalArgumentException(input + " 不是合法的罗马数字");
    }

    return result;
}

📌 踩坑提醒:

  • 必须先转成大写(toUpperCase()),避免大小写问题
  • 最后检查 romanNumeral.length() > 0,防止非法输入如 "MXIIII""ABC"
  • 使用 startsWith()substring() 配合,逻辑清晰不易错

4.3 测试验证

@Test
void given2018Roman_WhenConvertingToArabic_ThenReturn2018() {
    String roman2018 = "MMXVIII";

    int result = RomanArabicConverter.romanToArabic(roman2018);

    assertThat(result).isEqualTo(2018);
}

✅ 输出:2018 —— 转换正确!


5. 阿拉伯数字转罗马数字

反过来也一样,使用贪心策略:每次都选不超过当前数值的最大符号。

5.1 实现思路

输入:一个 1~3999 的整数
初始化结果为空字符串
从最大符号开始遍历(M → I)
  如果当前符号的值 ≤ 剩余数值:
    把该符号追加到结果中
    从剩余数值中减去该符号的值
  否则:
    尝试下一个更小的符号
重复直到数值归零

5.2 Java 实现

public static String arabicToRoman(int number) {
    if ((number <= 0) || (number > 4000)) {
        throw new IllegalArgumentException(number + " 超出支持范围 (1~3999)");
    }

    List<RomanNumeral> romanNumerals = RomanNumeral.getReverseSortedValues();

    int i = 0;
    StringBuilder sb = new StringBuilder();

    while ((number > 0) && (i < romanNumerals.size())) {
        RomanNumeral currentSymbol = romanNumerals.get(i);
        if (currentSymbol.getValue() <= number) {
            sb.append(currentSymbol.name());
            number -= currentSymbol.getValue();
        } else {
            i++;
        }
    }

    return sb.toString();
}

📌 优点:

  • ✅ 利用预定义的 getReverseSortedValues(),天然支持贪心匹配
  • StringBuilder 提高性能
  • ✅ 逻辑对称统一,和上一个方法结构几乎一致,便于维护

5.3 测试验证

@Test
void given1999Arabic_WhenConvertingToRoman_ThenReturnMCMXCIX() {
    int arabic1999 = 1999;

    String result = RomanArabicConverter.arabicToRoman(arabic1999);

    assertThat(result).isEqualTo("MCMXCIX");
}

✅ 输出:MCMXCIX —— 正确表示 1999(1000 + 900 + 90 + 9)


6. 总结

本文实现了一个双向罗马数字转换器,具备以下特点:

  • ✅ 使用 enum 统一管理符号和值,结构清晰
  • ✅ 支持减法规则(IV, IX 等),无需额外逻辑判断
  • ✅ 两个转换方法均采用贪心策略,代码简洁高效
  • ✅ 包含边界检查和异常处理,健壮性强
  • ✅ 单元测试覆盖典型用例,放心使用

完整代码已托管至 GitHub:https://github.com/johnchen902/RomanNumeralConverter

这类小工具看似简单,但在实际项目中偶尔会用到(比如生成文档标题编号、解析历史数据等),建议集合备用。


原始标题:Converting Between Roman and Arabic Numerals in Java