完成了六天的羅馬數字轉換器開發,今天我們要進行程式碼的最終整理與回顧。將散落在不同測試日期中的程式碼重構成一個乾淨、可維護、生產就緒的 Python 模組。
在過去 6 天的開發歷程中,我們從最簡單的測試 to_roman(1) → "I"
開始,逐步建立了完整的羅馬數字轉換器,包含基礎符號轉換、擴展範圍支援、雙向轉換功能,以及效能優化機制。
將六天開發的程式碼重構為一個統一、乾淨的模組:
# 建立 src/roman/converter.py
"""Roman Numeral Converter - Production-ready module"""
from typing import Dict, List, Tuple
from functools import lru_cache
# 基本符號對應表
ROMAN_MAPPINGS: List[Tuple[int, str]] = [
(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')
]
ROMAN_VALUES: Dict[str, int] = {
'M': 1000, 'CM': 900, 'D': 500, 'CD': 400,
'C': 100, 'XC': 90, 'L': 50, 'XL': 40,
'X': 10, 'IX': 9, 'V': 5, 'IV': 4, 'I': 1
}
VALID_ROMAN_CHARS = {'M', 'D', 'C', 'L', 'X', 'V', 'I'}
def to_roman(number: int) -> str:
"""將阿拉伯數字轉換為羅馬數字"""
if not isinstance(number, int):
raise TypeError(f"Input must be an integer, got {type(number).__name__}")
if number <= 0 or number > 3999:
raise ValueError(f"Number must be between 1 and 3999, got {number}")
result = []
remaining = number
for value, symbol in ROMAN_MAPPINGS:
count, remaining = divmod(remaining, value)
if count > 0:
result.append(symbol * count)
return ''.join(result)
def from_roman(roman: str) -> int:
"""將羅馬數字轉換為阿拉伯數字"""
if not isinstance(roman, str):
raise TypeError(f"Input must be a string, got {type(roman).__name__}")
if not roman.strip():
raise ValueError("Roman numeral cannot be empty")
normalized_roman = roman.strip().upper()
for char in normalized_roman:
if char not in VALID_ROMAN_CHARS:
raise ValueError(f"Invalid Roman character: '{char}'")
result = 0
i = 0
while i < len(normalized_roman):
if i + 1 < len(normalized_roman):
two_char = normalized_roman[i:i+2]
if two_char in ROMAN_VALUES:
result += ROMAN_VALUES[two_char]
i += 2
continue
one_char = normalized_roman[i]
if one_char in ROMAN_VALUES:
result += ROMAN_VALUES[one_char]
i += 1
else:
raise ValueError(f"Invalid Roman numeral sequence at position {i}")
return result
def is_valid_roman(roman: str) -> bool:
"""驗證羅馬數字格式是否正確"""
try:
number = from_roman(roman)
return to_roman(number) == roman.strip().upper()
except (ValueError, TypeError):
return False
@lru_cache(maxsize=4000)
def to_roman_cached(number: int) -> str:
"""快取版本的羅馬數字轉換器"""
return to_roman(number)
@lru_cache(maxsize=4000)
def from_roman_cached(roman: str) -> int:
"""快取版本的羅馬數字解析器"""
return from_roman(roman)
整合測試涵蓋所有核心功能:
# 建立 tests/day17/test_roman_converter.py
import pytest
from src.roman.converter import to_roman, from_roman, to_roman_cached
def test_complete_conversion_workflow():
"""測試完整的雙向轉換流程"""
test_cases = [
(1, 'I'),
(4, 'IV'),
(1994, 'MCMXCIV'),
(3999, 'MMMCMXCIX')
]
for number, expected in test_cases:
# 測試數字轉羅馬
assert to_roman(number) == expected
# 測試羅馬轉數字
assert from_roman(expected) == number
def test_invalid_input_handling():
"""測試無效輸入的錯誤處理"""
# 測試無效數字範圍
with pytest.raises(ValueError):
to_roman(0)
# 測試空字串輸入
with pytest.raises(ValueError):
from_roman('')
def test_cached_performance():
"""測試快取版本的功能正確性"""
# 確認快取版本結果與原版一致
assert to_roman_cached(1994) == to_roman(1994)
assert to_roman_cached(3999) == 'MMMCMXCIX'
透過六天的 Roman Numeral Kata,我們累積了哪些寶貴經驗?
紅-綠-重構 循環讓我們始終保持程式碼有測試覆蓋,每次只專注解決一個問題,透過重構持續改善設計,建立可靠的安全網。
從 to_roman(1) → "I"
開始,我們學會了先讓最簡單的測試通過,逐步增加複雜性,每次變更都有測試保護。
在 Python 實作中,我們特別運用了類型提示、文檔字串、functools.lru_cache 內建快取裝飾器,以及 pytest 簡潔的函數式測試風格。
to_roman
)from_roman
)is_valid_roman
)to_roman_cached
, from_roman_cached
)# 執行所有Day 17測試
pytest tests/day17/ -v
# 產生測試覆蓋率報告
pytest tests/day17/ --cov=src.roman --cov-report=term
# 檢查程式碼品質
black src/roman/ && isort src/roman/ && mypy src/roman/
Day 11-16的TDD實踐進程:
to_roman(1) == "I"
from_roman
雙向轉換功能def test_basic_roman_symbols():
"""測試基本羅馬數字符號"""
assert to_roman(1) == "I"
assert to_roman(5) == "V"
assert to_roman(10) == "X"
@pytest.mark.parametrize("number,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(44, "XLIV"),
(90, "XC"),
(400, "CD"),
(900, "CM")
])
def test_parameterized_conversion(number, expected):
"""使用參數化測試驗證多個案例"""
assert to_roman(number) == expected
透過六天的實作,我們的測試策略也在不斷進化:
## Day 11 - 簡單的單一測試
def test_converts_1_to_I():
assert to_roman(1) == 'I'
## 組織化的測試套件
class TestRomanConverter:
def test_basic_symbols(self):
"""測試基本符號轉換"""
pass
def test_subtraction_cases(self):
"""測試減法規則"""
pass
def test_error_handling(self):
"""測試錯誤處理"""
pass
通過 Roman Numeral Kata 的完整實作,我們深度體驗了 TDD 的精髓:
讓我們回顧這 17 天的學習之旅:
Roman Numeral Kata 的完成標誌著我們在 Python TDD 領域達成了重要里程碑!
每一個測試都是對品質的承諾,每一次重構都是對卓越的追求。讓我們帶著這些收穫,繼續前進!