昨天我們打下了羅馬數字轉換的基礎,看到了簡單數字的處理規則。但現實世界需要更大的數字!今天我們要讓程式碼「長大」,挑戰 100 以內的所有數字。準備好了嗎?這趟旅程會讓你見識到模式的力量!
開始 → 測試 11-19 → 處理 20-39 → 挑戰 40 (XL)
↓
完成 ← 模式總結 ← 處理 90-100 ← 引入 50 (L)
讓我們從 11 開始,看看昨天的程式碼能走多遠:
建立 tests/day13/test_roman_extended_basic.py
from src.roman.converter import to_roman
def test_convert_11():
assert to_roman(11) == "XI"
執行測試,測試通過了!太棒了,我們的 while
迴圈邏輯已經能夠正確處理重複的符號。11 = 10 + 1 = X + I = XI。
測試更多數字如 15(XV)、19(XIX)也都通過了!
測試 20(XX)和 33(XXXIII)都通過了!迴圈邏輯完美處理了重複符號。
這裡有個有趣的問題:羅馬數字不喜歡四個相同的符號連續出現。40 不是 XXXX,而是 XL(50-10)。這就是羅馬數字的「減法規則」!
# tests/day13/test_roman_forties.py
from src.roman.converter import to_roman
def test_convert_40():
assert to_roman(40) == "XL"
執行測試會失敗,因為目前實作會產生 "XXXX"。
錯誤訊息:
AssertionError: assert 'XXXX' == 'XL'
我們需要在映射表中加入 40 的規則:
# 更新 src/roman/converter.py
def to_roman(number: int) -> str:
if number <= 0:
raise ValueError("Number must be positive")
# 羅馬數字映射表(按數值大小降序排列)
mappings = [
(40, "XL"), # 新增 40 的映射
(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
執行測試,現在 40 正確轉換成 "XL" 了!注意映射表的順序很重要,必須把 40 放在 10 之前,這樣才會優先匹配。
羅馬數字中,50 有自己的專屬符號 "L"。讓我們加入這個新朋友:
# tests/day13/test_roman_fifties.py
from src.roman.converter import to_roman
def test_convert_50():
assert to_roman(50) == "L"
失敗了!目前會產生 "XXXXX"(五個 X)。
# 更新 src/roman/converter.py
def to_roman(number: int) -> str:
if number <= 0:
raise ValueError("Number must be positive")
mappings = [
(50, "L"), # 新增 50 的映射
(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
測試 51(LI)、55(LV)、59(LIX)等數字都能正確轉換。
就像 40 用 XL 表示,90 也有特殊的寫法:XC(100-10)。這展現了羅馬數字的優雅對稱性。
def test_convert_90():
assert to_roman(90) == "XC"
測試失敗,目前會產生 "LXXXX"(L + 四個 X)。
# 更新 src/roman/converter.py
mappings = [
(90, "XC"), # 新增 90 的映射
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I")
]
測試 91(XCI)、95(XCV)、99(XCIX)都通過了,99 是 100 以內最複雜的組合!
我們來到了今天的最終目標:100!羅馬數字用 "C" 表示(來自拉丁文 centum)。
def test_convert_100():
assert to_roman(100) == "C"
mappings = [
(100, "C"), # 新增 100 的映射
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I")
]
通過今天的擴展,我們可以看出羅馬數字的模式:
每個十進位級別都有類似的結構:
讓我們建立一個參數化測試,驗證所有關鍵數字:
建立 tests/day13/test_roman_complete.py
import pytest
from src.roman.converter import to_roman
@pytest.mark.parametrize("number,expected", [
# 基本測試
(1, "I"), (5, "V"), (10, "X"),
# 減法規則
(4, "IV"), (9, "IX"), (40, "XL"), (90, "XC"),
# 新符號
(50, "L"), (100, "C"),
# 複雜組合
(11, "XI"), (19, "XIX"), (33, "XXXIII"),
(44, "XLIV"), (55, "LV"), (66, "LXVI"),
(77, "LXXVII"), (88, "LXXXVIII"), (99, "XCIX")
])
def test_convert_comprehensive(number, expected):
assert to_roman(number) == expected
執行完整測試套件... 全部通過!
我們的實作採用「貪婪演算法」:
這就像換零錢:你總是先用大鈔,再用小鈔!
今天的開發過程完美展現了 TDD 的威力:
試著自己實作這些擴展,感受 TDD 的節奏感。
# 完整實作 src/roman/converter.py
def to_roman(number: int) -> str:
"""
將阿拉伯數字轉換為羅馬數字 (1-100)
Args:
number: 要轉換的正整數 (1-100)
Returns:
對應的羅馬數字字串
Examples:
>>> to_roman(40)
'XL'
>>> to_roman(99)
'XCIX'
"""
if number <= 0:
raise ValueError("Number must be positive")
# 羅馬數字映射表(按數值大小降序排列)
# 包含基本符號和減法組合
mappings = [
(100, "C"), # 百
(90, "XC"), # 九十 (100-10)
(50, "L"), # 五十
(40, "XL"), # 四十 (50-10)
(10, "X"), # 十
(9, "IX"), # 九 (10-1)
(5, "V"), # 五
(4, "IV"), # 四 (5-1)
(1, "I") # 一
]
result = ""
# 貪婪演算法:總是選擇最大可能的符號
for value, symbol in mappings:
while number >= value:
result += symbol
number -= value
return result
今天我們成功擴展了羅馬數字轉換器,能夠處理 1-100 的完整範圍。我們學習了:
TDD 為我們提供了安全網,讓我們能夠放心地進行持續改進!