iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0

在前面的學習中,我們已經完成了阿拉伯數字轉羅馬數字的功能。今天我們要實作反向轉換:將羅馬數字轉回阿拉伯數字 ✨

旅程回顧 📍

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

  • 基本符號轉換(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/Unit/Day15/RomanNumeralTest.php

<?php

use App\Roman\RomanNumeral;

describe('RomanNumeral fromRoman', function () {
    it('converts single roman symbols', function () {
        expect(RomanNumeral::fromRoman('I'))->toBe(1);
    });
});

執行測試,我們會看到紅燈:

pest tests/Unit/Day15/RomanNumeralTest.php

現在實作最簡單的版本:

更新 app/Roman/RomanNumeral.php

<?php

namespace App\Roman;

class RomanNumeral
{
    // ... 現有的 toRoman 方法 ...

    public static function fromRoman(string $roman): int
    {
        if ($roman === 'I') {
            return 1;
        }
        
        return 0;
    }
}

Green:讓測試通過

測試通過!

Refactor:擴展基本符號

擴展測試並重構實作:

更新 tests/Unit/Day15/RomanNumeralTest.php

<?php

use App\Roman\RomanNumeral;

describe('RomanNumeral fromRoman', function () {
    it('converts single roman symbols', function () {
        expect(RomanNumeral::fromRoman('I'))->toBe(1);
        expect(RomanNumeral::fromRoman('V'))->toBe(5);
        expect(RomanNumeral::fromRoman('X'))->toBe(10);
        expect(RomanNumeral::fromRoman('L'))->toBe(50);
        expect(RomanNumeral::fromRoman('C'))->toBe(100);
        expect(RomanNumeral::fromRoman('D'))->toBe(500);
        expect(RomanNumeral::fromRoman('M'))->toBe(1000);
    });
});

更新 app/Roman/RomanNumeral.php

<?php

namespace App\Roman;

class RomanNumeral
{
    private static array $symbols = [
        '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
    ];

    public static function toRoman(int $number): string
    {
        // ... 現有實作 ...
    }

    public static function fromRoman(string $roman): int
    {
        $values = [
            'I' => 1,
            'V' => 5,
            'X' => 10,
            'L' => 50,
            'C' => 100,
            'D' => 500,
            'M' => 1000
        ];

        return $values[$roman] ?? 0;
    }
}

處理多個字符和減法規則 ➕➖

現在我們要處理更複雜的情況,包括多個字符的組合和減法規則:

更新 tests/Unit/Day15/RomanNumeralTest.php

<?php

use App\Roman\RomanNumeral;

describe('RomanNumeral fromRoman', function () {
    it('converts single roman symbols', function () {
        expect(RomanNumeral::fromRoman('I'))->toBe(1);
        expect(RomanNumeral::fromRoman('V'))->toBe(5);
        expect(RomanNumeral::fromRoman('X'))->toBe(10);
        expect(RomanNumeral::fromRoman('L'))->toBe(50);
        expect(RomanNumeral::fromRoman('C'))->toBe(100);
        expect(RomanNumeral::fromRoman('D'))->toBe(500);
        expect(RomanNumeral::fromRoman('M'))->toBe(1000);
    });

    it('converts additive and subtractive roman numerals', function () {
        expect(RomanNumeral::fromRoman('II'))->toBe(2);
        expect(RomanNumeral::fromRoman('IV'))->toBe(4);
        expect(RomanNumeral::fromRoman('IX'))->toBe(9);
        expect(RomanNumeral::fromRoman('XL'))->toBe(40);
        expect(RomanNumeral::fromRoman('MCMXCIV'))->toBe(1994);
    });
});

重構 fromRoman 方法來處理所有情況:

public static function fromRoman(string $roman): int
{
    $values = [
        'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50,
        'C' => 100, 'D' => 500, 'M' => 1000
    ];

    $result = 0;
    $length = strlen($roman);

    for ($i = 0; $i < $length; $i++) {
        $current = $values[$roman[$i]];
        
        if ($i + 1 < $length && $current < $values[$roman[$i + 1]]) {
            $result -= $current; // 減法規則
        } else {
            $result += $current; // 加法規則
        }
    }

    return $result;
}

Red:添加輸入驗證

現在讓我們加入輸入驗證,確保只接受有效的羅馬數字:

更新 tests/Unit/Day15/RomanNumeralTest.php

describe('RomanNumeral fromRoman', function () {
    // ... 前面的測試 ...

    it('handles invalid roman numerals', function () {
        expect(fn () => RomanNumeral::fromRoman(''))->toThrow(InvalidArgumentException::class);
        expect(fn () => RomanNumeral::fromRoman('IIII'))->toThrow(InvalidArgumentException::class);
        expect(fn () => RomanNumeral::fromRoman('ABC'))->toThrow(InvalidArgumentException::class);
    });

    it('maintains bidirectional consistency', function () {
        $testNumbers = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994];
        
        foreach ($testNumbers as $number) {
            $roman = RomanNumeral::toRoman($number);
            $converted = RomanNumeral::fromRoman($roman);
            expect($converted)->toBe($number);
        }
    });
});

