iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Modern Web

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

Day 15 - 羅馬數字反向轉換 🔄

  • 分享至 

  • xImage
  •  

在前面的學習中,我們已經掌握了將阿拉伯數字轉換為羅馬數字的技巧。今天我們要使用 TDD 來實作反向轉換功能,將羅馬數字轉換回阿拉伯數字 ✨

旅程回顧 📍

我們已經完成了羅馬數字轉換器的單向功能:

  • 基本符號轉換(I, V, X)
  • 擴展到完整範圍(L, C, D, M)
  • 減法規則處理(IV, IX, XL, XC, CD, CM)
  • 千位數和邊界處理(1-3999)

今天要實作反向轉換,讓轉換器具備雙向功能!

今天的目標 🎯

  1. 實作 fromRoman() 函數
  2. 處理加法規則(I=1, V=5, X=10 等)
  3. 處理減法規則(IV=4, IX=9, XL=40 等)
  4. 加入輸入驗證
  5. 建立完整的雙向轉換系統

理解羅馬數字規則

基本符號與規則

  • I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000
  • 加法:相同或遞減符號相加(II = 2, VI = 6)
  • 減法:小符號在大符號前表示減法(IV = 4, IX = 9)

解析策略與減法規則

反向轉換需要處理:

  1. 左到右掃描:逐字符分析
  2. 上下文判斷:根據相鄰字符決定加減法
  3. 規則驗證:確保格式合法性

特殊減法組合:

  • I 只能在 V、X 前面(IV=4, IX=9)
  • X 只能在 L、C 前面(XL=40, XC=90)
  • C 只能在 D、M 前面(CD=400, CM=900)

開始 TDD 循環 🔴🟢🔵

Red:第一個測試失敗

建立測試檔案:

// tests/day15/from-roman.test.ts
import { describe, test, expect } from 'vitest'
import { fromRoman } from '../../src/roman/converter'

describe('fromRoman', () => {
  test('convertsSingleRomanNumerals', () => {
    expect(fromRoman('I')).toBe(1)
  })
})

Green:讓測試通過

先實作最簡單的版本:

// 更新 src/roman/converter.ts
export function fromRoman(roman: string): number {
  return 1
}

Refactor:擴展基本符號

擴展測試並重構實作:

// 更新 tests/day15/from-roman.test.ts
describe('fromRoman', () => {
  test('convertsSingleRomanNumerals', () => {
    expect(fromRoman('I')).toBe(1)
    expect(fromRoman('V')).toBe(5)
    expect(fromRoman('X')).toBe(10)
  })
})
// 更新 src/roman/converter.ts
export function fromRoman(roman: string): number {
  const values: Record<string, number> = {
    'I': 1, 'V': 5, 'X': 10, 'L': 50,
    'C': 100, 'D': 500, 'M': 1000
  }
  return values[roman] || 0
}

Red:處理多符號加法

在處理多個符號時,我們需要理解羅馬數字的累加邏輯:

// 更新 tests/day15/from-roman.test.ts
test('convertsMultipleRomanNumeralsWithAddition', () => {
  expect(fromRoman('II')).toBe(2)
  expect(fromRoman('VI')).toBe(6)
})
// 更新 src/roman/converter.ts
export function fromRoman(roman: string): number {
  const values: Record<string, number> = {
    'I': 1, 'V': 5, 'X': 10, 'L': 50,
    'C': 100, 'D': 500, 'M': 1000
  }
  
  let result = 0
  for (let i = 0; i < roman.length; i++) {
    result += values[roman[i]]
  }
  return result
}

Green:實作加法邏輯

這個實作只適用於純加法,需要前瞻算法處理減法規則。

為什麼需要前瞻?

在處理 "IV" 時,簡單累加會得到錯誤結果:

  • I (1) + V (5) = 6 ❌ 錯誤
  • 正確:V (5) - I (1) = 4 ✅

Red:處理減法規則

// 更新 tests/day15/from-roman.test.ts
test('convertsRomanNumeralsWithSubtraction', () => {
  expect(fromRoman('IV')).toBe(4)
  expect(fromRoman('IX')).toBe(9)
  expect(fromRoman('XL')).toBe(40)
})
// 更新 src/roman/converter.ts
export function fromRoman(roman: string): number {
  const values: Record<string, number> = {
    'I': 1, 'V': 5, 'X': 10, 'L': 50,
    'C': 100, 'D': 500, 'M': 1000
  }
  
  let result = 0
  for (let i = 0; i < roman.length; i++) {
    const current = values[roman[i]]
    const next = values[roman[i + 1]]
    
    if (next && current < next) {
      result += next - current
      i++ // 跳過下一個字符
    } else {
      result += current
    }
  }
  return result
}

Green:實作前瞻判斷

比較相鄰字符大小:如果 current < next 是減法,否則累加。

