From 1ff0281324d0fcb1ad2523c3512939dfc886ba95 Mon Sep 17 00:00:00 2001 From: Prayas Dey Date: Mon, 17 Aug 2026 22:23:07 +0530 Subject: [PATCH 1/3] test(Converting_Roman_to_Integer): add comprehensive unit test suite --- .../test_roman_to_integer.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 Converting_Roman_to_Integer/test_roman_to_integer.py diff --git a/Converting_Roman_to_Integer/test_roman_to_integer.py b/Converting_Roman_to_Integer/test_roman_to_integer.py new file mode 100644 index 00000000..9a76434d --- /dev/null +++ b/Converting_Roman_to_Integer/test_roman_to_integer.py @@ -0,0 +1,122 @@ +""" +Unit tests for Converting_Roman_to_Integer module. +""" + +import unittest +from Converting_Roman_to_Integer import ( + roman_to_int, + int_to_roman, + is_valid_roman, +) + + +class TestRomanToInteger(unittest.TestCase): + """Test cases for Roman numeral to integer conversions.""" + + def test_single_symbols(self): + self.assertEqual(roman_to_int('I'), 1) + self.assertEqual(roman_to_int('V'), 5) + self.assertEqual(roman_to_int('X'), 10) + self.assertEqual(roman_to_int('L'), 50) + self.assertEqual(roman_to_int('C'), 100) + self.assertEqual(roman_to_int('D'), 500) + self.assertEqual(roman_to_int('M'), 1000) + + def test_additive_combinations(self): + self.assertEqual(roman_to_int('III'), 3) + self.assertEqual(roman_to_int('VI'), 6) + self.assertEqual(roman_to_int('XV'), 15) + self.assertEqual(roman_to_int('LVIII'), 58) + self.assertEqual(roman_to_int('MDCLXVI'), 1666) + + def test_subtractive_combinations(self): + self.assertEqual(roman_to_int('IV'), 4) + self.assertEqual(roman_to_int('IX'), 9) + self.assertEqual(roman_to_int('XL'), 40) + self.assertEqual(roman_to_int('XC'), 90) + self.assertEqual(roman_to_int('CD'), 400) + self.assertEqual(roman_to_int('CM'), 900) + self.assertEqual(roman_to_int('MCMXCIV'), 1994) + + def test_case_insensitivity_and_whitespace(self): + self.assertEqual(roman_to_int('iv'), 4) + self.assertEqual(roman_to_int(' mcmxciv '), 1994) + self.assertEqual(roman_to_int('lViIi'), 58) + + def test_invalid_characters(self): + with self.assertRaises(ValueError): + roman_to_int('ABC') + with self.assertRaises(ValueError): + roman_to_int('123') + with self.assertRaises(ValueError): + roman_to_int('XIV12') + + def test_invalid_syntax(self): + with self.assertRaises(ValueError): + roman_to_int('IIII') + with self.assertRaises(ValueError): + roman_to_int('VV') + with self.assertRaises(ValueError): + roman_to_int('IC') + with self.assertRaises(ValueError): + roman_to_int('IL') + with self.assertRaises(ValueError): + roman_to_int('XD') + + def test_empty_and_non_string_inputs(self): + with self.assertRaises(ValueError): + roman_to_int('') + with self.assertRaises(ValueError): + roman_to_int(' ') + with self.assertRaises(TypeError): + roman_to_int(123) # type: ignore + + +class TestIntegerToRoman(unittest.TestCase): + """Test cases for integer to Roman numeral conversions.""" + + def test_valid_integers(self): + self.assertEqual(int_to_roman(1), 'I') + self.assertEqual(int_to_roman(4), 'IV') + self.assertEqual(int_to_roman(9), 'IX') + self.assertEqual(int_to_roman(58), 'LVIII') + self.assertEqual(int_to_roman(1994), 'MCMXCIV') + self.assertEqual(int_to_roman(3999), 'MMMCMXCIX') + + def test_round_trip_conversion(self): + for num in [1, 4, 9, 14, 44, 99, 400, 944, 1994, 2024, 3999]: + roman = int_to_roman(num) + self.assertEqual(roman_to_int(roman), num) + + def test_out_of_range_integers(self): + with self.assertRaises(ValueError): + int_to_roman(0) + with self.assertRaises(ValueError): + int_to_roman(-5) + with self.assertRaises(ValueError): + int_to_roman(4000) + + def test_type_error(self): + with self.assertRaises(TypeError): + int_to_roman("123") # type: ignore + + +class TestRomanValidation(unittest.TestCase): + """Test cases for Roman numeral validator.""" + + def test_valid_patterns(self): + self.assertTrue(is_valid_roman('I')) + self.assertTrue(is_valid_roman('MMMCMXCIX')) + self.assertTrue(is_valid_roman('MCMXCIV')) + self.assertTrue(is_valid_roman('CDXLIV')) + + def test_invalid_patterns(self): + self.assertFalse(is_valid_roman('')) + self.assertFalse(is_valid_roman('IIII')) + self.assertFalse(is_valid_roman('MMMM')) + self.assertFalse(is_valid_roman('VX')) + self.assertFalse(is_valid_roman('123')) + + +if __name__ == '__main__': + unittest.main() From d64a4ddfa44d1016c3bb6d0c26e32cc0e645ccd5 Mon Sep 17 00:00:00 2001 From: Prayas Dey Date: Mon, 17 Aug 2026 22:23:37 +0530 Subject: [PATCH 2/3] feat(Converting_Roman_to_Integer): add modular converter with validation, CLI & interactive mode --- .../Converting_Roman_to_Integer.py | 219 ++++++++++++++++-- 1 file changed, 197 insertions(+), 22 deletions(-) diff --git a/Converting_Roman_to_Integer/Converting_Roman_to_Integer.py b/Converting_Roman_to_Integer/Converting_Roman_to_Integer.py index 5ad1ef3b..c3a01e16 100644 --- a/Converting_Roman_to_Integer/Converting_Roman_to_Integer.py +++ b/Converting_Roman_to_Integer/Converting_Roman_to_Integer.py @@ -1,28 +1,203 @@ -import sys +""" +Roman to Integer (and Integer to Roman) Converter +================================================= +A robust, flexible utility to convert Roman numerals to integers +and vice versa, supporting both CLI arguments and interactive mode. +""" +import re +import sys +from typing import Dict -romanStr = sys.argv[1] -romanStr = str(romanStr) -dict = { - 'I':1, - 'V':5, - 'X':10, - 'L':50, - 'C':100, - 'D':500, - 'M':1000 +# Mapping of Roman numeral symbols to integer values +ROMAN_VALUES: Dict[str, int] = { + 'I': 1, + 'V': 5, + 'X': 10, + 'L': 50, + 'C': 100, + 'D': 500, + 'M': 1000, } -num = 0 +# Value-to-symbol pairs for Integer to Roman conversion +INTEGER_TO_ROMAN_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'), +] + +# Standard Roman Numeral Regex Pattern (1 to 3999) +ROMAN_REGEX = re.compile(r'^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$') + + +def is_valid_roman(roman_str: str) -> bool: + """ + Validate if a given string is a syntactically valid standard Roman numeral (1 - 3999). + """ + if not roman_str or not isinstance(roman_str, str): + return False + return bool(ROMAN_REGEX.fullmatch(roman_str.strip().upper())) + + +def roman_to_int(roman_str: str, validate: bool = True) -> int: + """ + Convert a Roman numeral string to an integer. + + Parameters: + roman_str (str): The Roman numeral string (case-insensitive). + validate (bool): Whether to strictly validate standard Roman numeral syntax. + + Returns: + int: The integer value of the Roman numeral. + + Raises: + ValueError: If input is empty, contains invalid characters, or fails validation. + """ + if not isinstance(roman_str, str): + raise TypeError("Input must be a string.") + + cleaned_str = roman_str.strip().upper() + + if not cleaned_str: + raise ValueError("Input string cannot be empty.") + + # Check for invalid characters + invalid_chars = [c for c in cleaned_str if c not in ROMAN_VALUES] + if invalid_chars: + raise ValueError( + f"Invalid Roman character(s) found: {', '.join(set(invalid_chars))}. " + f"Valid characters are {', '.join(ROMAN_VALUES.keys())}." + ) + + # Validate standard syntax if requested + if validate and not is_valid_roman(cleaned_str): + raise ValueError(f"'{roman_str.strip()}' is not a valid standard Roman numeral.") + + total = 0 + prev_value = 0 + + # Parse right-to-left + for char in reversed(cleaned_str): + current_value = ROMAN_VALUES[char] + if current_value < prev_value: + total -= current_value + else: + total += current_value + prev_value = current_value + + return total + + +def int_to_roman(number: int) -> str: + """ + Convert an integer (1 - 3999) to its standard Roman numeral representation. + + Parameters: + number (int): Integer to convert (between 1 and 3999). + + Returns: + str: The corresponding Roman numeral. + + Raises: + ValueError: If number is not within the range 1 to 3999. + """ + if not isinstance(number, int): + raise TypeError("Input must be an integer.") + + if not (1 <= number <= 3999): + raise ValueError("Number must be between 1 and 3999.") + + roman_digits = [] + for value, symbol in INTEGER_TO_ROMAN_MAP: + if number == 0: + break + count, number = divmod(number, value) + roman_digits.append(symbol * count) + + return "".join(roman_digits) + + +def print_help() -> None: + """Print command-line usage information.""" + print("Roman to Integer & Integer to Roman Converter") + print("---------------------------------------------") + print("Usage:") + print(" python Converting_Roman_to_Integer.py ") + print(" python Converting_Roman_to_Integer.py ") + print(" python Converting_Roman_to_Integer.py --help") + print("\nExamples:") + print(" python Converting_Roman_to_Integer.py XIV -> 14") + print(" python Converting_Roman_to_Integer.py MCMXCIV -> 1994") + print(" python Converting_Roman_to_Integer.py 2024 -> MMXXIV") + print("\nIf no argument is passed, interactive mode will start.") + + +def interactive_mode() -> None: + """Run an interactive console loop for conversions.""" + print("=" * 50) + print(" Welcome to the Roman Numeral Converter!") + print(" Enter a Roman numeral or integer (or 'q' to quit)") + print("=" * 50) + + while True: + try: + user_input = input("\nEnter input: ").strip() + if not user_input: + continue + if user_input.lower() in ('q', 'quit', 'exit'): + print("Goodbye!") + break + + # If input is digits, convert integer -> Roman + if user_input.isdigit(): + val = int(user_input) + result = int_to_roman(val) + print(f" Integer: {val} --> Roman Numeral: {result}") + else: + # Convert Roman -> Integer + result = roman_to_int(user_input) + print(f" Roman Numeral: {user_input.upper()} --> Integer: {result}") + + except ValueError as err: + print(f" Error: {err}") + except (KeyboardInterrupt, EOFError): + print("\nGoodbye!") + break + + +def main() -> None: + """Main CLI entry point.""" + if len(sys.argv) > 1: + arg = sys.argv[1].strip() + + if arg in ('-h', '--help', 'help'): + print_help() + sys.exit(0) + + # Check if argument is integer or Roman numeral + try: + if arg.isdigit(): + val = int(arg) + print(int_to_roman(val)) + else: + print(roman_to_int(arg)) + except ValueError as err: + print(f"Error: {err}", file=sys.stderr) + sys.exit(1) + else: + interactive_mode() -romanStr = romanStr.replace("IV","IIII") -romanStr = romanStr.replace("IX","VIIII") -romanStr = romanStr.replace("XL","XXXX") -romanStr = romanStr.replace("XC","LXXXX") -romanStr = romanStr.replace("CD","CCCC") -romanStr = romanStr.replace("CM","DCCCC") -myStr = list(romanStr) -for char in myStr: - num = num + dict[char] -print(num) \ No newline at end of file +if __name__ == '__main__': + main() \ No newline at end of file From ad3aff4b613415607c0dfc5a3dbea750695338cf Mon Sep 17 00:00:00 2001 From: Prayas Dey Date: Mon, 17 Aug 2026 22:23:39 +0530 Subject: [PATCH 3/3] docs(Converting_Roman_to_Integer): add complete documentation matching repository template --- Converting_Roman_to_Integer/README.md | 86 ++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/Converting_Roman_to_Integer/README.md b/Converting_Roman_to_Integer/README.md index 0fbaa13b..1ff15b9d 100644 --- a/Converting_Roman_to_Integer/README.md +++ b/Converting_Roman_to_Integer/README.md @@ -1,2 +1,84 @@ -Fixed issue #73, Convert Roman to Integer. -CLI python 3.10 script to convert the Roman to Integer. + +![Star Badge](https://img.shields.io/static/v1?label=%F0%9F%8C%9F&message=If%20Useful&style=style=flat&color=BC4E99) +![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103) + +# Roman to Integer & Integer to Roman Converter + +A versatile, user-friendly Python CLI tool and importable module that accurately converts standard Roman numerals (1 to 3999) to integers and vice versa. It includes rigorous input validation, error handling, interactive console mode, command-line arguments support, and comprehensive unit tests. + +## 🛠️ Description + +This script provides bidirectional conversion between Roman numerals and Arabic integers: +1. **Roman to Integer Conversion**: Converts Roman numeral strings (e.g., `MCMXCIV` $\rightarrow$ `1994`, `XIV` $\rightarrow$ `14`). +2. **Integer to Roman Conversion**: Converts integers (1 to 3999) to standard Roman numerals (e.g., `2024` $\rightarrow$ `MMXXIV`). +3. **Syntax & Character Validation**: Validates standard Roman numeral grammar using regular expressions to prevent illegal combinations like `IIII` or `VV`. +4. **Input Sanitization**: Handles case-insensitive input (`xiv`, `MCMXCIV`) and trims extraneous whitespaces. +5. **Interactive & CLI Support**: Run with arguments for instant answers or launch without arguments for an interactive loop. + +## ⚙️ Languages or Frameworks Used + +- **Language:** Python 3 (standard library only) +- **Built-in Modules:** `re`, `sys`, `unittest`, `typing` + +## 🌟 How to run + +### 1. Interactive Mode +Run the script without arguments to start the interactive prompt: +```bash +python Converting_Roman_to_Integer.py +``` +**Example Session:** +```text +================================================== + Welcome to the Roman Numeral Converter! + Enter a Roman numeral or integer (or 'q' to quit) +================================================== + +Enter input: XIV + Roman Numeral: XIV --> Integer: 14 + +Enter input: 1994 + Integer: 1994 --> Roman Numeral: MCMXCIV + +Enter input: q +Goodbye! +``` + +### 2. Command Line Arguments +Pass a Roman numeral or integer directly via CLI: +```bash +# Convert Roman numeral to integer +python Converting_Roman_to_Integer.py MCMXCIV +# Output: 1994 + +# Convert integer to Roman numeral +python Converting_Roman_to_Integer.py 2024 +# Output: MMXXIV + +# View help and usage +python Converting_Roman_to_Integer.py --help +``` + +### 3. Running Unit Tests +Run the unit test suite to verify conversions and edge cases: +```bash +python -m unittest test_roman_to_integer.py +``` + +## 📺 Demo + +```text +$ python Converting_Roman_to_Integer.py MCMXCIV +1994 + +$ python Converting_Roman_to_Integer.py 58 +LVIII + +$ python Converting_Roman_to_Integer.py IIII +Error: 'IIII' is not a valid standard Roman numeral. +``` + +## 🤖 Author + +- Open Source Contribution +- Repository: [python-mini-project](https://github.com/ndleah/python-mini-project)