From 78f8e1a3a016a39c06b3521f95dcb5aec4ac67b6 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:06:33 +0700 Subject: [PATCH 01/34] RED: test convert 1 to I --- test/roman_numerals_test.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index ab73b3a..0dc4f81 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -1 +1,8 @@ -void main() {} +import 'package:tdd_katas/roman_numerals.dart' as roman_numerals; +import 'package:test/test.dart'; + +void main() { + test('converts 1 to I', () { + expect(roman_numerals.integerToRoman(1), 'I'); + }); +} From 0faa1de47c9f97d7ac6d5fa24b9ebb0e53ea3023 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:08:35 +0700 Subject: [PATCH 02/34] GREEN: return hardcoded "I" for 1 --- lib/roman_numerals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 0cbc072..56b7b8e 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,3 +1,3 @@ String integerToRoman(int number) { - throw UnimplementedError(); + return 'I'; } From a4e668942781e58656c4d6849cd9d7e87d7acb41 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:10:32 +0700 Subject: [PATCH 03/34] RED: test converts 2 to II --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 0dc4f81..df86d16 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -5,4 +5,8 @@ void main() { test('converts 1 to I', () { expect(roman_numerals.integerToRoman(1), 'I'); }); + + test('converts 2 to II', () { + expect(roman_numerals.integerToRoman(2), 'II'); + }); } From 48b8967e0fd3102811ed794f17ba1f40e6fd9d7b Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:11:12 +0700 Subject: [PATCH 04/34] GREEN: return hardcoded "II" for 2 --- lib/roman_numerals.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 56b7b8e..af5a0c1 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,3 +1,10 @@ String integerToRoman(int number) { - return 'I'; + if (number == 1) { + return 'I'; + } + if (number == 2) { + return 'II'; + } + + throw UnimplementedError('Conversion not implemented for $number'); } From 6f5b2ab2f5fe63852c6c04fa833dab948be47403 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:12:28 +0700 Subject: [PATCH 05/34] RED: test convert 3 to III --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index df86d16..c8f9f17 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -9,4 +9,8 @@ void main() { test('converts 2 to II', () { expect(roman_numerals.integerToRoman(2), 'II'); }); + + test('converts 3 to III', () { + expect(roman_numerals.integerToRoman(3), 'III'); + }); } From ba924edde7227b8e0a4f6e4da1daf1bf92ec5a13 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:14:06 +0700 Subject: [PATCH 06/34] GREEN: add simple logic to handle 1-3 --- lib/roman_numerals.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index af5a0c1..d41a455 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,10 +1,7 @@ String integerToRoman(int number) { - if (number == 1) { - return 'I'; + var result = ''; + for (int i = 0; i < number; i++) { + result += 'I'; } - if (number == 2) { - return 'II'; - } - - throw UnimplementedError('Conversion not implemented for $number'); + return result; } From 0de7f6708015b6de8952baadbb627c5c1609e7ca Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:17:06 +0700 Subject: [PATCH 07/34] RED: test convert 5 to V --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index c8f9f17..dd7fded 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -13,4 +13,8 @@ void main() { test('converts 3 to III', () { expect(roman_numerals.integerToRoman(3), 'III'); }); + + test('converts 5 to V', () { + expect(roman_numerals.integerToRoman(5), 'V'); + }); } From 1cfd2199ee32f8419a0ce2ed5efcfb5d1a2c5c5c Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:19:23 +0700 Subject: [PATCH 08/34] GREEN: return hardcoded "V" for 5 --- lib/roman_numerals.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index d41a455..ef9f436 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,6 +1,11 @@ String integerToRoman(int number) { var result = ''; for (int i = 0; i < number; i++) { + if (number == 5) { + result += 'V'; + break; + } + result += 'I'; } return result; From 87b7e7946239b33bded6f8ca109c150330b7f0e6 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:20:05 +0700 Subject: [PATCH 09/34] RED: test convert 4 to IV --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index dd7fded..324c4ea 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -17,4 +17,8 @@ void main() { test('converts 5 to V', () { expect(roman_numerals.integerToRoman(5), 'V'); }); + + test('converts 4 to IV', () { + expect(roman_numerals.integerToRoman(4), 'IV'); + }); } From 5c9962eb6102d4bd17915807188301bea62c0170 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:22:39 +0700 Subject: [PATCH 10/34] GREEN: return hardcoded "IV" for 4 --- lib/roman_numerals.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index ef9f436..a962a11 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -3,7 +3,12 @@ String integerToRoman(int number) { for (int i = 0; i < number; i++) { if (number == 5) { result += 'V'; - break; + return result; + } + + if (number == 4) { + result += 'IV'; + return result; } result += 'I'; From c1a5ed7ed7dd018b055a12f96517cf2699704512 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:39:34 +0700 Subject: [PATCH 11/34] REFACTOR: extract logic and remove loop --- lib/roman_numerals.dart | 26 +++++++++++++------------- test/roman_numerals_test.dart | 8 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index a962a11..8bd07b5 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,17 +1,17 @@ String integerToRoman(int number) { - var result = ''; - for (int i = 0; i < number; i++) { - if (number == 5) { - result += 'V'; - return result; - } + const romanSymbols = {5: 'V', 1: 'I'}; - if (number == 4) { - result += 'IV'; - return result; - } - - result += 'I'; + if (_isSubtractiveCase(number)) { + return romanSymbols[1]! + romanSymbols[5]!; } - return result; + + if (_canRepeatSymbol(number)) { + return romanSymbols[1]! * number; + } + + return romanSymbols[number] ?? ''; } + +bool _isSubtractiveCase(int number) => number == 4; + +bool _canRepeatSymbol(int number) => number < 4; diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 324c4ea..bc1732f 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -14,11 +14,11 @@ void main() { expect(roman_numerals.integerToRoman(3), 'III'); }); - test('converts 5 to V', () { - expect(roman_numerals.integerToRoman(5), 'V'); - }); - test('converts 4 to IV', () { expect(roman_numerals.integerToRoman(4), 'IV'); }); + + test('converts 5 to V', () { + expect(roman_numerals.integerToRoman(5), 'V'); + }); } From dfa3b66ccb466a27709bfbbf289f61af84474cec Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:42:02 +0700 Subject: [PATCH 12/34] RED: test convert 6 to VI --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index bc1732f..f344ea0 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -21,4 +21,8 @@ void main() { test('converts 5 to V', () { expect(roman_numerals.integerToRoman(5), 'V'); }); + + test('converts 6 to VI', () { + expect(roman_numerals.integerToRoman(6), 'VI'); + }); } From 445ba862eefd668b9a323631e80da7eeefea8307 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:43:00 +0700 Subject: [PATCH 13/34] GREEN: add roman symbol for 6 --- lib/roman_numerals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 8bd07b5..75e27bc 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,5 @@ String integerToRoman(int number) { - const romanSymbols = {5: 'V', 1: 'I'}; + const romanSymbols = {6: 'VI', 5: 'V', 1: 'I'}; if (_isSubtractiveCase(number)) { return romanSymbols[1]! + romanSymbols[5]!; From 44845ed71b2f477d625f938af2432f4908ec72fd Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:43:57 +0700 Subject: [PATCH 14/34] RED: test convert 7 to VII --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index f344ea0..292247e 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -25,4 +25,8 @@ void main() { test('converts 6 to VI', () { expect(roman_numerals.integerToRoman(6), 'VI'); }); + + test('converts 7 to VII', () { + expect(roman_numerals.integerToRoman(7), 'VII'); + }); } From 6ad0556fcf5b606bc0cc386b3714b5b5ee918715 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:44:18 +0700 Subject: [PATCH 15/34] GREEN: add roman symbol for 7 --- lib/roman_numerals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 75e27bc..3476605 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,5 @@ String integerToRoman(int number) { - const romanSymbols = {6: 'VI', 5: 'V', 1: 'I'}; + const romanSymbols = {7: 'VII', 6: 'VI', 5: 'V', 1: 'I'}; if (_isSubtractiveCase(number)) { return romanSymbols[1]! + romanSymbols[5]!; From b5a216cceaee5983b6d70b0c26062348d80c1354 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:44:55 +0700 Subject: [PATCH 16/34] RED: test convert 8 to VIII --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 292247e..43f1869 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -29,4 +29,8 @@ void main() { test('converts 7 to VII', () { expect(roman_numerals.integerToRoman(7), 'VII'); }); + + test('converts 8 to VIII', () { + expect(roman_numerals.integerToRoman(8), 'VIII'); + }); } From f6d75f15e1d26a185396dc980434a657e9a8e42a Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:45:51 +0700 Subject: [PATCH 17/34] GREEN: add roman symbol for 8 --- lib/roman_numerals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 3476605..9ba01a9 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,5 @@ String integerToRoman(int number) { - const romanSymbols = {7: 'VII', 6: 'VI', 5: 'V', 1: 'I'}; + const romanSymbols = {8: 'VIII', 7: 'VII', 6: 'VI', 5: 'V', 1: 'I'}; if (_isSubtractiveCase(number)) { return romanSymbols[1]! + romanSymbols[5]!; From 0a87dc96cb8f16bb26ed15ca18f13b52aba2228a Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:48:59 +0700 Subject: [PATCH 18/34] RED: test convert 9 to IX --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 43f1869..5504e98 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -33,4 +33,8 @@ void main() { test('converts 8 to VIII', () { expect(roman_numerals.integerToRoman(8), 'VIII'); }); + + test('converts 9 to IX', () { + expect(roman_numerals.integerToRoman(9), 'IX'); + }); } From da8f7bbfb10057a36d8f203ccedc758212823f91 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:49:39 +0700 Subject: [PATCH 19/34] GREEN: add roman symbol for 9 --- lib/roman_numerals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 9ba01a9..5c084ea 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,5 @@ String integerToRoman(int number) { - const romanSymbols = {8: 'VIII', 7: 'VII', 6: 'VI', 5: 'V', 1: 'I'}; + const romanSymbols = {9: 'IX', 8: 'VIII', 7: 'VII', 6: 'VI', 5: 'V', 1: 'I'}; if (_isSubtractiveCase(number)) { return romanSymbols[1]! + romanSymbols[5]!; From 2117e4c193b91e3e5fb9fbd1cc368531301f585e Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:50:33 +0700 Subject: [PATCH 20/34] RED: test convert 10 to X --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 5504e98..4bde3a0 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -37,4 +37,8 @@ void main() { test('converts 9 to IX', () { expect(roman_numerals.integerToRoman(9), 'IX'); }); + + test('converts 10 to X', () { + expect(roman_numerals.integerToRoman(10), 'X'); + }); } From 7eed3f8e2ace836ed7f88080d17bc80f852a6c74 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 08:50:50 +0700 Subject: [PATCH 21/34] GREEN: add roman symbol for 10 --- lib/roman_numerals.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 5c084ea..f4fbc57 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,13 @@ String integerToRoman(int number) { - const romanSymbols = {9: 'IX', 8: 'VIII', 7: 'VII', 6: 'VI', 5: 'V', 1: 'I'}; + const romanSymbols = { + 10: 'X', + 9: 'IX', + 8: 'VIII', + 7: 'VII', + 6: 'VI', + 5: 'V', + 1: 'I', + }; if (_isSubtractiveCase(number)) { return romanSymbols[1]! + romanSymbols[5]!; From b12821147d95e606e665ddca68dc1168796be439 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:12:34 +0700 Subject: [PATCH 22/34] REFACTOR: apply DRY, remove magic numbers, add `conversionRules` domain --- lib/roman_numerals.dart | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index f4fbc57..f4666c1 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,25 +1,15 @@ String integerToRoman(int number) { - const romanSymbols = { - 10: 'X', - 9: 'IX', - 8: 'VIII', - 7: 'VII', - 6: 'VI', - 5: 'V', - 1: 'I', - }; + const conversionRules = [(10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')]; - if (_isSubtractiveCase(number)) { - return romanSymbols[1]! + romanSymbols[5]!; + final result = StringBuffer(); + var remaining = number; + + for (final (value, symbol) in conversionRules) { + while (remaining >= value) { + result.write(symbol); + remaining -= value; + } } - if (_canRepeatSymbol(number)) { - return romanSymbols[1]! * number; - } - - return romanSymbols[number] ?? ''; + return result.toString(); } - -bool _isSubtractiveCase(int number) => number == 4; - -bool _canRepeatSymbol(int number) => number < 4; From e411e38a314adb5ea8f518f16ebb56e8394be776 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:12:54 +0700 Subject: [PATCH 23/34] RED: test convert 40 to XL --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 4bde3a0..90d3e90 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -41,4 +41,8 @@ void main() { test('converts 10 to X', () { expect(roman_numerals.integerToRoman(10), 'X'); }); + + test('converts 40 to XL', () { + expect(roman_numerals.integerToRoman(40), 'XL'); + }); } From bef6a56d0621d8e5d131acb53fcef1c078d5449c Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:16:38 +0700 Subject: [PATCH 24/34] GREEN: add coversion rule for 40 --- lib/roman_numerals.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index f4666c1..abddc4d 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,12 @@ String integerToRoman(int number) { - const conversionRules = [(10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')]; + const conversionRules = [ + (40, 'XL'), + (10, 'X'), + (9, 'IX'), + (5, 'V'), + (4, 'IV'), + (1, 'I'), + ]; final result = StringBuffer(); var remaining = number; From 44566369d0e8f8a7066bbc006f03f4637350a24a Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:18:11 +0700 Subject: [PATCH 25/34] RED: test convert 50 to L --- test/roman_numerals_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 90d3e90..da5dc7e 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -45,4 +45,8 @@ void main() { test('converts 40 to XL', () { expect(roman_numerals.integerToRoman(40), 'XL'); }); + + test('converts 50 to L', () { + expect(roman_numerals.integerToRoman(50), 'L'); + }); } From dc6728bea5446b028340b968e7a0682c2722efc4 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:18:47 +0700 Subject: [PATCH 26/34] GREEN: add conversion rule for 50 --- lib/roman_numerals.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index abddc4d..0697888 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,6 @@ String integerToRoman(int number) { const conversionRules = [ + (50, 'L'), (40, 'XL'), (10, 'X'), (9, 'IX'), From 24cdceaa9254ceae0b3fa8d2a19eb12a553ec86b Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:20:39 +0700 Subject: [PATCH 27/34] RED: test convert 90 to XC and 100 to C --- test/roman_numerals_test.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index da5dc7e..6cc7f75 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -49,4 +49,12 @@ void main() { test('converts 50 to L', () { expect(roman_numerals.integerToRoman(50), 'L'); }); + + test('converts 90 to XC', () { + expect(roman_numerals.integerToRoman(90), 'XC'); + }); + + test('converts 100 to C', () { + expect(roman_numerals.integerToRoman(100), 'C'); + }); } From 1b0336edeb8c8a6d384d492dd85bd95a6247dbd2 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:21:10 +0700 Subject: [PATCH 28/34] GREEN: add convertion rules for 90 and 100 --- lib/roman_numerals.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 0697888..f2bb0d8 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,7 @@ String integerToRoman(int number) { const conversionRules = [ + (100, 'C'), + (90, 'XC'), (50, 'L'), (40, 'XL'), (10, 'X'), From ccfc795ee0502939009ad4d5efa765ae87453f5b Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:24:03 +0700 Subject: [PATCH 29/34] RED: test convert 400 (CD), 500 (D), 900 (CM), 1000 (M) --- test/roman_numerals_test.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 6cc7f75..36b02c1 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -46,6 +46,10 @@ void main() { expect(roman_numerals.integerToRoman(40), 'XL'); }); + test('converts 49 to XLIX', () { + expect(roman_numerals.integerToRoman(49), 'XLIX'); + }); + test('converts 50 to L', () { expect(roman_numerals.integerToRoman(50), 'L'); }); @@ -57,4 +61,20 @@ void main() { test('converts 100 to C', () { expect(roman_numerals.integerToRoman(100), 'C'); }); + + test('converts 400 to CD', () { + expect(roman_numerals.integerToRoman(400), 'CD'); + }); + + test('converts 500 to D', () { + expect(roman_numerals.integerToRoman(500), 'D'); + }); + + test('converts 900 to CM', () { + expect(roman_numerals.integerToRoman(900), 'CM'); + }); + + test('converts 1000 to M', () { + expect(roman_numerals.integerToRoman(1000), 'M'); + }); } From 5bf2839e33b5687d98008c9b66cfdfff928a77ed Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:25:07 +0700 Subject: [PATCH 30/34] GREEN: add conversion rules for 400. 500. 900, and 1000 --- lib/roman_numerals.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index f2bb0d8..cac6eb6 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,5 +1,9 @@ String integerToRoman(int number) { const conversionRules = [ + (1000, 'M'), + (900, 'CM'), + (500, 'D'), + (400, 'CD'), (100, 'C'), (90, 'XC'), (50, 'L'), From 5dfc85fc2094e5052854044e76bdd268a5b44394 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:31:30 +0700 Subject: [PATCH 31/34] REFACTOR: organize tests into groups and add edge cases --- test/roman_numerals_test.dart | 134 ++++++++++++++++------------------ 1 file changed, 62 insertions(+), 72 deletions(-) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index 36b02c1..d9c8619 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -1,80 +1,70 @@ -import 'package:tdd_katas/roman_numerals.dart' as roman_numerals; +import 'package:tdd_katas/roman_numerals.dart'; import 'package:test/test.dart'; void main() { - test('converts 1 to I', () { - expect(roman_numerals.integerToRoman(1), 'I'); - }); + group('Roman Numerals Conversion', () { + group('Basic Symbols', () { + test('smallest unit', () => expect(integerToRoman(1), 'I')); + test('five', () => expect(integerToRoman(5), 'V')); + test('ten', () => expect(integerToRoman(10), 'X')); + test('fifty', () => expect(integerToRoman(50), 'L')); + test('hundred', () => expect(integerToRoman(100), 'C')); + test('five hundred', () => expect(integerToRoman(500), 'D')); + test('thousand', () => expect(integerToRoman(1000), 'M')); + }); - test('converts 2 to II', () { - expect(roman_numerals.integerToRoman(2), 'II'); - }); + group('Subtractive Notation', () { + test('four (one before five)', () => expect(integerToRoman(4), 'IV')); + test('nine (one before ten)', () => expect(integerToRoman(9), 'IX')); + test('forty (ten before fifty)', () => expect(integerToRoman(40), 'XL')); + test( + 'ninety (ten before hundred)', + () => expect(integerToRoman(90), 'XC'), + ); + test( + 'four hundred (hundred before five hundred)', + () => expect(integerToRoman(400), 'CD'), + ); + test( + 'nine hundred (hundred before thousand)', + () => expect(integerToRoman(900), 'CM'), + ); + }); - test('converts 3 to III', () { - expect(roman_numerals.integerToRoman(3), 'III'); - }); + group('Additive Combinations', () { + test('two (repeated symbol)', () => expect(integerToRoman(2), 'II')); + test( + 'three (maximum repetition)', + () => expect(integerToRoman(3), 'III'), + ); + test('six (additive after five)', () => expect(integerToRoman(6), 'VI')); + test( + 'twenty-seven (multiple symbols)', + () => expect(integerToRoman(27), 'XXVII'), + ); + }); - test('converts 4 to IV', () { - expect(roman_numerals.integerToRoman(4), 'IV'); - }); - - test('converts 5 to V', () { - expect(roman_numerals.integerToRoman(5), 'V'); - }); - - test('converts 6 to VI', () { - expect(roman_numerals.integerToRoman(6), 'VI'); - }); - - test('converts 7 to VII', () { - expect(roman_numerals.integerToRoman(7), 'VII'); - }); - - test('converts 8 to VIII', () { - expect(roman_numerals.integerToRoman(8), 'VIII'); - }); - - test('converts 9 to IX', () { - expect(roman_numerals.integerToRoman(9), 'IX'); - }); - - test('converts 10 to X', () { - expect(roman_numerals.integerToRoman(10), 'X'); - }); - - test('converts 40 to XL', () { - expect(roman_numerals.integerToRoman(40), 'XL'); - }); - - test('converts 49 to XLIX', () { - expect(roman_numerals.integerToRoman(49), 'XLIX'); - }); - - test('converts 50 to L', () { - expect(roman_numerals.integerToRoman(50), 'L'); - }); - - test('converts 90 to XC', () { - expect(roman_numerals.integerToRoman(90), 'XC'); - }); - - test('converts 100 to C', () { - expect(roman_numerals.integerToRoman(100), 'C'); - }); - - test('converts 400 to CD', () { - expect(roman_numerals.integerToRoman(400), 'CD'); - }); - - test('converts 500 to D', () { - expect(roman_numerals.integerToRoman(500), 'D'); - }); - - test('converts 900 to CM', () { - expect(roman_numerals.integerToRoman(900), 'CM'); - }); - - test('converts 1000 to M', () { - expect(roman_numerals.integerToRoman(1000), 'M'); + group('Complex Edge Cases', () { + test( + 'forty-nine (combines subtractive symbols)', + () => expect(integerToRoman(49), 'XLIX'), + ); + test( + 'ninety-nine (maximum two-digit complexity)', + () => expect(integerToRoman(99), 'XCIX'), + ); + test( + 'four hundred forty-four (all subtractive positions)', + () => expect(integerToRoman(444), 'CDXLIV'), + ); + test( + '1994 (year notation stress test)', + () => expect(integerToRoman(1994), 'MCMXCIV'), + ); + test( + '3999 (maximum valid Roman numeral)', + () => expect(integerToRoman(3999), 'MMMCMXCIX'), + ); + }); }); } From 216aa72d39b0ca49f1f17eeb1ca3e986fa908f6e Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:33:44 +0700 Subject: [PATCH 32/34] RED: test roman numerals contraints --- test/roman_numerals_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/roman_numerals_test.dart b/test/roman_numerals_test.dart index d9c8619..831aa9c 100644 --- a/test/roman_numerals_test.dart +++ b/test/roman_numerals_test.dart @@ -66,5 +66,19 @@ void main() { () => expect(integerToRoman(3999), 'MMMCMXCIX'), ); }); + + group('Error Handling', () { + test('rejects zero', () { + expect(() => integerToRoman(0), throwsArgumentError); + }); + + test('rejects negative numbers', () { + expect(() => integerToRoman(-5), throwsArgumentError); + }); + + test('rejects numbers above 3999', () { + expect(() => integerToRoman(4000), throwsArgumentError); + }); + }); }); } From 438221e2d76b10052975dc802e1bd5498d64c06a Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:34:43 +0700 Subject: [PATCH 33/34] GREEN: implement roman numerals contraints --- lib/roman_numerals.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index cac6eb6..0490bcb 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -15,6 +15,10 @@ String integerToRoman(int number) { (1, 'I'), ]; + if (number <= 0 || number > 3999) { + throw ArgumentError('Number must be between 1 and 3999'); + } + final result = StringBuffer(); var remaining = number; From 6e75926f7970e0e4d07e6a637e8a1fb9e6d21050 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 09:38:58 +0700 Subject: [PATCH 34/34] REFACTOR: split validation into value object --- lib/roman_numerals.dart | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/roman_numerals.dart b/lib/roman_numerals.dart index 0490bcb..1309230 100644 --- a/lib/roman_numerals.dart +++ b/lib/roman_numerals.dart @@ -1,4 +1,21 @@ +class RomanNumeralInput { + static const _minValue = 1; + static const _maxValue = 3999; + + final int value; + + RomanNumeralInput(this.value) { + if (value < _minValue || value > _maxValue) { + throw ArgumentError( + 'Roman numeral input must be between $_minValue and $_maxValue', + ); + } + } +} + String integerToRoman(int number) { + final input = RomanNumeralInput(number); + const conversionRules = [ (1000, 'M'), (900, 'CM'), @@ -15,12 +32,8 @@ String integerToRoman(int number) { (1, 'I'), ]; - if (number <= 0 || number > 3999) { - throw ArgumentError('Number must be between 1 and 3999'); - } - final result = StringBuffer(); - var remaining = number; + var remaining = input.value; for (final (value, symbol) in conversionRules) { while (remaining >= value) {