想像一下,你正在開發一個歷史文件數位化系統,需要處理從古羅馬到現代的所有年份標記。今天我們要用 TDD 完成最後一塊拼圖,讓轉換器能夠處理完整的羅馬數字範圍:1-3999!
今天我們將專注於:
昨天的查找表設計展現了強大擴展性,今天只需加入四個新符號就能擴展到 1-3999!
羅馬數字傳統上止於 3999 的原因:
要處理 1-3999,需要加入:
建立 tests/Unit/Day14/RomanNumeralCompleteTest.php
:
<?php
use App\Roman\RomanNumeral;
test('converts 400 to CD', function () {
$converter = new RomanNumeral();
expect($converter->convert(400))->toBe('CD');
});
紅燈 🔴!更新 app/Roman/RomanNumeral.php
:
<?php
namespace App\Roman;
class RomanNumeral
{
private array $romanNumerals = [
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'
];
public function convert(int $number): string
{
$result = '';
foreach ($this->romanNumerals as $value => $numeral) {
while ($number >= $value) {
$result .= $numeral;
$number -= $value;
}
}
return $result;
}
}
繼續新增其他符號的測試:
test('converts 500 to D', function () {
$converter = new RomanNumeral();
expect($converter->convert(500))->toBe('D');
});
test('converts 900 to CM', function () {
$converter = new RomanNumeral();
expect($converter->convert(900))->toBe('CM');
});
test('converts 1000 to M', function () {
$converter = new RomanNumeral();
expect($converter->convert(1000))->toBe('M');
});
綠燈 🟢!一次性加入所有符號讓相關測試都通過了。
現在測試一些重要的里程碑數字,驗證我們的實作在複雜組合下的正確性:
test('converts historical year 1994 to MCMXCIV', function () {
// 1994 = M(1000) + CM(900) + XC(90) + IV(4)
// 這是一個包含多種減法組合的複雜例子
$converter = new RomanNumeral();
expect($converter->convert(1994))->toBe('MCMXCIV');
});
test('converts modern year 2024 to MMXXIV', function () {
// 2024 = MM(2000) + XX(20) + IV(4)
// 測試現代年份的轉換
$converter = new RomanNumeral();
expect($converter->convert(2024))->toBe('MMXXIV');
});
test('converts maximum value 3999 to MMMCMXCIX', function () {
// 3999 = MMM(3000) + CM(900) + XC(90) + IX(9)
// 這是羅馬數字系統的理論上限
$converter = new RomanNumeral();
expect($converter->convert(3999))->toBe('MMMCMXCIX');
});
test('converts edge cases with all subtraction rules', function () {
$converter = new RomanNumeral();
// 測試包含所有減法規則的數字
expect($converter->convert(3949))->toBe('MMMCMXLIX'); // 3000 + 900 + 40 + 9
expect($converter->convert(1444))->toBe('MCDXLIV'); // 1000 + 400 + 40 + 4
expect($converter->convert(2999))->toBe('MMCMXCIX'); // 2000 + 900 + 90 + 9
});
執行測試,綠燈 🟢!我們的查找表方法成功處理了所有複雜組合。
讓我們手動追蹤 1994 的轉換過程:
這個過程展示了貪婪算法在羅馬數字轉換中的完美應用。
羅馬數字使用七個基本符號,展現 1-5-10 的週期性模式:
減法規則:只有 I、X、C 可用於減法,形成 IV(4)、IX(9)、XL(40)、XC(90)、CD(400)、CM(900)。
在這個 Kata 中,我們充分體驗了 Pest 的優勢:
// Pest 的簡潔寫法
test('converts 1994 to MCMXCIV', function () {
$converter = new RomanNumeral();
expect($converter->convert(1994))->toBe('MCMXCIV');
});
// 對比 PHPUnit 的傳統寫法
class RomanNumeralTest extends TestCase {
public function testConverts1994ToMCMXCIV() {
$converter = new RomanNumeral();
$this->assertEquals('MCMXCIV', $converter->convert(1994));
}
}
expect($result)->toBe('MCMXCIV');
expect($result)->toMatch('/^[MDCLXVI]+$/');
我們的實作達到 O(1) 時間複雜度,因為查找表大小固定(13 個元素)。貪婪算法在羅馬數字中完美適用:選擇最大可用符號總是最佳策略,確保唯一正確表示。
今天我們掌握了:
我們的轉換器特點:
<?php
namespace App\Roman;
class RomanNumeral
{
private array $romanNumerals = [
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'
];
public function convert(int $number): string
{
$result = '';
foreach ($this->romanNumerals as $value => $numeral) {
while ($number >= $value) {
$result .= $numeral;
$number -= $value;
}
}
return $result;
}
}
更新 app/Roman/RomanNumeral.php
添加錯誤處理:
public function convert(int $number): string
{
if ($number <= 0) {
throw new \InvalidArgumentException('Number must be positive');
}
if ($number > 3999) {
throw new \InvalidArgumentException('Number must be <= 3999');
}
// ... 現有邏輯
}
建立 tests/Unit/Day14/RomanNumeralBoundaryTest.php
:
<?php
use App\Roman\RomanNumeral;
test('throws exception for zero', function () {
$converter = new RomanNumeral();
expect(fn() => $converter->convert(0))
->toThrow(\InvalidArgumentException::class);
});
test('throws exception for negative numbers', function () {
$converter = new RomanNumeral();
expect(fn() => $converter->convert(-5))
->toThrow(\InvalidArgumentException::class);
});
test('throws exception for numbers over 3999', function () {
$converter = new RomanNumeral();
expect(fn() => $converter->convert(4000))
->toThrow(\InvalidArgumentException::class);
});
test('handles boundary values correctly', function () {
$converter = new RomanNumeral();
expect($converter->convert(1))->toBe('I');
expect($converter->convert(3999))->toBe('MMMCMXCIX');
});
當測試案例增多時,善用 Pest 的 describe
功能來組織測試。效能優化上,可以重複使用轉換器實例、使用資料提供者進行參數化測試,以及利用 Pest 的平行測試執行功能加速測試。
test('converts various numbers', function ($input, $expected) {
$converter = new RomanNumeral();
expect($converter->convert($input))->toBe($expected);
})->with([
[1994, 'MCMXCIV'],
[2024, 'MMXXIV'],
[3999, 'MMMCMXCIX'],
[444, 'CDXLIV'],
[888, 'DCCCLXXXVIII'],
]);
這種作法可以大幅減少測試代碼的重複性,讓測試更清晰。
技術成就:完整羅馬數字系統(1-3999)、Pest 框架熟練、O(1) 高效實作
開發素養:TDD 紅綠重構、漸進式開發、測試組織、邊界處理
恭喜你完成了 Roman Numeral Kata!這個經典練習為我們的 Laravel TDD 之旅奠定了堅實基礎。明天我們將開始探索新的挑戰!🚀