完成了六天的羅馬數字轉換器開發,今天我們要進行程式碼的最終整理與回顧。將散落在不同測試日期中的程式碼重構成一個乾淨、可維護、生產就緒的模組。
在過去 6 天的開發歷程中,我們從最簡單的測試 toRoman(1) → "I"
開始,逐步建立了完整的羅馬數字轉換器。
建立統一的模組實作:
// 建立 src/roman/index.ts
export interface RomanConverter {
toRoman: (num: number) => string
fromRoman: (roman: string) => number
}
// 基本符號對應表
const ROMAN_MAPPINGS: ReadonlyArray<readonly [number, string]> = [
[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']
] as const
const ROMAN_VALUES: Record<string, number> = {
'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
} as const
export function toRoman(num: number): string {
if (!Number.isInteger(num) || num <= 0 || num > 3999) {
throw new Error('Number must be an integer between 1 and 3999')
}
let result = ''
let remaining = num
for (const [value, symbol] of ROMAN_MAPPINGS) {
const count = Math.floor(remaining / value)
if (count > 0) {
result += symbol.repeat(count)
remaining -= value * count
}
}
return result
}
export function fromRoman(roman: string): number {
if (!roman || typeof roman !== 'string') {
throw new Error('Invalid Roman numeral: empty or non-string input')
}
const normalizedRoman = roman.trim().toUpperCase()
const validChars = new Set(['I', 'V', 'X', 'L', 'C', 'D', 'M'])
for (const char of normalizedRoman) {
if (!validChars.has(char)) {
throw new Error(`Invalid Roman character: '${char}'`)
}
}
let result = 0
let i = 0
while (i < normalizedRoman.length) {
const twoChar = normalizedRoman.substring(i, i + 2)
const oneChar = normalizedRoman[i]
if (ROMAN_VALUES[twoChar]) {
result += ROMAN_VALUES[twoChar]
i += 2
} else if (ROMAN_VALUES[oneChar]) {
result += ROMAN_VALUES[oneChar]
i += 1
}
}
return result
}
export function isValidRoman(roman: string): boolean {
try {
const num = fromRoman(roman)
return toRoman(num) === roman.trim().toUpperCase()
} catch {
return false
}
}
const romanConverter: RomanConverter = { toRoman, fromRoman }
export default romanConverter
建立完整的測試套件:
// 建立 tests/day17/roman-converter.test.ts
import { describe, test, expect } from 'vitest'
import { toRoman, fromRoman, isValidRoman } from '../../src/roman/index'
describe('Roman Converter - Production Tests', () => {
test('converts basic numbers correctly', () => {
expect(toRoman(1)).toBe('I')
expect(toRoman(4)).toBe('IV')
expect(toRoman(1994)).toBe('MCMXCIV')
})
test('converts Roman numerals correctly', () => {
expect(fromRoman('I')).toBe(1)
expect(fromRoman('IV')).toBe(4)
expect(fromRoman('MCMXCIV')).toBe(1994)
})
test('bidirectional conversion consistency', () => {
const testNumbers = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994]
for (const num of testNumbers) {
const roman = toRoman(num)
expect(fromRoman(roman)).toBe(num)
}
})
test('validates Roman numerals', () => {
expect(isValidRoman('IV')).toBe(true)
expect(isValidRoman('IIII')).toBe(false)
})
test('handles error cases', () => {
expect(() => toRoman(0)).toThrow()
expect(() => fromRoman('ABC')).toThrow()
})
})
透過六天的 Roman Numeral Kata,我們學到了什麼?
紅-綠-重構 循環讓我們始終保持程式碼有測試覆蓋,每次只專注解決一個問題,透過重構持續改善設計,建立可靠的安全網。
從 toRoman(1) → "I"
開始,我們學會了先讓最簡單的測試通過,逐步增加複雜性,每次變更都有測試保護。
有了測試作為安全網,我們能夠放心改善程式碼結構,優化效能而不破壞功能,重新組織模組架構。
toRoman
)fromRoman
)isValidRoman
)npm test tests/day17/
npm test tests/day17/ --coverage
npx tsc --noEmit
npx eslint src/roman/
在開發過程中的常見問題:
// 類型錯誤處理
const num = parseInt(input, 10)
if (!isNaN(num)) toRoman(num)
// 邊界值測試
expect(() => toRoman(0)).toThrow('between 1 and 3999')
從Day 11到Day 16的TDD實踐:
toRoman(1) → "I"
fromRoman
雙向轉換功能經過六天實作,我們深刻理解:
測試優先的設計理念:
interface RomanConverter {
toRoman: (num: number) => string
fromRoman: (roman: string) => number
}
從最簡單開始:單一數字→重複數字→減法規則→完整範圍→雙向轉換→效能優化。每一步都有測試保護。
type RomanNumeral = string & { __brand: 'roman' }
import { describe, test, expect } from 'vitest'
透過六天的實作,我們的測試策略也在不斷進化:
// Day 11 - 簡單的單一測試
test('converts 1 to I', () => {
expect(toRoman(1)).toBe('I')
})
// Day 17 - 組織化的測試套件
describe('Roman Converter', () => {
describe('toRoman', () => {
test('handles single digits', () => { /* ... */ })
test('handles subtraction cases', () => { /* ... */ })
})
})
讓我們回顧這 17 天的學習之旅:
完成 Roman Numeral Kata 是一個重要的里程碑!我們不僅學會了 TDD 的核心技術,更建立了以測試驅動開發的思維模式。
每一個測試都是對品質的承諾,每一次重構都是對卓越的追求。讓我們帶著這些收穫,繼續前進!