iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

React TDD 實戰:用 Vitest 打造可靠的前端應用系列 第 14

Day 14 - 完整範圍實作(1-3999) 🏛️

  • 分享至 

  • xImage
  •  

想像一下,你正在開發一個歷史教育網站,需要將年份轉換成羅馬數字。昨天的查表策略展現了強大的可擴展性,今天我們只需要加入四個新符號,就能將範圍擴展到 1-3999!🎯

本日學習目標 🎯

今天我們將專注於:

  • 用 TDD 方法擴展到 1-3999 完整範圍
  • 學習羅馬數字的千位表示法
  • 深入理解查找表演算法的威力
  • 掌握 Vitest 測試框架的進階技巧

為什麼是 3999?

羅馬數字傳統上止於 3999 的原因:

  1. MMMCMXCIX = 3999,最大的四位數羅馬數字
  2. 4000 以上需要新記號:傳統系統沒有直接表示方法
  3. 重複限制:基本符號最多重複三次,MMM 已是 M 的極限
  4. 系統完整性:1-3999 涵蓋所有基本符號組合

新增符號 🔤

要處理 1-3999,需要加入:

  • D = 500:五百的基本符號
  • CD = 400:四百的減法表示(500-100)
  • CM = 900:九百的減法表示(1000-100)
  • M = 1000:千位的基本符號

TDD 實戰:處理新符號 🚀

步驟 1:測試四百的轉換

建立 tests/day14/roman-complete.test.ts

import { describe, it, expect } from 'vitest'
import { toRoman } from '../../src/roman/romanNumerals'

describe('Roman Numerals Complete Range', () => {
  it('convertsFourHundredToCD', () => {
    expect(toRoman(400)).toBe('CD')
  })
})

紅燈 🔴!更新 src/roman/romanNumerals.ts

