Merge pull request GH-4 from gh/string-calculator
string calculator
This commit is contained in:
commit
5b34454317
4 changed files with 215 additions and 19 deletions
134
README.md
134
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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:tdd_katas/roman_numerals.dart' as roman_numerals;
|
||||
|
||||
void main(List<String> arguments) {
|
||||
print('Hello world: ${roman_numerals.integerToRoman(1)}');
|
||||
print('TDD Katas exercises, please read the README.md file.');
|
||||
}
|
||||
|
|
|
|||
55
lib/string_calculator.dart
Normal file
55
lib/string_calculator.dart
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
41
test/string_calculator_test.dart
Normal file
41
test/string_calculator_test.dart
Normal file
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue