diff --git a/README.md b/README.md index e7c506b..803531d 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,99 @@ gildedRose.updateQuality(); // Updates all items per domain rules --- +### 4. String Calculator ✅ + +**Implementation:** [`lib/string_calculator.dart`](lib/string_calculator.dart) +**Tests:** [`test/string_calculator_test.dart`](test/string_calculator_test.dart) + +A Bug Hunt Kata demonstrating how to use TDD to discover and fix bugs in existing code. Each bug is exposed with a RED test, then fixed with GREEN implementation. + +#### Domain Context + +A simple calculator that sums numbers from a string input with various delimiter support: + +**Features:** + +1. **Empty String:** Returns 0 +2. **Single Number:** Returns that number (`"5"` → 5) +3. **Comma Delimiter:** Sums comma-separated numbers (`"1,2,3"` → 6) +4. **Custom Delimiters:** Supports format `"//[delimiter]\n[numbers]"` (`"//;\n1;2"` → 3) +5. **Ignore Large Numbers:** Numbers > 1000 are ignored (`"2,1001"` → 2) + +#### The Bug Hunt Approach + +**Different from Previous Katas:** This wasn't built test-first. Instead, we started with **buggy working code** and used tests to expose and fix bugs one by one. + +**Bug Hunt Process:** +1. **RED:** Write test exposing a specific bug +2. **GREEN:** Fix only that bug +3. **Commit:** Document the bug found and fixed +4. **Repeat:** Move to next bug + +#### Bugs Found & Fixed + +**Bug #1: Empty String Returns Wrong Value** +- **Bug:** Returned 1 instead of 0 +- **Test:** `expect(calculator.add(''), equals(0))` +- **Fix:** Changed return value from 1 to 0 +- **Commits:** RED → GREEN + +**Bug #2: Single Number Off-By-One** +- **Bug:** Added +1 to parsed number +- **Test:** `expect(calculator.add('5'), equals(5))` +- **Expected:** 5, **Actual:** 6 +- **Fix:** Removed `+ 1` from parsing +- **Commits:** RED → GREEN + +**Bug #3: Summation Loop Misses Last Element** +- **Bug:** Loop condition `i < length - 1` skipped last item +- **Test:** `expect(calculator.add('1,2'), equals(3))` +- **Expected:** 3, **Actual:** 1 +- **Fix:** Changed to `i < length` +- **Commits:** RED → GREEN + +**Bug #4: Custom Delimiter Not Extracted** +- **Bug:** Delimiter extraction line was commented out +- **Test:** `expect(calculator.add('//;\n1;2'), equals(3))` +- **Error:** FormatException trying to parse '1;2' +- **Fix:** Uncommented `delimiter = parts[0].substring(2)` +- **Commits:** RED → GREEN + +**Bug #5: Missing Feature - Ignore Numbers > 1000** +- **Bug:** All numbers included in sum +- **Test:** `expect(calculator.add('2,1001'), equals(2))` +- **Expected:** 2, **Actual:** 1003 +- **Fix:** Added `.where((n) => n <= 1000)` filter +- **Commits:** RED → GREEN + +#### Usage + +```dart +import 'package:tdd_katas/string_calculator.dart'; + +final calculator = StringCalculator(); + +calculator.add(''); // Returns: 0 +calculator.add('5'); // Returns: 5 +calculator.add('1,2,3'); // Returns: 6 +calculator.add('//;\n1;2'); // Returns: 3 +calculator.add('2,1001'); // Returns: 2 (1001 ignored) +``` + +#### The "Aha!" Moments + +1. **Tests as Bug Detectors:** Each test acted like a spotlight, illuminating exactly ONE bug at a time. No guessing—the test tells you what's broken. + +2. **RED-GREEN Still Works:** Even when fixing bugs (not adding features), the RED-GREEN rhythm provides safety. You're never fixing blind. + +3. **Regression Prevention:** After fixing each bug, ALL previous tests stay green. This proves you didn't break something while fixing something else. + +4. **Incremental Debugging:** Fixing one bug at a time with commits creates a clear audit trail. You can see exactly what each bug was and how it was fixed. + +5. **Real-World Skill:** This mirrors production work—most code you touch is existing code with bugs, not greenfield TDD. + +--- + ## Running Tests ```bash @@ -229,6 +322,7 @@ dart test dart test test/roman_numerals_test.dart dart test test/bowling_game_test.dart dart test test/gilded_rose_test.dart +dart test test/string_calculator_test.dart # Run with coverage dart test --coverage @@ -536,22 +630,23 @@ Decision: Skip refactor commit—code is already excellent. ## Comparing the Katas -### All Three Katas at a Glance +### All Four Katas at a Glance -| Aspect | Roman Numerals | Bowling Game | Gilded Rose | -|--------|----------------|--------------|-------------| -| **Complexity** | Beginner | Intermediate | Advanced | -| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | -| **State** | Stateless | Stateful | Stateful | -| **Algorithm** | Table-driven lookup | Frame iteration | Strategy pattern | -| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | -| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | -| **Lines of Code** | ~45 production | ~30 production | ~120 production | -| **Test Count** | ~15 tests | ~10 tests | ~17 tests | -| **Aha! Moment** | Table = data structure | Simple → complex works | Refactoring = safety | +| Aspect | Roman Numerals | Bowling Game | Gilded Rose | String Calculator | +|--------|----------------|--------------|-------------|-------------------| +| **Complexity** | Beginner | Intermediate | Advanced | Beginner | +| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | Bug hunting | +| **State** | Stateless | Stateful | Stateful | Stateless | +| **Algorithm** | Table-driven | Frame iteration | Strategy pattern | String parsing | +| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | Finding bugs | +| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | Filters & pipes | +| **Lines of Code** | ~45 production | ~30 production | ~120 production | ~25 production | +| **Test Count** | ~15 tests | ~10 tests | ~17 tests | 6 tests | +| **Aha! Moment** | Table = data | Simple → complex | Refactor = safe | Tests find bugs | ### What Each Kata Teaches +**Roman Numerals:** **Roman Numerals:** - Converting domain rules into data structures - Value Objects for enforcing constraints @@ -561,22 +656,29 @@ Decision: Skip refactor commit—code is already excellent. - State management without over-engineering - Look-ahead logic in sequential data - How correct abstractions scale beyond test cases + **Gilded Rose:** - Safely refactoring legacy code with characterization tests - Strategy pattern for eliminating conditional complexity - Open-Closed Principle for extensibility - Working effectively with code you didn't write +**String Calculator:** +- Using tests to expose bugs in existing code +- Bug hunting with RED-GREEN discipline +- Incremental debugging with clear commits +- Regression prevention through test accumulation + ### Progressive Learning Path 1. **Roman Numerals first:** Learn TDD fundamentals without state complexity 2. **Bowling Game second:** Apply TDD to stateful problems 3. **Gilded Rose third:** Master refactoring legacy code with tests as safety net -4. **Next kata:** Choose based on what you want to practice: - - **String Calculator:** Parsing, validation, error handling - - **Mars Rover:** Command pattern, multiple behaviors - - **Prime Factors:** Mathematical decomposition, algorithmic thinkingsts +4. **String Calculator fourth:** Practice bug hunting and fixing with TDD +5. **Next kata:** Choose based on what you want to practice: - **Mars Rover:** Command pattern, multiple behaviors + - **Prime Factors:** Mathematical decomposition, algorithmic thinking + - **Tennis Scoring:** State machines, domain language --- diff --git a/bin/tdd_katas.dart b/bin/tdd_katas.dart index 987a845..24f0e29 100644 --- a/bin/tdd_katas.dart +++ b/bin/tdd_katas.dart @@ -1,5 +1,3 @@ -import 'package:tdd_katas/roman_numerals.dart' as roman_numerals; - void main(List arguments) { - print('Hello world: ${roman_numerals.integerToRoman(1)}'); + print('TDD Katas exercises, please read the README.md file.'); } diff --git a/lib/string_calculator.dart b/lib/string_calculator.dart new file mode 100644 index 0000000..6e5ec31 --- /dev/null +++ b/lib/string_calculator.dart @@ -0,0 +1,55 @@ +/// String Calculator - Buggy Implementation +/// This code has intentional bugs for the Bug Hunt Kata exercise +/// +/// Requirements: +/// 1. Empty string returns 0 +/// 2. Single number returns that number +/// 3. Two numbers comma-delimited returns sum +/// 4. Handle newlines as delimiters +/// 5. Support custom delimiters: "//[delimiter]\n[numbers]" +library; + +class StringCalculator { + int add(String numbers) { + // Bug 1: Empty string handling + if (numbers.isEmpty) { + return 0; // Fixed: Return 0 for empty string + } + + // Bug 2: Single number parsing + if (!numbers.contains(',') && + !numbers.contains('\n') && + !numbers.startsWith('//')) { + return int.parse(numbers); // Fixed: Removed off-by-one error + } + + String delimiter = ','; + String numbersToProcess = numbers; + + // Custom delimiter support + if (numbers.startsWith('//')) { + // Bug 4: Custom delimiter not actually used + final parts = numbers.split('\n'); + delimiter = parts[0].substring(2); // Fixed: Extract custom delimiter + numbersToProcess = parts.skip(1).join('\n'); + } + + // Bug 3 & 4: Delimiter handling issues + final numList = numbersToProcess + .replaceAll('\n', delimiter) + .split(delimiter) + .where((s) => s.isNotEmpty) + .map((s) => int.parse(s)) + .where((n) => n <= 1000) // Filter out numbers > 1000 + .toList(); + + // Bug 3: Off-by-one in summation + int sum = 0; + for (int i = 0; i < numList.length; i++) { + // Fixed: Include last element + sum += numList[i]; + } + + return sum; + } +} diff --git a/test/string_calculator_test.dart b/test/string_calculator_test.dart new file mode 100644 index 0000000..1d4ee44 --- /dev/null +++ b/test/string_calculator_test.dart @@ -0,0 +1,41 @@ +import 'package:tdd_katas/string_calculator.dart'; +import 'package:test/test.dart'; + +void main() { + group('String Calculator - Bug Hunt', () { + late StringCalculator calculator; + + setUp(() { + calculator = StringCalculator(); + }); + + test('empty string returns 0', () { + expect(calculator.add(''), equals(0)); + }); + + test('single number returns that number', () { + expect(calculator.add('5'), equals(5)); + expect(calculator.add('42'), equals(42)); + }); + + test('two comma-delimited numbers return sum', () { + expect(calculator.add('1,2'), equals(3)); + expect(calculator.add('10,20'), equals(30)); + }); + + test('multiple comma-delimited numbers return sum', () { + expect(calculator.add('1,2,3'), equals(6)); + expect(calculator.add('5,10,15,20'), equals(50)); + }); + + test('custom delimiter works correctly', () { + expect(calculator.add('//;\n1;2'), equals(3)); + expect(calculator.add('//|\n10|20|30'), equals(60)); + }); + + test('numbers greater than 1000 are ignored', () { + expect(calculator.add('2,1001'), equals(2)); + expect(calculator.add('1000,1001,2'), equals(1002)); + }); + }); +}