在前面的學習中,我們已經完成了阿拉伯數字轉羅馬數字的功能。今天我們要實作反向轉換:將羅馬數字轉回阿拉伯數字 ✨
我們已經完成了羅馬數字轉換器的單向功能:
今天要實作反向轉換,讓轉換器具備雙向功能!
fromRoman()
方法反向轉換需要處理:
特殊減法組合:
建立測試檔案:
建立 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;
}
}
測試通過!
擴展測試並重構實作:
更新 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;
}
現在讓我們加入輸入驗證,確保只接受有效的羅馬數字:
更新 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);
}
});
});
更新 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;
}
為了確保轉換的正確性,讓我們加入更多測試案例:
更新 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/
所有測試都應該通過,顯示我們的雙向轉換器已經完成!
今天我們完成了羅馬數字的反向轉換功能:
明天我們將繼續完善羅馬數字轉換器,加入更多的測試案例和功能增強,確保我們的實作能夠處理各種邊界情況和實際應用需求。
透過 TDD 的紅綠重構循環,我們成功建立了一個穩健的雙向轉換系統,這種方法確保了程式碼的正確性和可維護性 🚀