Fun with Python: Converting To and From Roman Numerals
hive-186392·@thecrazygm·
10.234 HBDFun with Python: Converting To and From Roman Numerals
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, `X̅` is 10,000, `C̅` is 100,000, and so on. #### How It Works  - **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. ```python #!/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: ```bash python3 roman.py --integer 2025 ``` Output: `MMXXV` To convert a Roman numeral to an integer: ```bash python3 roman.py --roman MCMXCIV ``` Output: `1994` And here's an example with a larger number: ```bash 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
👍 beco132, freecompliments, teamvn, hdmed, hetty-rowan, wongi, boeltermc, beststart, poplar-22, ngwinndave, georgesantana73, hive-140084, pepetoken, fc-curation, fc-rewards, fc-arbitration, indiasierra, trovepower, mmbbot, jacor, moretea, morwhale, coolguy123, guurry123, gurseerat, invest4free, captaincryptic, trashyomen, vrezyy, tub3r0, chrisly.social, holdeck2, vrezion, tengolotodo.leo, timix648, converter.bonus, bankrobbery, spoonies, d-a-d, pepe.voter, claudiavb, murtaza-7868, digi-alt, michael561, hive.pizza, joeyarnoldvn, emsenn0, prohive, savvyfrog, techken, morwhaleplus, morwhalebonus, najat, franco10, grocko, diosarich, fireguardian, ravenmus1c, adamada, manclar, hivetrending, goliathus, dadspardan, huzzah, cooperclub, creodas, wizzitywillican, thecbp-hiver, john9inch, szmobacsi, banzafahra, thedoc07, lothbrox, highfist, the-pockets, dalekma, masterfarmer, life7clothing, svanbo, jazzhero, bestofph, esmeesmith, fonestreet, casimodo, mk992039, instagram-models, partytime.inleo, lisamgentile1961, ijebest, bambukah, steembasicincome, sbi2, sbi3, anikys3reasure, awesomegames007, sbi-tokens, qwertm, sbi4, sbi5, sbi6, sneakyninja, sbi7, sbi8, sbi9, sbi10, penguinpablo, cryptonized, hive.friends, alphacore, hungrybear, funnyman, andriko, gamer00, neoxvoter, pixelfan, jacuzzi, chaosmagic23, chaos23, slothlydoesit, sports.power.bot, kiemis, youloseagain, mviews, holovision.stem, learn2code, stem.alfa, slothburn, calmphoenix, rudy-dj, kasih-sayang, rulivyxo, ecoinstant, msp-makeaminnow, voter000, promo-mentors, fatman, eturnerx, cryptoknight12, jozefkrichards, voter002, bilpcoinbpc, mvd, dante31, futurethinker, osarueseosato, tinyhousecryptos, eturnerx-dbuzz, dpend.active, sketching, mxm0unite, tomiscurious, pakx, bbarelyseal, ronasoliva1104, artlover, podanrj, lrekt01, deepresearch, szukamnemo, techguard, omra-sky, longhunter, cryptoniusrex, mirroredspork, mes, adulruna, deggial, magic.byte, tydynrain, likedeeler, dune69, followjohngalt, tiffin, mcsherriff, herman-german, ecoinstats, treasure.hoard, ryosai, thecrazygm.bank, blessskateshop, archon-gov, irisworld, pof.archon, abrockman, monsterrerentals, everythingsmgirl, ykretz, eforucom, fw206, woelfchen, ray5fan, niallon11, fjworld, lolz.byte,