iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Python pytest TDD 實戰:從零開始的測試驅動開發系列 第 13

Day 13 - 擴展到百位數(11-100) 💯

  • 分享至 

  • xImage
  •  

昨天我們打下了羅馬數字轉換的基礎,看到了簡單數字的處理規則。但現實世界需要更大的數字!今天我們要讓程式碼「長大」,挑戰 100 以內的所有數字。準備好了嗎?這趟旅程會讓你見識到模式的力量!

本日學習地圖 🗺️

開始 → 測試 11-19 → 處理 20-39 → 挑戰 40 (XL)
  ↓
完成 ← 模式總結 ← 處理 90-100 ← 引入 50 (L)

第一步:測試 11-19 的邊界

讓我們從 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:雙倍的 X

測試 20(XX)和 33(XXXIII)都通過了!迴圈邏輯完美處理了重複符號。

關鍵挑戰:數字 40 的減法規則

這裡有個有趣的問題:羅馬數字不喜歡四個相同的符號連續出現。40 不是 XXXX,而是 XL(50-10)。這就是羅馬數字的「減法規則」!

紅燈:為數字 40 寫測試

# 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 符號

羅馬數字中,50 有自己的專屬符號 "L"。讓我們加入這個新朋友:

紅燈:測試 50

# tests/day13/test_roman_fifties.py
from src.roman.converter import to_roman

def test_convert_50():
    assert to_roman(50) == "L"

失敗了!目前會產生 "XXXXX"(五個 X)。

綠燈:加入 L 的映射

# 更新 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)等數字都能正確轉換。

數字 90:另一個減法規則

就像 40 用 XL 表示,90 也有特殊的寫法:XC(100-10)。這展現了羅馬數字的優雅對稱性。

紅燈:測試 90

def test_convert_90():
    assert to_roman(90) == "XC"

測試失敗,目前會產生 "LXXXX"(L + 四個 X)。

綠燈:加入 XC 映射

# 更新 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 符號

我們來到了今天的最終目標:100!羅馬數字用 "C" 表示(來自拉丁文 centum)。

紅燈:測試 100

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

模式識別:羅馬數字的優雅結構

通過今天的擴展,我們可以看出羅馬數字的模式:

基本符號

  • I = 1, V = 5, X = 10, L = 50, C = 100

減法組合

  • IV = 4 (5-1), IX = 9 (10-1)
  • XL = 40 (50-10), XC = 90 (100-10)

模式觀察

每個十進位級別都有類似的結構:

  • 個位:1(I), 4(IV), 5(V), 9(IX)
  • 十位:10(X), 40(XL), 50(L), 90(XC)
  • 百位:100(C)

全面測試:建立完整測試套件

讓我們建立一個參數化測試,驗證所有關鍵數字:

建立 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

執行完整測試套件... 全部通過!

演算法分析:貪婪的智慧

我們的實作採用「貪婪演算法」:

  1. 總是選擇最大可能的符號
  2. 映射表降序排列是關鍵
  3. 減法規則優先於基本符號

這就像換零錢:你總是先用大鈔,再用小鈔!

TDD 的價值體現

今天的開發過程完美展現了 TDD 的威力:

  1. 漸進式開發:從 11 到 100,一步步擴展
  2. 及早發現問題:40 和 90 的減法規則
  3. 重構信心:每次改動都有測試保護
  4. 清晰的需求:每個測試都是一個明確的規格

跟著做

試著自己實作這些擴展,感受 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 的完整範圍。我們學習了:

  1. 模式識別:羅馬數字的重複結構
  2. 貪婪演算法:選擇最大可能符號的策略
  3. 減法規則:IV、IX、XL、XC 的處理
  4. 測試驅動:用 TDD 來驗證和擴展功能

思考練習

  1. 為什麼映射表要降序排列? 如果改成升序會怎樣?
  2. 減法規則的模式是什麼? 為什麼是 4、9、40、90?
  3. 小挑戰:試著寫一個反向函數!

TDD 為我們提供了安全網,讓我們能夠放心地進行持續改進!


上一篇
Day 12 - 基礎符號轉換(1-10) 🔢
下一篇
Day 14 - 完整範圍實作(1-3999) 🏛️
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言