想像一下,你正在開發一個歷史文件數位化系統,需要處理從古羅馬到現代的所有年份標記。今天我們要用 TDD 完成最後一塊拼圖,讓轉換器能夠處理完整的羅馬數字範圍:1-3999!
今天我們將專注於:
昨天的映射表設計展現了強大擴展能力,今天只需加入四個新符號就能擴展到 1-3999!
羅馬數字傳統上止於 3999 的原因:
要處理 1-3999,需要加入:
建立 tests/day14/test_roman_complete.py
:
from src.roman.converter import to_roman
def test_convert_400():
assert to_roman(400) == "CD"
紅燈 🔴!更新 src/roman/converter.py
:
def to_roman(number: int) -> str:
if number <= 0:
raise ValueError("Number must be positive")
mappings = [
(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")
]
result = ""
for value, symbol in mappings:
while number >= value:
result += symbol
number -= value
return result
繼續新增其他符號的測試:
def test_convert_500():
assert to_roman(500) == "D"
def test_convert_900():
assert to_roman(900) == "CM"
def test_convert_1000():
assert to_roman(1000) == "M"
綠燈 🟢!一次性加入所有符號讓相關測試都通過了。
現在測試一些重要的里程碑數字,驗證我們的實作在複雜組合下的正確性:
def test_convert_1994():
"""測試歷史年份 1994
1994 = M(1000) + CM(900) + XC(90) + IV(4) = MCMXCIV
這是一個包含多種減法組合的複雜例子
"""
assert to_roman(1994) == "MCMXCIV"
def test_convert_2024():
"""測試現代年份 2024
2024 = MM(2000) + XX(20) + IV(4) = MMXXIV
測試現代年份的轉換
"""
assert to_roman(2024) == "MMXXIV"
def test_convert_3999():
"""測試最大值 3999
3999 = MMM(3000) + CM(900) + XC(90) + IX(9) = MMMCMXCIX
這是羅馬數字系統的理論上限
"""
assert to_roman(3999) == "MMMCMXCIX"
def test_convert_edge_cases_with_all_subtraction_rules():
"""測試包含所有減法規則的數字"""
assert to_roman(3949) == "MMMCMXLIX" # 3000 + 900 + 40 + 9
assert to_roman(1444) == "MCDXLIV" # 1000 + 400 + 40 + 4
assert to_roman(2999) == "MMCMXCIX" # 2000 + 900 + 90 + 9
執行測試,綠燈 🟢!我們的映射表方法成功處理了所有複雜組合。
讓我們手動追蹤 1994 的轉換過程:
這個過程展示了貪婪算法在羅馬數字轉換中的完美應用。
羅馬數字使用七個基本符號,展現 1-5-10 的週期性模式:
符號 | 數值 | 類型 | 用途 |
---|---|---|---|
I | 1 | 基數 | 個位數表示 |
V | 5 | 半位 | 五的表示 |
X | 10 | 基數 | 十位數表示 |
L | 50 | 半位 | 五十的表示 |
C | 100 | 基數 | 百位數表示 |
D | 500 | 半位 | 五百的表示 |
M | 1000 | 基數 | 千位數表示 |
減法規則只有 I、X、C 可用於減法,形成六種特殊組合:
我們的實作達到 O(1) 時間複雜度 和 O(1) 空間複雜度:
貪婪算法的三個正確性保證:
用 TDD 添加錯誤處理:
import pytest
from src.roman.converter import to_roman
def test_invalid_input_zero():
"""測試零的輸入"""
with pytest.raises(ValueError, match="Number must be positive"):
to_roman(0)
def test_invalid_input_negative():
"""測試負數輸入"""
with pytest.raises(ValueError, match="Number must be positive"):
to_roman(-1)
def test_invalid_input_too_large():
"""測試超出範圍的輸入"""
with pytest.raises(ValueError, match="Number must be <= 3999"):
to_roman(4000)
實作更新:
def to_roman(number: int) -> str:
if number <= 0:
raise ValueError("Number must be positive")
if number > 3999:
raise ValueError("Number must be <= 3999")
# ... 現有邏輯
當測試案例增多時,善用 pytest 的功能來組織測試。效能優化上,可以重複使用轉換器實例、使用參數化測試,以及利用 pytest 的平行測試執行功能。
import pytest
@pytest.mark.parametrize("input_num,expected", [
(1994, "MCMXCIV"), (2024, "MMXXIV"), (3999, "MMMCMXCIX"),
(444, "CDXLIV"), (888, "DCCCLXXXVIII"),
(1, "I"), (999, "CMXCIX"), (1001, "MI"),
])
def test_convert_various_numbers(input_num, expected):
assert to_roman(input_num) == expected
試試看這些特殊數字組合:
# 建立 tests/day14/test_special_cases.py
def test_repeating_symbols():
assert to_roman(3333) == "MMMCCCXXXIII" # 各位都是3
assert to_roman(2222) == "MMCCXXII" # 各位都是2
def test_all_subtraction_in_one():
assert to_roman(3949) == "MMMCMXLIX" # CM, XL, IX
assert to_roman(1494) == "MCDXCIV" # CD, XC, IV
透過 Roman Numeral Kata,我們掌握了 pytest 的核心功能:
assert
語句的簡潔威力pytest.raises
處理錯誤情況@pytest.mark.parametrize
減少重複完整實作 src/roman/converter.py
:
def to_roman(number: int) -> str:
if number <= 0:
raise ValueError("Number must be positive")
if number > 3999:
raise ValueError("Number must be <= 3999")
mappings = [
(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")
]
result = ""
for value, symbol in mappings:
while number >= value:
result += symbol
number -= value
return result
完整測試 tests/day14/test_roman_complete.py
:
import pytest
from src.roman.converter import to_roman
def test_convert_400():
assert to_roman(400) == "CD"
def test_convert_500():
assert to_roman(500) == "D"
def test_convert_900():
assert to_roman(900) == "CM"
def test_convert_1000():
assert to_roman(1000) == "M"
def test_convert_1994():
assert to_roman(1994) == "MCMXCIV"
def test_convert_2024():
assert to_roman(2024) == "MMXXIV"
def test_convert_3999():
assert to_roman(3999) == "MMMCMXCIX"
def test_convert_edge_cases_with_all_subtraction_rules():
assert to_roman(3949) == "MMMCMXLIX"
assert to_roman(1444) == "MCDXLIV"
assert to_roman(2999) == "MMCMXCIX"
def test_invalid_input_zero():
with pytest.raises(ValueError, match="Number must be positive"):
to_roman(0)
def test_invalid_input_negative():
with pytest.raises(ValueError, match="Number must be positive"):
to_roman(-1)
def test_invalid_input_too_large():
with pytest.raises(ValueError, match="Number must be <= 3999"):
to_roman(4000)
@pytest.mark.parametrize("input_num,expected", [
(1, "I"), (4, "IV"), (5, "V"), (9, "IX"), (10, "X"),
(40, "XL"), (50, "L"), (90, "XC"), (100, "C"),
(400, "CD"), (500, "D"), (900, "CM"), (1000, "M"),
])
def test_all_special_numbers(input_num, expected):
assert to_roman(input_num) == expected
技術成就:完整羅馬數字系統(1-3999)、pytest 框架熟練、O(1) 高效實作
開發素養:TDD 紅綠重構、漸進式開發、測試組織、邊界處理
恭喜你完成了 Roman Numeral Kata!這個經典練習為我們的 Python TDD 之旅奠定了堅實基礎。明天我們將開始探索新的挑戰!🚀