在 Day 12 我們成功建立了處理 1-10 的羅馬數字轉換器。今天我們要用 TDD 來擴展這個實作,讓它能夠處理更大的數字範圍,包括 11-100。
開始 → 測試 11-19 → 處理 20-39 → 挑戰 40 (XL)
↓
完成 ← 模式總結 ← 處理 90-100 ← 引入 50 (L)
讓我們從測試 11 開始,看看現有實作是否能處理。這是一個重要的實驗,因為它會告訴我們現有演算法的威力。
建立 tests/Unit/Day13/RomanNumeralExtendedTest.php
<?php
use App\Roman\RomanNumeral;
test('converts 11 to XI', function () {
$converter = new RomanNumeral();
expect($converter->convert(11))->toBe('XI');
});
test('converts 19 to XIX', function () {
$converter = new RomanNumeral();
expect($converter->convert(19))->toBe('XIX');
});
執行測試:
php artisan test --filter=RomanNumeralExtendedTest
太好了!測試通過了。我們的查找表實作已經能正確處理 11-19 的範圍!
這表示我們之前設計的 while 迴圈策略非常正確,能夠自動處理重複的符號。這就是好的設計的價值!
現在讓我們測試 20 和 27:
更新 tests/Unit/Day13/RomanNumeralExtendedTest.php
test('converts 20 to XX', function () {
$converter = new RomanNumeral();
expect($converter->convert(20))->toBe('XX');
});
test('converts 27 to XXVII', function () {
$converter = new RomanNumeral();
expect($converter->convert(27))->toBe('XXVII');
});
test('converts 30 to XXX', function () {
$converter = new RomanNumeral();
expect($converter->convert(30))->toBe('XXX');
});
test('converts 39 to XXXIX', function () {
$converter = new RomanNumeral();
expect($converter->convert(39))->toBe('XXXIX');
});
執行測試:
php artisan test --filter="converts (20|27|30|39)"
確認這些測試也通過!我們的 while 迴圈設計正確地處理了重複的符號。
這是因為我們的演算法設計:
現在我們遇到了第一個新的減法規則:40 = XL(50-10)。
這是一個很重要的測試案例,因為它會指出我們現有實作的不足。
test('converts 40 to XL', function () {
$converter = new RomanNumeral();
expect($converter->convert(40))->toBe('XL');
});
test('converts 44 to XLIV', function () {
$converter = new RomanNumeral();
expect($converter->convert(44))->toBe('XLIV');
});
執行測試...紅燈!我們需要添加這個減法規則到查找表中:
更新 app/Roman/RomanNumeral.php
private array $romanNumerals = [
40 => 'XL', // 重要:必須放在 10 之前
10 => 'X',
9 => 'IX',
5 => 'V',
4 => 'IV',
1 => 'I'
];
現在通過了!
這裡有一個重要的設計原則:減法組合必須放在對應的基本符號之前。如果 XL(40) 放在 X(10) 之後,40 會被處理成 XXXX,而不是 XL。
現在我們要處理新的符號 L = 50:
test('converts 50 to L', function () {
$converter = new RomanNumeral();
expect($converter->convert(50))->toBe('L');
});
test('converts 59 to LIX', function () {
$converter = new RomanNumeral();
expect($converter->convert(59))->toBe('LIX');
});
執行測試...紅燈!我們需要在查找表中添加 50。
更新 app/Roman/RomanNumeral.php
private array $romanNumerals = [
50 => 'L', // 新增 50
40 => 'XL',
10 => 'X',
9 => 'IX',
5 => 'V',
4 => 'IV',
1 => 'I'
];
現在所有 50-59 的測試都通過了!
讓我們跳到 90,這是另一個減法規則:90 = XC(100-10)。
這是另一個關鍵的測試點,因為它會測試我們的演算法是否能正確處理複雜的減法組合。
test('converts 90 to XC', function () {
$converter = new RomanNumeral();
expect($converter->convert(90))->toBe('XC');
});
test('converts 99 to XCIX', function () {
$converter = new RomanNumeral();
expect($converter->convert(99))->toBe('XCIX');
});
執行測試會失敗,因為目前會產生 "LXXXX" 而不是 "XC"。我們需要添加這個減法規則。
在添加程式碼前,想想看:為什麼羅馬數字要使用減法規則?如果允許四個相同符號(如 XXXX),會有什麼問題?
最後,讓我們處理 100 = C:
test('converts 100 to C', function () {
$converter = new RomanNumeral();
expect($converter->convert(100))->toBe('C');
});
通過今天的擴展,我們發現了羅馬數字的重要模式:
我們的查找表方法非常適合這種模式,因為它:
讓我們建立一個完整的測試套件來驗證我們的實作:
test('converts key numbers correctly', function () {
$converter = new RomanNumeral();
$testCases = [
[11, 'XI'], [20, 'XX'], [27, 'XXVII'],
[40, 'XL'], [44, 'XLIV'], [50, 'L'],
[59, 'LIX'], [90, 'XC'], [99, 'XCIX'],
[100, 'C']
];
foreach ($testCases as [$number, $expected]) {
expect($converter->convert($number))->toBe($expected);
}
});
我們的實作使用貪婪演算法配合查找表:
時間複雜度:O(1),空間複雜度:O(1)
完整實作 app/Roman/RomanNumeral.php
<?php
namespace App\Roman;
class RomanNumeral
{
private array $romanNumerals = [
100 => 'C', // 一百
90 => 'XC', // 九十(減法規則)
50 => 'L', // 五十
40 => 'XL', // 四十(減法規則)
10 => 'X', // 十
9 => 'IX', // 九(減法規則)
5 => 'V', // 五
4 => 'IV', // 四(減法規則)
1 => 'I' // 一
];
public function convert(int $number): string
{
$result = '';
foreach ($this->romanNumerals as $value => $numeral) {
while ($number >= $value) {
$result .= $numeral;
$number -= $value;
}
}
return $result;
}
}
我們現在擁有一個優雅且可靠的羅馬數字轉換器!通過今天的實作,我們看到了 TDD 如何幫助我們系統性地擴展功能。