Fun with Python: Converting To and From Roman Numerals

avatar
(Edited)

Hey everyone,

It's been a little while since I've done a "Fun with Python" post, with the last one being the word_clock.py script that tells time with words. Today, I've got another fun little project to share: a command-line tool I wrote to convert integers to and from Roman numerals.

Roman numerals are an interesting programming puzzle. It's not just a simple one-to-one mapping of numbers to letters; you have to account for the subtractive principle where "IV" is 4 (not "IIII") and "CM" is 900 (not "DCCCC"). It makes for a great little logic challenge.

The roman.py Script

I put together a simple Python script to handle these conversions. It's a command-line tool that can take an integer and give you the Roman numeral, or take a Roman numeral and give you the integer.

A neat feature I included is support for larger numbers using the vinculum (or overbar) notation, where a bar over a numeral multiplies its value by 1,000. So, is 10,000, is 100,000, and so on.

How It Works

Well beyond the Roman Era

  • Integer to Roman: To convert a number like 1994, the script works its way down from the largest values. It sees that 1994 is bigger than 1000, so it adds an "M" and subtracts 1000, leaving 994. Then it sees 900, adds "CM" and subtracts 900, leaving 94. It continues this for 90 ("XC") and 4 ("IV") until it gets to zero, building the final string: MCMXCIV.
  • Roman to Integer: Going the other way, it reads the Roman numeral string from left to right, looking for the largest symbol it can match first (so it will see "CM" before it sees "C"). It adds that symbol's value to a running total and then chops that symbol off the string, repeating the process until the string is empty.

The Code

Here is the full script. It's self-contained and doesn't require any external libraries.

#!/usr/bin/env python3

import argparse
from typing import List, Tuple

# Ordered list of Roman numeral symbols (value, symbol) in descending order.
# Includes overbar (vinculum) characters for larger numbers.
SYMBOLS: List[Tuple[int, str]] = [
    (1000000, "M̅"),
    (100000, "C̅"),
    (10000, "X̅"),
    (1000, "M"),
    (900, "CM"),
    (500, "D"),
    (400, "CD"),
    (100, "C"),
    (90, "XC"),
    (50, "L"),
    (40, "XL"),
    (10, "X"),
    (9, "IX"),
    (5, "V"),
    (4, "IV"),
    (1, "I"),
]

# Derived mapping for quick symbol → value lookups.
SYMBOL_TO_VALUE = {s: v for v, s in SYMBOLS}


def int_to_roman(num: int) -> str:
    """
    Convert an integer to a Roman numeral.
    """
    if not 0 < num < 4000000:  # Practical upper bound
        raise ValueError("Integer must be between 1 and 3,999,999.")

    roman_numeral = ""
    # Build the numeral greedily from largest to smallest value.
    for value, symbol in SYMBOLS:
        while num >= value:
            roman_numeral += symbol
            num -= value
    return roman_numeral


def roman_to_int(roman: str) -> int:
    """
    Convert a Roman numeral to an integer.
    """
    if not roman:
        raise ValueError("Roman numeral cannot be empty.")

    roman = roman.upper()
    # Sort symbols by length so multi-char symbols (like 'CM') match first.
    symbols = sorted(SYMBOL_TO_VALUE.keys(), key=len, reverse=True)

    value = 0
    while roman:
        matched = False
        for symbol in symbols:
            if roman.startswith(symbol):
                value += SYMBOL_TO_VALUE[symbol]
                roman = roman[len(symbol) :]
                matched = True
                break
        if not matched:
            raise ValueError(f"Invalid character or sequence in Roman numeral: '{roman}'")
    return value


def main() -> int:
    """Entry-point for command-line interface."""
    parser = argparse.ArgumentParser(
        description="Convert between integers and Roman numerals."
    )

    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "-i", "--integer", type=int, help="Integer to convert to Roman numeral."
    )
    group.add_argument(
        "-r", "--roman", type=str, help="Roman numeral to convert to integer."
    )

    args = parser.parse_args()

    try:
        if args.integer is not None:
            print(int_to_roman(args.integer))
        else:
            print(roman_to_int(args.roman))
    except ValueError as exc:
        parser.error(str(exc))
    return 0


if __name__ == "__main__":
    main()

How to Use It

Using it from your terminal is straightforward:

To convert an integer to a Roman numeral:

python3 roman.py --integer 2025

Output: MMXXV

To convert a Roman numeral to an integer:

python3 roman.py --roman MCMXCIV

Output: 1994

And here's an example with a larger number:

python3 roman.py -i 12345

Output: X̅MMCCCXLV

It's a simple, fun project and a great way to practice some basic algorithm logic. Hope you enjoy it!

As always,
Michael Garcia a.k.a. TheCrazyGM



0
0
0.000
4 comments
avatar

All the vibe coders should also learn to code with great content like this! We can make a collextion later.

!PAKX
!PIMP
!PIZZA

0
0
0.000
avatar

While I never learned Roman numerals in depth, I had no idea that they presented such interesting challenges to converting to and from Arabic numerals. Very cool, I learned something! 😁 🙏 💚 ✨ 🤙

0
0
0.000