昨天我們成功建立了處理 1-10 的羅馬數字轉換器。今天我們要用 TDD 來擴展到 100!
開始 → 測試 11-19 → 處理 20-39 → 挑戰 40 (XL)
↓
完成 ← 模式總結 ← 處理 90-100 ← 引入 50 (L)
讓我們從 11 開始測試。建立 tests/day13/roman-numerals.test.ts
:
import { describe, it, expect } from 'vitest'
import { toRoman } from '../../src/roman/romanNumerals'
describe('Roman Numerals Extended', () => {
it('converts 11 to XI', () => {
expect(toRoman(11)).toBe('XI')
})
})
執行測試...紅燈!現在的實作無法處理 11。我們需要用迴圈來重複處理十位數。
問題在於我們需要用 while
迴圈來重複處理相同的符號:
// 處理 10 - 改用 while 重複處理
while (num >= 10) {
result += 'X'
num -= 10
}
現在測試通過了!讓我們繼續測試更多數字。
it('converts 20 to XX', () => {
expect(toRoman(20)).toBe('XX')
})
it('converts 27 to XXVII', () => {
expect(toRoman(27)).toBe('XXVII')
})
綠燈!因為我們用了 while
迴圈,20 會產生兩個 'X'。
羅馬數字中,40 不是 "XXXX",而是 "XL"(50-10)。這是另一個減法規則!
it('converts 40 to XL', () => {
expect(toRoman(40)).toBe('XL')
})
紅燈!輸出是 "XXXX"。我們需要加入 40 的映射。
這裡我們發現了一個重要原則:減法組合(如 XL)必須放在對應的基本符號(如 X)之前,否則會被錯誤地處理成多個重複符號。
it('converts 50 to L', () => {
expect(toRoman(50)).toBe('L')
})
it('converts 67 to LXVII', () => {
expect(toRoman(67)).toBe('LXVII')
})
我們需要加入 50(L)的處理。這代表了羅馬數字中的五十位數基礎單位。
90 在羅馬數字中是 "XC"(100-10),不是 "LXXXX":
it('converts 90 to XC', () => {
expect(toRoman(90)).toBe('XC')
})
it('converts 99 to XCIX', () => {
expect(toRoman(99)).toBe('XCIX')
})
it('converts 100 to C', () => {
expect(toRoman(100)).toBe('C')
})
我們發現了一個模式:羅馬數字轉換本質上是一個「查表」過程,我們總是選擇最大可能的符號:
export function toRoman(num: number): string {
const values = [
{ value: 100, symbol: 'C' },
{ value: 90, symbol: 'XC' },
{ value: 50, symbol: 'L' },
{ value: 40, symbol: 'XL' },
{ value: 10, symbol: 'X' },
{ value: 9, symbol: 'IX' },
{ value: 5, symbol: 'V' },
{ value: 4, symbol: 'IV' },
{ value: 1, symbol: 'I' }
]
let result = ''
for (const { value, symbol } of values) {
while (num >= value) {
result += symbol
num -= value
}
}
return result
}
這個重構版本更簡潔、更清晰!
讓我們加入完整的測試來驗證我們的實作:
describe('Roman Numerals Extended - Full Coverage', () => {
const testCases = [
{ input: 1, expected: 'I' },
{ input: 4, expected: 'IV' },
{ input: 9, expected: 'IX' },
{ input: 10, expected: 'X' },
{ input: 11, expected: 'XI' },
{ input: 20, expected: 'XX' },
{ input: 40, expected: 'XL' },
{ input: 44, expected: 'XLIV' },
{ input: 50, expected: 'L' },
{ input: 90, expected: 'XC' },
{ input: 99, expected: 'XCIX' },
{ input: 100, expected: 'C' }
]
testCases.forEach(({ input, expected }) => {
it(`converts ${input} to ${expected}`, () => {
expect(toRoman(input)).toBe(expected)
})
})
})
所有測試都通過!
完整實作 src/roman/romanNumerals.ts
:
export function toRoman(num: number): string {
const values = [
{ value: 100, symbol: 'C' },
{ value: 90, symbol: 'XC' },
{ value: 50, symbol: 'L' },
{ value: 40, symbol: 'XL' },
{ value: 10, symbol: 'X' },
{ value: 9, symbol: 'IX' },
{ value: 5, symbol: 'V' },
{ value: 4, symbol: 'IV' },
{ value: 1, symbol: 'I' }
]
let result = ''
for (const { value, symbol } of values) {
while (num >= value) {
result += symbol
num -= value
}
}
return result
}
完整測試 tests/day13/roman-numerals.test.ts
:
import { describe, it, expect } from 'vitest'
import { toRoman } from '../../src/roman/romanNumerals'
describe('Roman Numerals Extended', () => {
const testCases = [
{ input: 1, expected: 'I' },
{ input: 4, expected: 'IV' },
{ input: 9, expected: 'IX' },
{ input: 10, expected: 'X' },
{ input: 11, expected: 'XI' },
{ input: 20, expected: 'XX' },
{ input: 40, expected: 'XL' },
{ input: 44, expected: 'XLIV' },
{ input: 50, expected: 'L' },
{ input: 90, expected: 'XC' },
{ input: 99, expected: 'XCIX' },
{ input: 100, expected: 'C' }
]
testCases.forEach(({ input, expected }) => {
it(`converts ${input} to ${expected}`, () => {
expect(toRoman(input)).toBe(expected)
})
})
})
透過 TDD,我們發現了羅馬數字轉換的核心模式:
npm test tests/day13
所有測試都應該通過!我們現在有一個可以處理 1-100 所有數字的羅馬數字轉換器。
今天的 TDD 過程教會了我們:
在繼續之前,思考這些問題:
如果要處理更大的數字,我們需要:
今天我們學到了:
透過小步迭代,我們最終得到了優雅而高效的解決方案!每一個紅燈都指出了問題,每一個綠燈都確認了解決方案,而重構則讓我們的代碼越來越精練。