export function toRoman(num: number): string {
  const values = [
    { value: 1000, symbol: 'M' }, { value: 900, symbol: 'CM' },
    { value: 500, symbol: 'D' }, { value: 400, symbol: 'CD' },
    { 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
}

步驟 2:測試其他新符號

繼續新增其他符號的測試:

it('convertsFiveHundredToD', () => {
  expect(toRoman(500)).toBe('D')
})

it('convertsNineHundredToCM', () => {
  expect(toRoman(900)).toBe('CM')
})

it('convertsOneThousandToM', () => {
  expect(toRoman(1000)).toBe('M')
})

綠燈 🟢!一次性加入所有符號讓相關測試都通過了。

關鍵測試:複雜數字組合 ⚡

里程碑數字測試

現在測試一些重要的里程碑數字,驗證我們的實作在複雜組合下的正確性:

it('convertsModernYear', () => {
  // 1994 = M(1000) + CM(900) + XC(90) + IV(4)
  // 這是一個包含多種減法組合的複雜例子
  expect(toRoman(1994)).toBe('MCMXCIV')
})

it('convertsCurrentYear', () => {
  // 2024 = MM(2000) + XX(20) + IV(4)
  // 測試現代年份的轉換
  expect(toRoman(2024)).toBe('MMXXIV')
})

it('convertsMaximumValue', () => {
  // 3999 = MMM(3000) + CM(900) + XC(90) + IX(9)
  // 這是羅馬數字系統的理論上限
  expect(toRoman(3999)).toBe('MMMCMXCIX')
})

it('handlesComplexSubtractionCombinations', () => {
  // 測試包含所有減法規則的數字
  expect(toRoman(3949)).toBe('MMMCMXLIX') // 3000 + 900 + 40 + 9
  expect(toRoman(1444)).toBe('MCDXLIV')   // 1000 + 400 + 40 + 4
  expect(toRoman(2999)).toBe('MMCMXCIX')  // 2000 + 900 + 90 + 9
})

執行測試,綠燈 🟢!我們的查找表方法成功處理了所有複雜組合。

理解複雜數字的分解過程

讓我們手動追蹤 1994 的轉換過程:

  1. 1994 >= 1000 → 加入 "M",剩餘 994
  2. 994 >= 900 → 加入 "CM",剩餘 94
  3. 94 >= 90 → 加入 "XC",剩餘 4
  4. 4 >= 4 → 加入 "IV",剩餘 0
  5. 結果:"MCMXCIV"

這個過程展示了貪婪算法在羅馬數字轉換中的完美應用。

羅馬數字系統分析 📊

羅馬數字使用七個基本符號,展現 1-5-10 的週期性模式:

  • I=1, V=5, X=10(個位組)
  • L=50, C=100(十位和百位基數)
  • D=500, M=1000(百位和千位基數)

減法規則:只有 I、X、C 可用於減法,形成 IV(4)、IX(9)、XL(40)、XC(90)、CD(400)、CM(900)。

TypeScript + Vitest 框架的優勢 ⭐

在這個 Kata 中,我們充分體驗了 TypeScript 與 Vitest 的優勢:

簡潔的測試語法

// Vitest 的簡潔寫法
it('convertsModernYear', () => {
  expect(toRoman(1994)).toBe('MCMXCIV')
})

// 對比傳統測試框架可能需要更多設置

強大的斷言 API

expect(result).toBe('MCMXCIV')
expect(result).toMatch(/^[MDCLXVI]+$/)

演算法設計的深度分析 🔍

我們的實作達到 O(1) 時間複雜度,因為查找表大小固定(13 個元素)。貪婪算法在羅馬數字中完美適用:選擇最大可用符號總是最佳策略,確保唯一正確表示。

學習成果

今天我們掌握了:

  • 完整羅馬數字系統:1-3999 的轉換規則
  • 減法規則:CD(400) 和 CM(900) 的重要性
  • 演算法優化:查找表 + 貪婪法的結合
  • TDD 威力:系統性建構複雜功能

我們的轉換器特點:

  • 測試全面:每個功能都有測試
  • 代碼清晰:查找表對應規則
  • 效能優秀:O(1) 複雜度
  • 易維護:規則更新簡單

完整實作 💡

完整實作 src/roman/romanNumerals.ts

export function toRoman(num: number): string {
  const values = [
    { value: 1000, symbol: 'M' }, { value: 900, symbol: 'CM' },
    { value: 500, symbol: 'D' }, { value: 400, symbol: 'CD' },
    { 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/day14/roman-complete.test.ts

import { describe, it, expect } from 'vitest'
import { toRoman } from '../../src/roman/romanNumerals'

describe('Roman Numerals Complete Range', () => {
  it('convertsFourHundredToCD', () => {
    expect(toRoman(400)).toBe('CD')
  })
  it('convertsFiveHundredToD', () => {
    expect(toRoman(500)).toBe('D')
  })
  it('convertsNineHundredToCM', () => {
    expect(toRoman(900)).toBe('CM')
  })
  it('convertsOneThousandToM', () => {
    expect(toRoman(1000)).toBe('M')
  })
  it('convertsModernYear', () => {
    expect(toRoman(1994)).toBe('MCMXCIV')
  })
  it('convertsMaximumValue', () => {
    expect(toRoman(3999)).toBe('MMMCMXCIX')
  })
})

錯誤處理與邊界條件 ⚠️

實作邊界檢查

用 TDD 添加錯誤處理:

describe('Error Handling', () => {
  it('throwsErrorForZero', () => {
    expect(() => toRoman(0)).toThrow('Number must be positive')
  })
  
  it('throwsErrorForNegativeNumbers', () => {
    expect(() => toRoman(-1)).toThrow('Number must be positive')
  })
  
  it('throwsErrorForNumbersTooLarge', () => {
    expect(() => toRoman(4000)).toThrow('Number must be <= 3999')
  })
})

實作更新:

export function toRoman(num: number): string {
  if (num <= 0) throw new Error('Number must be positive')
  if (num > 3999) throw new Error('Number must be <= 3999')
  // ... 現有邏輯
}

邊界測試案例

建立 tests/day14/roman-boundary.test.ts

import { describe, it, expect } from 'vitest'
import { toRoman } from '../../src/roman/romanNumerals'

describe('Roman Numeral Boundary Tests', () => {
  it('handlesBoundaryValuesCorrectly', () => {
    expect(toRoman(1)).toBe('I')
    expect(toRoman(3999)).toBe('MMMCMXCIX')
  })
  
  it('validatesInputRanges', () => {
    expect(() => toRoman(0)).toThrow()
    expect(() => toRoman(4000)).toThrow()
  })
})

實戰小技巧 💡

當測試案例增多時,善用 Vitest 的 describe 功能來組織測試。效能優化上,可以重複使用轉換器實例、使用資料提供者進行參數化測試,以及利用 Vitest 的平行測試執行功能。

今日成就與回顧 🎊

今日成就檢查清單

  • ✅ 擴展到完整範圍(1-3999)
  • ✅ 新符號導入(D、CD、CM、M)
  • ✅ 複雜測試驗證(1994、3999、2024)
  • ✅ 錯誤處理與邊界測試完善
  • ✅ TypeScript 型別安全設計
  • ✅ Vitest 現代測試語法應用

學習成果總結

技術成就:完整羅馬數字系統(1-3999)、Vitest 框架熟練、O(1) 高效實作

開發素養:TDD 紅綠重構、漸進式開發、測試組織、邊界處理

關鍵學習點回顧

  1. 查找表的威力:僅需 13 個映射就能處理 3999 個數字
  2. 貪婪算法應用:在羅馬數字中總是選擇最大符號
  3. 測試驅動開發:每個新功能都從失敗測試開始
  4. 邊界條件重要性:良好的錯誤處理讓程式更穩健

恭喜你完成了 Roman Numeral Kata!這個經典練習為我們的 React TDD 之旅奠定了堅實基礎。明天我們將開始探索新的挑戰!🚀


上一篇
Day 13 - 擴展到百位數(11-100)
下一篇
Day 15 - 羅馬數字反向轉換 🔄
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言