"MCMXCIV" (1994) 執行過程:

  • M(1000): 下個是 C,正常累加
  • C(100): 下個是 M,100 < 1000,減法 900
  • X(10): 下個是 C,10 < 100,減法 90
  • I(1): 下個是 V,1 < 5,減法 4
  • 結果:1000 + 900 + 90 + 4 = 1994

Refactor:測試複雜情況

test('convertsComplexRomanNumerals', () => {
  expect(fromRoman('XIV')).toBe(14)
  expect(fromRoman('MCMXCIV')).toBe(1994)
  expect(fromRoman('MMMCMXCIX')).toBe(3999)
})

test('convertsEdgeCases', () => {
  expect(fromRoman('CDXLIV')).toBe(444)
  expect(fromRoman('CMXC')).toBe(990)
  expect(fromRoman('MCDXLIV')).toBe(1444)
})

這些案例測試多個減法組合的正確性。

Red:添加輸入驗證

test('handlesInvalidInput', () => {
  expect(() => fromRoman('')).toThrow('Invalid Roman numeral')
  expect(() => fromRoman('IIII')).toThrow('Invalid Roman numeral')
  expect(() => fromRoman('VX')).toThrow('Invalid Roman numeral')
  expect(() => fromRoman('ABCD')).toThrow('Invalid Roman numeral')
})

驗證需求:檢查空字符串、無效字符、重複錯誤及無效減法組合。

// 完整實作 src/roman/converter.ts
export function fromRoman(roman: string): number {
  if (!roman) throw new Error('Invalid Roman numeral')
  
  const values: Record<string, number> = {
    'I': 1, 'V': 5, 'X': 10, 'L': 50,
    'C': 100, 'D': 500, 'M': 1000
  }
  
  // 檢查無效字符
  for (const char of roman) {
    if (!values[char]) throw new Error('Invalid Roman numeral')
  }
  
  // 檢查重複規則
  const invalidPatterns = ['IIII', 'VV', 'XXXX', 'LL', 'CCCC', 'DD']
  for (const pattern of invalidPatterns) {
    if (roman.includes(pattern)) {
      throw new Error('Invalid Roman numeral')
    }
  }
  
  // 檢查無效減法組合
  const invalidSubtractions = ['VX', 'VL', 'VC', 'VD', 'VM', 'LC', 'LD', 'LM', 'DM']
  for (const invalid of invalidSubtractions) {
    if (roman.includes(invalid)) {
      throw new Error('Invalid Roman numeral')
    }
  }
  
  let result = 0
  for (let i = 0; i < roman.length; i++) {
    const current = values[roman[i]]
    const next = values[roman[i + 1]]
    
    if (next && current < next) {
      result += next - current
      i++
    } else {
      result += current
    }
  }
  return result
}

Green:完整驗證實作

分層驗證:字符檢查、重複檢查、減法檢查,確保函數健壯性。

Refactor:雙向一致性測試

// 更新 tests/day15/from-roman.test.ts
import { fromRoman, toRoman } from '../../src/roman/converter'

test('isInverseOfToRoman', () => {
  const testNumbers = [1, 4, 5, 9, 10, 40, 90, 400, 900, 1994]
  for (const num of testNumbers) {
    expect(fromRoman(toRoman(num))).toBe(num)
  }
})

測試執行與驗證 ✅

執行所有測試來確認功能正常:

npm test tests/day15/

常見問題排除

  1. 導入錯誤:確保正確 export/import 函數
  2. 轉換錯誤:檢查前瞻邏輯和索引跳躍
  3. 驗證問題:平衡嚴格性與實用性

性能分析 📊

複雜度分析

  • 時間複雜度:O(n) - 需要遍歷每個字符
  • 空間複雜度:O(1) - 只使用固定的查找表

優化策略

  1. 預計算常用轉換:快取常見羅馬數字
  2. 早期返回:遇到無效格式立即返回
  3. 批次處理:一次處理多個相同符號

今天的收穫 🏆

今天我們完成了羅馬數字的反向轉換功能:

關鍵成就

  • TDD 實踐:從簡單到複雜,逐步實作功能
  • 算法理解:掌握羅馬數字解析和減法規則識別
  • 程式碼品質:完整錯誤處理和邊界測試
  • 系統設計:建立穩健的雙向轉換系統
  • 除錯技能:學會追蹤算法執行和問題排除
  • 性能意識:理解算法複雜度和優化策略

下一步展望

明天我們將繼續完善羅馬數字轉換器,加入更多的測試案例和功能增強,確保我們的實作能夠處理各種邊界情況和實際應用需求。

透過 TDD 的紅綠重構循環,我們成功建立了一個穩健的雙向轉換系統,這種方法確保了程式碼的正確性和可維護性 🚀


上一篇
Day 14 - 完整範圍實作(1-3999) 🏛️
下一篇
Day 16 - 效能優化 🚀
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言