Green:完整驗證實作

更新 app/Roman/RomanNumeral.php

public static function fromRoman(string $roman): int
{
    if (empty($roman)) {
        throw new InvalidArgumentException('Roman numeral cannot be empty');
    }

    if (!self::isValidRoman($roman)) {
        throw new InvalidArgumentException('Invalid roman numeral: ' . $roman);
    }

    $values = [
        'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50,
        'C' => 100, 'D' => 500, 'M' => 1000
    ];

    $result = 0;
    $length = strlen($roman);

    for ($i = 0; $i < $length; $i++) {
        $current = $values[$roman[$i]];
        
        if ($i + 1 < $length && $current < $values[$roman[$i + 1]]) {
            $result -= $current;
        } else {
            $result += $current;
        }
    }

    return $result;
}

private static function isValidRoman(string $roman): bool
{
    if (!preg_match('/^[IVXLCDM]+$/', $roman)) {
        return false;
    }

    if (preg_match('/(IIII|XXXX|CCCC|VV|LL|DD)/', $roman)) {
        return false;
    }

    return true;
}

Refactor:雙向一致性測試

為了確保轉換的正確性,讓我們加入更多測試案例:

更新 tests/Unit/Day15/RomanNumeralTest.php

describe('RomanNumeral complete bidirectional conversion', function () {
    it('correctly converts edge cases', function () {
        // 邊界值測試
        expect(RomanNumeral::fromRoman('III'))->toBe(3);
        expect(RomanNumeral::fromRoman('VIII'))->toBe(8);
        expect(RomanNumeral::fromRoman('XIV'))->toBe(14);
        expect(RomanNumeral::fromRoman('XXIX'))->toBe(29);
        expect(RomanNumeral::fromRoman('XLIV'))->toBe(44);
        expect(RomanNumeral::fromRoman('XCIX'))->toBe(99);
        expect(RomanNumeral::fromRoman('CDXLIV'))->toBe(444);
        expect(RomanNumeral::fromRoman('CMXCIX'))->toBe(999);
    });
    
    it('converts complex numbers correctly', function () {
        expect(RomanNumeral::fromRoman('MMXXIV'))->toBe(2024);
        expect(RomanNumeral::fromRoman('MCMLXXXIV'))->toBe(1984);
        expect(RomanNumeral::fromRoman('MMMCMXCIX'))->toBe(3999);
    });
});

測試執行與驗證 ✅

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

pest tests/Unit/Day15/

所有測試都應該通過,顯示我們的雙向轉換器已經完成!

常見問題排除

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

性能分析 📊

複雜度分析

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

優化策略

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

今天的收穫 🏆

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

關鍵成就

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

下一步展望

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

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


上一篇
Day 14 - 完整範圍實作(1-3999) 🏛️
下一篇
Day 16 - 效能優化 🚀
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言