improve docs, black formatting
This commit is contained in:
86
roman/README.md
Normal file
86
roman/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Roman numerals converter
|
||||
|
||||
Original assessment brief: https://github.com/turner-townsend/backend-assessment/blob/master/README.md#roman-numerals
|
||||
|
||||
A simple Python program that converts Roman numerals to Arabic numbers using additive logic.
|
||||
|
||||
## Supported numerals
|
||||
|
||||
| Numeral | Value |
|
||||
| ------- | ----- |
|
||||
| I | 1 |
|
||||
| V | 5 |
|
||||
| X | 10 |
|
||||
| C | 100 |
|
||||
| M | 1000 |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python roman.py
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
Please enter some Roman numerals: I
|
||||
I = 1
|
||||
|
||||
Please enter some Roman numerals: VI
|
||||
VI = 5 + 1 = 6
|
||||
|
||||
Please enter some Roman numerals: MXVII
|
||||
MXVII = 1000 + 10 + 5 + 1 + 1 = 1017
|
||||
|
||||
Please enter some Roman numerals: ABC
|
||||
ERROR: Invalid input
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Additive conversion**: Treats all numerals as additive (IV = 1 + 5 = 6)
|
||||
- **Case insensitive**: Accepts both uppercase and lowercase
|
||||
- **Input filtering**: Ignores invalid characters, extracts valid numerals
|
||||
- **Interactive**: Continuous input until Ctrl+C
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+ (uses pattern matching syntax)
|
||||
- pytest (for running tests)
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pytest -v
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```shell
|
||||
➜ roman git:(main) ✗ pytest -v
|
||||
================================================================ test session starts ================================================================
|
||||
platform linux -- Python 3.12.2, pytest-8.3.4, pluggy-1.5.0 -- /home/jamey/.venv/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/jamey/Projects/turner-townsend-backend-assessment/roman
|
||||
plugins: mock-3.14.0, asyncio-0.21.2
|
||||
asyncio: mode=Mode.STRICT
|
||||
collected 9 items
|
||||
|
||||
test_roman.py::test_valid_single_numerals PASSED [ 11%]
|
||||
test_roman.py::test_valid_multiple_numerals PASSED [ 22%]
|
||||
test_roman.py::test_lowercase_input PASSED [ 33%]
|
||||
test_roman.py::test_mixed_case_input PASSED [ 44%]
|
||||
test_roman.py::test_invalid_characters PASSED [ 55%]
|
||||
test_roman.py::test_mixed_valid_and_invalid_characters PASSED [ 66%]
|
||||
test_roman.py::test_empty_string PASSED [ 77%]
|
||||
test_roman.py::test_only_spaces PASSED [ 88%]
|
||||
test_roman.py::test_spaces_and_valid_numerals PASSED [100%]
|
||||
|
||||
================================================================= 9 passed in 0.02s =================================================================
|
||||
```
|
||||
|
||||
## Design notes
|
||||
|
||||
- **Simple approach**: Uses additive logic only, as the brief states complex Roman numeral rules are not required
|
||||
- **User-friendly**: Filters out invalid characters rather than rejecting entire input
|
||||
- **Modern Python**: Uses pattern matching (requires Python 3.10+)
|
||||
@@ -1,36 +1,61 @@
|
||||
import readline
|
||||
|
||||
def convert_to_number(numerals: str) -> str:
|
||||
filtered = list(filter(lambda x: x in "IVXCM", list(numerals.upper())))
|
||||
if len(filtered) == 0:
|
||||
return "ERROR: Invalid input"
|
||||
|
||||
values: list[int] = []
|
||||
for i in range(len(filtered)):
|
||||
match filtered[i]:
|
||||
case "I":
|
||||
values.append(1)
|
||||
case "V":
|
||||
values.append(5)
|
||||
case "X":
|
||||
values.append(10)
|
||||
case "C":
|
||||
values.append(100)
|
||||
case "M":
|
||||
values.append(1000)
|
||||
|
||||
if len(values) > 1:
|
||||
return f"{''.join(filtered)} = {' + '.join(str(v) for v in values)} = {sum(values)}"
|
||||
else:
|
||||
return f"{''.join(filtered)} = {values[0]}"
|
||||
|
||||
class RomanNumeralConverter:
|
||||
def __init__(self):
|
||||
self.valid_numerals = "IVXCM"
|
||||
self.values = {"I": 1, "V": 5, "X": 10, "C": 100, "M": 1000}
|
||||
|
||||
def convert_to_number(self, numerals: str) -> str:
|
||||
filtered = self._filter_valid_numerals(numerals)
|
||||
if not filtered:
|
||||
return "ERROR: Invalid input"
|
||||
|
||||
values = self._convert_numerals_to_values(filtered)
|
||||
return self._format_output(filtered, values)
|
||||
|
||||
def _filter_valid_numerals(self, numerals: str) -> str:
|
||||
"""Extract only valid Roman numerals from input."""
|
||||
filtered_chars = [
|
||||
char for char in numerals.upper() if char in self.valid_numerals
|
||||
]
|
||||
return "".join(filtered_chars)
|
||||
|
||||
def _convert_numerals_to_values(self, numerals: str) -> list[int]:
|
||||
"""Convert Roman numeral characters to their integer values."""
|
||||
values = []
|
||||
for char in numerals:
|
||||
match char:
|
||||
case "I":
|
||||
values.append(1)
|
||||
case "V":
|
||||
values.append(5)
|
||||
case "X":
|
||||
values.append(10)
|
||||
case "C":
|
||||
values.append(100)
|
||||
case "M":
|
||||
values.append(1000)
|
||||
return values
|
||||
|
||||
def _format_output(self, numerals: str, values: list[int]) -> str:
|
||||
"""Format the conversion result for display."""
|
||||
if len(values) == 1:
|
||||
return f"{numerals} = {values[0]}"
|
||||
else:
|
||||
values_str = " + ".join(str(v) for v in values)
|
||||
return f"{numerals} = {values_str} = {sum(values)}"
|
||||
|
||||
|
||||
def main():
|
||||
converter = RomanNumeralConverter()
|
||||
while True:
|
||||
try:
|
||||
numerals = input("Please enter some Roman numerals: ")
|
||||
print(convert_to_number(numerals))
|
||||
print(converter.convert_to_number(numerals))
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,38 +1,88 @@
|
||||
import pytest
|
||||
from roman import convert_to_number
|
||||
from roman import RomanNumeralConverter
|
||||
|
||||
ERROR_MESSAGE = "ERROR: Invalid input"
|
||||
|
||||
|
||||
def test_valid_single_numerals():
|
||||
assert convert_to_number("I") == "I = 1"
|
||||
assert convert_to_number("V") == "V = 5"
|
||||
assert convert_to_number("X") == "X = 10"
|
||||
assert convert_to_number("C") == "C = 100"
|
||||
assert convert_to_number("M") == "M = 1000"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number("I") == "I = 1"
|
||||
assert converter.convert_to_number("V") == "V = 5"
|
||||
assert converter.convert_to_number("X") == "X = 10"
|
||||
assert converter.convert_to_number("C") == "C = 100"
|
||||
assert converter.convert_to_number("M") == "M = 1000"
|
||||
|
||||
|
||||
def test_valid_multiple_numerals():
|
||||
assert convert_to_number("IV") == "IV = 1 + 5 = 6"
|
||||
assert convert_to_number("XCM") == "XCM = 10 + 100 + 1000 = 1110"
|
||||
assert convert_to_number("MCMXCIV") == "MCMXCIV = 1000 + 100 + 1000 + 10 + 100 + 1 + 5 = 2216"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number("IV") == "IV = 1 + 5 = 6"
|
||||
assert converter.convert_to_number("XCM") == "XCM = 10 + 100 + 1000 = 1110"
|
||||
assert (
|
||||
converter.convert_to_number("MCMXCIV")
|
||||
== "MCMXCIV = 1000 + 100 + 1000 + 10 + 100 + 1 + 5 = 2216"
|
||||
)
|
||||
|
||||
|
||||
def test_lowercase_input():
|
||||
assert convert_to_number("ivxcm") == "IVXCM = 1 + 5 + 10 + 100 + 1000 = 1116"
|
||||
converter = RomanNumeralConverter()
|
||||
assert (
|
||||
converter.convert_to_number("ivxcm") == "IVXCM = 1 + 5 + 10 + 100 + 1000 = 1116"
|
||||
)
|
||||
|
||||
|
||||
def test_mixed_case_input():
|
||||
assert convert_to_number("iVxCm") == "IVXCM = 1 + 5 + 10 + 100 + 1000 = 1116"
|
||||
converter = RomanNumeralConverter()
|
||||
assert (
|
||||
converter.convert_to_number("iVxCm") == "IVXCM = 1 + 5 + 10 + 100 + 1000 = 1116"
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_characters():
|
||||
assert convert_to_number("EFGH") == "Invalid input"
|
||||
assert convert_to_number("123") == "Invalid input"
|
||||
assert convert_to_number("!@#") == "Invalid input"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number("EFGH") == ERROR_MESSAGE
|
||||
assert converter.convert_to_number("123") == ERROR_MESSAGE
|
||||
assert converter.convert_to_number("!@#") == ERROR_MESSAGE
|
||||
|
||||
|
||||
def test_mixed_valid_and_invalid_characters():
|
||||
assert convert_to_number("AIXB") == "IX = 1 + 10 = 11"
|
||||
assert convert_to_number("M1C2") == "MC = 1000 + 100 = 1100"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number("AIXB") == "IX = 1 + 10 = 11"
|
||||
assert converter.convert_to_number("M1C2") == "MC = 1000 + 100 = 1100"
|
||||
|
||||
|
||||
def test_empty_string():
|
||||
assert convert_to_number("") == "Invalid input"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number("") == ERROR_MESSAGE
|
||||
|
||||
|
||||
def test_only_spaces():
|
||||
assert convert_to_number(" ") == "Invalid input"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number(" ") == ERROR_MESSAGE
|
||||
|
||||
|
||||
def test_spaces_and_valid_numerals():
|
||||
assert convert_to_number(" I V ") == "IV = 1 + 5 = 6"
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter.convert_to_number(" I V ") == "IV = 1 + 5 = 6"
|
||||
|
||||
|
||||
def test_filter_valid_numerals():
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter._filter_valid_numerals("M1X2V") == "MXV"
|
||||
assert converter._filter_valid_numerals("def") == ""
|
||||
assert converter._filter_valid_numerals(" I V ") == "IV"
|
||||
|
||||
|
||||
def test_convert_numerals_to_values():
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter._convert_numerals_to_values("IV") == [1, 5]
|
||||
assert converter._convert_numerals_to_values("MXV") == [1000, 10, 5]
|
||||
assert converter._convert_numerals_to_values("C") == [100]
|
||||
|
||||
|
||||
def test_format_output():
|
||||
converter = RomanNumeralConverter()
|
||||
assert converter._format_output("I", [1]) == "I = 1"
|
||||
assert converter._format_output("IV", [1, 5]) == "IV = 1 + 5 = 6"
|
||||
assert (
|
||||
converter._format_output("MXV", [1000, 10, 5]) == "MXV = 1000 + 10 + 5 = 1015"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user