How to Convert Integers to Roman Numerals and Viceversa in Python
Roman numerals are a numeric system originating in ancient Rome, using combinations of letters from the Latin alphabet to signify values. While modern computing uses the decimal system, converting integers to Roman numerals is a common task in software development for copyright dates, stylized lists, or generation of outlines.
This guide explains the logic behind Roman numeral composition and provides efficient Python solutions for converting integers to Roman numerals and vice versa.
Understanding Roman Numeral Logic
Standard Roman numerals use seven basic symbols. However, the system relies on a combination of addition and subtraction rules.
| Symbol | Value |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
The Subtraction Rule
To avoid four consecutive characters (like "IIII"), smaller values are placed before larger values to indicate subtraction. To write an efficient algorithm, we treat these special pairs as distinct "values."
- IV (4) and IX (9)
- XL (40) and XC (90)
- CD (400) and CM (900)
By including these subtraction pairs in our lookup table, we simplify the logic significantly. Instead of writing complex "if previous char" logic, we just treat "CM" as a single symbol worth 900.
Method 1: Integer to Roman (Greedy Algorithm)
The most efficient way to convert an integer to a Roman numeral is the Greedy Algorithm. We iterate through the values from largest to smallest, subtracting the value from the input number and appending the corresponding symbol until the number reaches zero.
def int_to_roman(num):
# Map values to symbols in descending order
# Includes standard subtraction pairs (e.g., 900, 400, 90, 40)
val_map = [
(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')
]
roman_numeral = ''
for value, symbol in val_map:
# While the number is large enough to contain this value
while num >= value:
roman_numeral += symbol
num -= value
return roman_numeral
# Testing the conversion
print(f"1994 -> {int_to_roman(1994)}")
print(f"58 -> {int_to_roman(58)}")
print(f"4 -> {int_to_roman(4)}")
Output:
1994 -> MCMXCIV
58 -> LVIII
4 -> IV
Method 2: Roman to Integer (Reverse Parsing)
To convert back from Roman numerals to an integer, we scan the string. A clever trick is to iterate in reverse. This allows us to maintain a running total where we add the value if it is greater than or equal to the previous value seen, or subtract it if it is smaller.
def roman_to_int(s):
roman_values = {
'I': 1, 'V': 5, 'X': 10,
'L': 50, 'C': 100, 'D': 500, 'M': 1000
}
total = 0
prev_value = 0
# Process from right to left (reversed)
for char in reversed(s.upper()):
current_value = roman_values.get(char, 0)
if current_value >= prev_value:
total += current_value
else:
total -= current_value
prev_value = current_value
return total
print(f"MCMXCIV -> {roman_to_int('MCMXCIV')}")
print(f"IV -> {roman_to_int('IV')}")
Output:
MCMXCIV -> 1994
IV -> 4
Handling Errors and Edge Cases
Standard Roman numerals are typically defined for the range 1 to 3999. Zero and negative numbers were not part of the original system, and standard notation breaks down above 3999 (requiring lines over letters to indicate multiplication by 1000).
You should implement validation to ensure inputs adhere to these constraints.
def int_to_roman(num):
# Map values to symbols in descending order
# Includes standard subtraction pairs (e.g., 900, 400, 90, 40)
val_map = [
(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')
]
roman_numeral = ''
for value, symbol in val_map:
# While the number is large enough to contain this value
while num >= value:
roman_numeral += symbol
num -= value
return roman_numeral
def safe_int_to_roman(num):
# ⛔️ Check for invalid ranges
if not isinstance(num, int):
raise TypeError("Input must be an integer.")
if not (0 < num < 4000):
raise ValueError("Number must be between 1 and 3999.")
# ... perform conversion logic here ...
return int_to_roman(num)
# ✅ Correct Usage
try:
print(safe_int_to_roman(2023))
except ValueError as e:
print(e)
# ⛔️ Error Usage
try:
print(safe_int_to_roman(0))
except ValueError as e:
print(f"Error: {e}")
Output:
MMXXIII
Error: Number must be between 1 and 3999.
Python dictionaries are fast, but roman_to_int will fail if the input string contains invalid characters (like 'A' or 'Z'). Always ensure roman_values.get(char) handles missing keys or validate the string beforehand.
Conclusion
Converting integers to Roman numerals is a classic algorithmic problem solved efficiently using a greedy approach with a lookup list containing subtraction pairs.
- Define Mappings: Create a list of tuples for values and symbols, including special cases like
900 (CM)and4 (IV). - Iterate Descending: Process the largest values first.
- Validate Inputs: Ensure the integer is within the standard range (1-3999).
- Reverse for Decoding: When converting back to integers, processing the string from right to left simplifies the logic for subtraction rules.