Created
February 15, 2012 11:04
-
-
Save skalinets/1835067 to your computer and use it in GitHub Desktop.
StringCalculator Kata (no regex, very close to ideal clean code -- for me)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Linq; | |
using System.Text; | |
using FluentAssertions; | |
using Xunit; | |
namespace StringCalculatorKata | |
{ | |
public class StringCalculatorTests | |
{ | |
[Fact] | |
public void should_return_0_for_empty_string() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add(""); | |
// assert | |
result | |
.Should() | |
.Be(0); | |
} | |
[Fact] | |
public void should_return_5_for_5() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add("5"); | |
// assert | |
result | |
.Should() | |
.Be(5); | |
} | |
[Fact] | |
public void should_return_6_for_2_and_4() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add("2,4"); | |
// assert | |
result | |
.Should() | |
.Be(6); | |
} | |
[Fact] | |
public void should_return_sum_for_unknown_amount_of_numbers() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var unknownNumbers = GetUnknownNumbers(); | |
var unknownNumbersString = String.Join(",", unknownNumbers); | |
var result = calculator.Add(unknownNumbersString); | |
// assert | |
var expectedResult = unknownNumbers.Sum(); | |
result | |
.Should() | |
.Be(expectedResult); | |
} | |
[Fact] | |
public void random_numbers_should_be_random() | |
{ | |
GetUnknownNumbers().Should().NotEqual(GetUnknownNumbers()); | |
} | |
[Fact] | |
public void should_support_new_line_as_delimiter() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add("1\n2,3"); | |
// assert | |
result | |
.Should() | |
.Be(1 + 2 + 3); | |
} | |
[Fact] | |
public void should_allow_to_define_custom_delimiter() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add("//*\n2*5*4"); | |
// assert | |
result | |
.Should() | |
.Be(2 + 5 + 4); | |
} | |
[Fact] | |
public void should_raise_exception_for_negative_numbers_and_include_numbers_to_message() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
Action action = () => calculator.Add("-1,-5,-9,3,4"); | |
// assert | |
action | |
.ShouldThrow<InvalidOperationException>() | |
.WithMessage(StringCalculator.NegativesAreNotAllowedError + "-1,-5,-9"); | |
} | |
[Fact] | |
public void should_ignore_numbers_bigger_than_1000() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
int result = calculator.Add("1001,2"); | |
// assert | |
result | |
.Should() | |
.Be(2); | |
} | |
[Fact] | |
public void should_support_delimiters_of_any_length() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add("//[&@#]\n2&@#7&@#8&@#2"); | |
// assert | |
result | |
.Should() | |
.Be(2 + 7 + 8 + 2); | |
} | |
[Fact] | |
public void should_support_muliple_delimiters_of_any_length() | |
{ | |
// arrange | |
var calculator = new StringCalculator(); | |
// act | |
var result = calculator.Add("//[^%][(*]\n2^%3^%7(*4(*5"); | |
// assert | |
result | |
.Should() | |
.Be(2 + 3 + 7 + 4 + 5); | |
} | |
private int[] GetUnknownNumbers() | |
{ | |
var random = new Random(Guid.NewGuid().GetHashCode()); | |
var number = random.Next(100); | |
return Enumerable.Range(0, number) | |
.Select(_ => random.Next(100)) | |
.ToArray(); | |
} | |
} | |
public class StringCalculator | |
{ | |
public const string NegativesAreNotAllowedError = "negatives are not allowed: "; | |
private string[] delimiters = new[] {",", "\n"}; | |
private int[] ints; | |
private string numbers; | |
public int Add(string numbers) | |
{ | |
if (numbers == "") return 0; | |
this.numbers = numbers; | |
ProcessDelimiters(); | |
SplitNumbersToInts(); | |
CheckIntsForNegatives(); | |
return ints.Sum(); | |
} | |
private void SplitNumbersToInts() | |
{ | |
ints = numbers | |
.Split(delimiters, StringSplitOptions.None) | |
.Select(Int32.Parse) | |
.Where(_ => _ <= 1000) | |
.ToArray(); | |
} | |
private void CheckIntsForNegatives() | |
{ | |
var negatives = ints.Where(i => i < 0).ToArray(); | |
if (negatives.Any()) | |
{ | |
throw new InvalidOperationException(NegativesAreNotAllowedError + String.Join(",", negatives)); | |
} | |
} | |
private void ProcessDelimiters() | |
{ | |
var customDelimitersSpecified = numbers.StartsWith("//"); | |
if (customDelimitersSpecified) | |
{ | |
var multiCharDelimitersSpecified = numbers[2] == '['; | |
delimiters = multiCharDelimitersSpecified ? GetAllMultiCharCustomDelimiters() : GetSingleCharCustomDelimiter(); | |
RemoveDelimitersFromNumbers(); | |
} | |
} | |
private string[] GetSingleCharCustomDelimiter() | |
{ | |
return new[] {numbers[2].ToString(CultureInfo.InvariantCulture)}; | |
} | |
private string[] GetAllMultiCharCustomDelimiters() | |
{ | |
var result = new List<string>(); | |
numbers.TakeWhile(c => c != '\n') | |
.Aggregate(new StringBuilder(), (builder, c) => DoMagic(result, c, builder)); | |
return result.ToArray(); | |
} | |
private static StringBuilder DoMagic(ICollection<string> list, char currentChar, StringBuilder stringBuilder) | |
{ | |
if (currentChar == '[') | |
{ | |
stringBuilder.Clear(); | |
} | |
else if (currentChar == ']') | |
{ | |
list.Add(stringBuilder.ToString()); | |
} | |
else | |
{ | |
stringBuilder.Append(currentChar); | |
} | |
return stringBuilder; | |
} | |
private void RemoveDelimitersFromNumbers() | |
{ | |
numbers = String.Join("", numbers | |
.SkipWhile(c => c != '\n')); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment