improve docs, black formatting

This commit is contained in:
2025-08-31 11:34:20 +01:00
parent 6488ea0ede
commit df11a33a54
10 changed files with 642 additions and 159 deletions

86
roman/README.md Normal file
View 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+)

View File

@@ -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()

View File

@@ -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"
)