This is my first TDD Kata I've done and I'd appreciate it if someone could review it.
String Calculator
- Create a simple String calculator with a method int Add(string numbers)
- The method can take 0, 1 or 2 numbers, and will return their sum (for an empty string it will return 0) for example “” or “1” or “1,2”
- Start with the simplest test case of an empty string and move to 1 and two numbers
- Remember to solve things as simply as possible so that you force yourself to write tests you did not think about
- Remember to refactor after each passing test
- Allow the Add method to handle an unknown amount of numbers
- Allow the Add method to handle new lines between numbers (instead of commas).
- the following input is ok: “1\n2,3” (will equal 6)
- the following input is NOT ok: “1,\n” (not need to prove it - just clarifying)
- Support different delimiters
- to change a delimiter, the beginning of the string will contain a separate line that looks like this: “//[delimiter]\n[numbers…]” for example “//;\n1;2” should return three where the default delimiter is ‘;’.
- the first line is optional. all existing scenarios should still be supported
- Calling Add with a negative number will throw an > exception “negatives not allowed” - and the negative that was passed.if there are multiple negatives, show all of them in the exception message
- Numbers bigger than 1000 should be ignored, so adding 2 + 1001 = 2
- Delimiters can be of any length with the following format: “//[delimiter]\n” for example: “//[***]\n1***2***3” should return 6
- Allow multiple delimiters like this: “//[delim1][delim2]\n” for example “//[*][%]\n1*2%3” should return 6.
- make sure you can also handle multiple delimiters with length longer than one char
Unit Tests:
[TestFixture]
public class StringCalculatorTests
{
private StringCalculator _underTest;
[SetUp]
public void SetUp()
{
_underTest = new StringCalculator();
}
private int Act_CalculateNumbers(string numbers)
{
var calculatedResult = _underTest.Add(numbers);
return calculatedResult;
}
[Test]
[TestCase("", ExpectedResult = 0)]
[TestCase(null, ExpectedResult = 0)]
public int Returns_0_When_Null_Or_EmptyString_Input(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("1", ExpectedResult = 1)]
[TestCase("100", ExpectedResult = 100)]
public int Returns_SameNumber_When_ValidNumber_Input(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("1,2", ExpectedResult = 3)]
[TestCase("11,12", ExpectedResult = 23)]
public int Returns_SumOf_TwoNumbers_When_Two_ValidNumbers_Input(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("1,2,3,4", ExpectedResult = 10)]
[TestCase("11,12,13,14", ExpectedResult = 50)]
public int Returns_SumOf_Numbers_When_Multiple_ValidNumbers_Input(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("1\n2", ExpectedResult = 3)]
[TestCase("1\n14", ExpectedResult = 15)]
[TestCase("1\n1,4", ExpectedResult = 6)]
[TestCase("1\n5,4", ExpectedResult = 10)]
public int Returns_CorectSum_When_NewLine_Delimiter_Used(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("//$\n1", ExpectedResult = 1)]
[TestCase("//$\n1$2", ExpectedResult = 3)]
[TestCase("//$\n1$2,3", ExpectedResult = 6)]
[TestCase("//$\n1$2,3\n4", ExpectedResult = 10)]
[TestCase("//$\n1$2,3\n4$5", ExpectedResult = 15)]
public int Returns_CorectSum_When_Custom_Delimiter_Used(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("-1", "-1")]
[TestCase("1,-1", "-1")]
[TestCase("1\n-1", "-1")]
[TestCase("//$\n-1", "-1")]
[TestCase("//$\n1$-2", "-2")]
[TestCase("//$\n1$-2,3", "-2")]
[TestCase("//$\n1$-2,-3\n4", "-2,-3")]
[TestCase("//$\n1$2,3\n4$-5", "-5")]
public void Throws_Correct_Exception_When_NegativeNumber_Input(string numbers, string negativeNumbers)
{
//Act
var exception = Should.Throw<FormatException>(() => Act_CalculateNumbers(numbers));
//Assert
exception.Message.ShouldBe($"negatives not allowed '{negativeNumbers}'");
}
[Test]
[TestCase("1001", ExpectedResult = 0)]
[TestCase("1,1001", ExpectedResult = 1)]
[TestCase("1\n1001", ExpectedResult = 1)]
[TestCase("//$\n1,1001", ExpectedResult = 1)]
[TestCase("//$\n1$1001", ExpectedResult = 1)]
[TestCase("//$\n1$2,1001", ExpectedResult = 3)]
public int Returns_CorrectSum_When_Ignoring_Numbers_Greater_Than_1000(string numbers)
{
return Act_CalculateNumbers(numbers);
}
[Test]
[TestCase("//[$$][££]\n1££1", ExpectedResult = 2)]
[TestCase("//[$$][££]\n1$$1££1", ExpectedResult = 3)]
[TestCase("//[$$][££]\n1$$1,1££1", ExpectedResult = 4)]
[TestCase("//[$$$][£££]\n1$$$1,1\n1£££1", ExpectedResult = 5)]
public int Returns_CorrectSum_With_Custom_Delimiters_Of_Any_Length(string numbers)
{
return Act_CalculateNumbers(numbers);
}
}
Implementation:
public class StringCalculator
{
private readonly List<string> _defaultDelimiters = new List<string> { ",", "\n" };
private const int StartIndexOfNumbersWithCustomDelimiter = 3;
private const int StartIndexOfCustomDelimiter = 2;
private const int MaxNumberLimit = 1000;
private const string CustomDelimiterIdentifier = "//";
public int Add(string numbers)
{
if (string.IsNullOrEmpty(numbers)) return 0;
if (numbers.StartsWith(CustomDelimiterIdentifier))
{
numbers = GetNumbersExcludingCustomDelimiter(numbers);
}
var sumOfNumbers = GetSumOfNumbers(numbers);
return sumOfNumbers;
}
private int GetSumOfNumbers(string numbers)
{
var convertedNumbers = numbers.Split(_defaultDelimiters.ToArray(), StringSplitOptions.None).Select(int.Parse).ToList();
ValidateNumbersArePositive(convertedNumbers);
var sumOfNumbers = convertedNumbers.Where(x => x <= MaxNumberLimit).Sum();
return sumOfNumbers;
}
private string GetNumbersExcludingCustomDelimiter(string numbers)
{
var startIndexOfString = AssignCustomDelimiterAndReturnStartIndexOfNumbers(numbers);
numbers = numbers.Substring(startIndexOfString);
return numbers;
}
private int AssignCustomDelimiterAndReturnStartIndexOfNumbers(string numbers)
{
var customDelimiters = GetCustomDelimiter(numbers);
_defaultDelimiters.AddRange(customDelimiters);
var hasMultipleDelimiters = customDelimiters.Count > 1;
var multipleDelimiterLength = hasMultipleDelimiters ? (customDelimiters.Count * 2) : 0;
return StartIndexOfNumbersWithCustomDelimiter + customDelimiters.Sum(x => x.Length) + multipleDelimiterLength;
}
private static void ValidateNumbersArePositive(IReadOnlyCollection<int> convertedNumbers)
{
if (!convertedNumbers.Any(x => x < 0)) return;
var negativeNumbers = string.Join(",", convertedNumbers.Where(x => x < 0).Select(x => x.ToString()).ToArray());
throw new FormatException($"negatives not allowed '{negativeNumbers}'");
}
private static IList<string> GetCustomDelimiter(string numbers)
{
var allDelimiters = numbers.Substring(StartIndexOfCustomDelimiter, numbers.IndexOf('\n') - StartIndexOfCustomDelimiter);
var splitDelimiters = allDelimiters.Split('[').Select(x => x.TrimEnd(']')).ToList();
if (splitDelimiters.Contains(string.Empty))
{
splitDelimiters.Remove(string.Empty);
}
return splitDelimiters;
}
}