經過前幾天的 Roman Numeral Kata 練習,我們已經掌握了將阿拉伯數字轉換為羅馬數字的技巧。今天我們要挑戰反向轉換:將羅馬數字解析回阿拉伯數字 ✨
這個挑戰更加有趣,因為我們需要理解羅馬數字的減法規則,並建立一個穩健的解析器 🎯
在過去幾天的學習中,我們已經完成了:
今天要實作反向轉換,讓我們的轉換器具備雙向功能!
from_roman()
函數在開始實作前,讓我們回顧羅馬數字的解析規則:
特殊減法組合:
反向轉換需要處理:
讓我們用 TDD 的方式來實作這個解析器。
建立 tests/day15/test_from_roman_basic.py:
from src.roman.converter import from_roman
def test_single_symbol():
assert from_roman("I") == 1
assert from_roman("V") == 5
assert from_roman("X") == 10
建立 src/roman/converter.py 的 from_roman 函數:
def from_roman(roman):
if roman == "I":
return 1
elif roman == "V":
return 5
elif roman == "X":
return 10
執行測試:
pytest tests/day15/test_from_roman_basic.py -v
執行測試:
pytest tests/day15/test_from_roman_basic.py -v
測試通過!✅
更新 src/roman/converter.py:
def from_roman(roman):
symbol_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
if len(roman) == 1:
return symbol_values[roman]
建立 tests/day15/test_from_roman_addition.py:
from src.roman.converter import from_roman
def test_multiple_symbols_addition():
assert from_roman("II") == 2
assert from_roman("III") == 3
assert from_roman("VI") == 6
assert from_roman("VII") == 7
assert from_roman("VIII") == 8
更新實作,處理多個符號:
def from_roman(roman):
symbol_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
result = 0
for char in roman:
result += symbol_values[char]
return result
執行測試:
pytest tests/day15/test_from_roman_addition.py -v
測試通過!✅ 基本的相加情況已經處理完成。
建立 tests/day15/test_from_roman_subtraction.py:
from src.roman.converter import from_roman
def test_subtraction_rules():
assert from_roman("IV") == 4
assert from_roman("IX") == 9
assert from_roman("XL") == 40
assert from_roman("XC") == 90
assert from_roman("CD") == 400
assert from_roman("CM") == 900
現在我們的實作會失敗,因為還沒處理減法規則。讓我們更新實作:
def from_roman(roman):
symbol_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
result = 0
i = 0
while i < len(roman):
# 檢查是否需要減法
if i + 1 < len(roman):
current_value = symbol_values[roman[i]]
next_value = symbol_values[roman[i + 1]]
if current_value < next_value:
# 減法情況
result += next_value - current_value
i += 2 # 跳過兩個字符
continue
# 正常相加
result += symbol_values[roman[i]]
i += 1
return result
執行測試確認實作正確:
pytest tests/day15/test_from_roman_subtraction.py -v
測試通過!✅
建立 tests/day15/test_from_roman_complex.py:
from src.roman.converter import from_roman
def test_complex_combinations():
assert from_roman("XIV") == 14 # 10 + (5 - 1)
assert from_roman("XIX") == 19 # 10 + (10 - 1)
assert from_roman("XLIV") == 44 # (50 - 10) + (5 - 1)
assert from_roman("XCIV") == 94 # (100 - 10) + (5 - 1)
assert from_roman("CDXLIV") == 444 # (500 - 100) + (50 - 10) + (5 - 1)
執行測試:
pytest tests/day15/test_from_roman_complex.py -v
我們的實作應該已經能處理這些複雜情況了!✅
建立 tests/day15/test_from_roman_validation.py:
import pytest
from src.roman.converter import from_roman
def test_classic_examples():
assert from_roman("MCMXCIV") == 1994
assert from_roman("MMMCMXCIX") == 3999
def test_input_validation():
with pytest.raises(ValueError, match="Empty roman numeral"):
from_roman("")
with pytest.raises(ValueError, match="Invalid character"):
from_roman("ABC")
with pytest.raises(ValueError, match="Invalid subtraction"):
from_roman("IC") # I 不能在 C 前面
更新實作加入驗證:
def from_roman(roman):
if not roman:
raise ValueError("Empty roman numeral")
symbol_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
# 驗證字符
for char in roman:
if char not in symbol_values:
raise ValueError(f"Invalid character: {char}")
# 定義合法的減法組合
valid_subtractions = {'IV', 'IX', 'XL', 'XC', 'CD', 'CM'}
result = 0
i = 0
while i < len(roman):
if i + 1 < len(roman):
current_value = symbol_values[roman[i]]
next_value = symbol_values[roman[i + 1]]
if current_value < next_value:
# 檢查是否為合法的減法
subtraction = roman[i:i+2]
if subtraction not in valid_subtractions:
raise ValueError(f"Invalid subtraction: {subtraction}")
result += next_value - current_value
i += 2
continue
result += symbol_values[roman[i]]
i += 1
return result
執行驗證測試:
pytest tests/day15/test_from_roman_validation.py -v
最後,我們要確保 to_roman
和 from_roman
可以完美配合:
建立 tests/day15/test_roman_bidirectional.py:
from src.roman.converter import to_roman, from_roman
def test_bidirectional_conversion():
"""測試雙向轉換的一致性"""
test_numbers = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994]
for number in test_numbers:
roman = to_roman(number)
converted_back = from_roman(roman)
assert converted_back == number
執行所有測試確保實作正確:
# 執行 Day 15 專門測試
pytest tests/day15/ -v
# 執行覆蓋率測試
pytest tests/day15/ --cov=src.roman --cov-report=term-missing
預期輸出:
=================== test session starts ===================
tests/day15/test_from_roman_basic.py::test_single_symbol PASSED
tests/day15/test_from_roman_addition.py::test_multiple_symbols_addition PASSED
tests/day15/test_from_roman_subtraction.py::test_subtraction_rules PASSED
tests/day15/test_from_roman_complex.py::test_complex_combinations PASSED
tests/day15/test_from_roman_validation.py::test_classic_examples PASSED
tests/day15/test_from_roman_validation.py::test_input_validation PASSED
tests/day15/test_roman_bidirectional.py::test_bidirectional_conversion PASSED
=================== 7 passed in 0.05s ===================
Name Stmts Miss Cover Missing
------------------------------------------------------
src/roman/__init__.py 0 0 100%
src/roman/converter.py 45 0 100%
------------------------------------------------------
TOTAL 45 0 100%
我們的實作具有良好的性能:
完整實作 src/roman/converter.py:
def from_roman(roman: str) -> int:
"""將羅馬數字轉換為阿拉伯數字"""
if not roman:
raise ValueError("Empty roman numeral")
symbol_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
valid_subtractions = {'IV', 'IX', 'XL', 'XC', 'CD', 'CM'}
# 驗證字符
for char in roman:
if char not in symbol_values:
raise ValueError(f"Invalid character: {char}")
result = 0
i = 0
while i < len(roman):
if i + 1 < len(roman):
current_value = symbol_values[roman[i]]
next_value = symbol_values[roman[i + 1]]
if current_value < next_value:
# 檢查是否為合法的減法
subtraction = roman[i:i+2]
if subtraction not in valid_subtractions:
raise ValueError(f"Invalid subtraction: {subtraction}")
result += next_value - current_value
i += 2
continue
result += symbol_values[roman[i]]
i += 1
return result
今天我們完成了羅馬數字的反向轉換功能:
明天我們將繼續完善羅馬數字轉換器,加入更多的測試案例和功能增強,確保我們的實作能夠處理各種邊界情況和實際應用需求。
透過 TDD 的紅綠重構循環,我們成功建立了一個穩健的雙向轉換系統,這種方法確保了程式碼的正確性和可維護性 🚀