Wednesday, September 11, 2013

The "Calculator Kata" challenge

Calculator Kata

The calculator kata made by Roy Osherove is an exercise in test driven development and goes like this:

  • Try not to read ahead.
  • Do one task at a time. The trick is to learn to work incrementally.
  • Make sure you only test for correct inputs. there is no need to test for invalid inputs for this kata

Create a simple String calculator with a method int Add(string numbers)

  1. 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
  2. Allow the Add method to handle an unknown amount of numbers
  3. 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)
  4. 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
  5. 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
    stop here if you are a beginner. Continue if you can finish the steps so far in less than 30 minutes.
  6. Numbers bigger than 1000 should be ignored, so adding 2 + 1001 = 2
  7. Delimiters can be of any length with the following format: “//[delimiter]\n” for example: “//[]\n12*3” should return 6
  8. Allow multiple delimiters like this: “//[delim1][delim2]\n” for example “//[][%]\n12%3” should return 6.
    make sure you can also handle multiple delimiters with length longer than one char

With nothing better to do this afternoon I decided to to give it a try. This is what I came up with in the first version.

The tests:

   [TestMethod]
    public void Add_EmptyString_ReturnsZero()
    {
        var result = CreateCalculator().Add(string.Empty);

        Assert.AreEqual(0, result);
    }

    [TestMethod]
    public void Add_SingleNumber_ReturnsNumber()
    {
        var result = CreateCalculator().Add("1");

        Assert.AreEqual(1, result);
    }

    [TestMethod]
    public void Add_TwoNumbers_ReturnsSum()
    {
        var result = CreateCalculator().Add("12,2");

        Assert.AreEqual(14, result);
    }

    [TestMethod]
    public void Add_MultipleNumbers_ReturnsSum()
    {
        var result = CreateCalculator().Add("1,2,3");

        Assert.AreEqual(6, result);
    }

    [TestMethod]
    public void Add_UsingNewLineDeliminator_ReturnsSum()
    {
        var result = CreateCalculator().Add("1\n2,3");

        Assert.AreEqual(6, result);
    }

    [TestMethod]
    public void Add_CustomDeliminator_ReturnsSum()
    {
        var result = CreateCalculator().Add("//;\n1;2");

        Assert.AreEqual(3, result);
    }

    [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
    public void Add_NegativeValue_ThrowsException()
    {
        CreateCalculator().Add("-1");            
    }

    [TestMethod]
    public void Add_NumbersLargerThan1000_ReturnsSum()
    {
        var result = CreateCalculator().Add("2,1001");

        Assert.AreEqual(2, result);
    }

    [TestMethod]
    public void Add_LongDelimiter_ReturnsSum()
    {
        var result = CreateCalculator().Add("//[***]\n1***2***3");

        Assert.AreEqual(6, result);
    }

    [TestMethod]
    public void Add_MultipleDelimiters_ReturnsSum()
    {
        var result = CreateCalculator().Add("//[*][%]\n1*2%3");

        Assert.AreEqual(6, result);
    }

    [TestMethod]
    public void Add_MultipleLongDelimiters_ReturnsSum()
    {
        var result = CreateCalculator().Add("//[***][%%%]\n1***2%%%3");

        Assert.AreEqual(6, result);
    }

And the implementation looked something like this:

public interface ICalculator
{
    int Add(string input);
}

public class Calculator : ICalculator
{
    public int Add(string input)
    {            
        if (HasCustomDelimiter(input))
        {
            string delimiters = StripOffNumbers(input);
            input = StripOffDelimiterMetadata(input);                
            return AddNumbers(input, GetDelimiters(delimiters));
        }

        return AddNumbers(input, GetDelimiters(string.Empty));
    }

    private string StripOffNumbers(string input)
    {
        return input.Substring(0, input.IndexOf('\n')).Substring(2);
    }

    private int AddNumbers(string input, string[] delimiters)
    {
        if (input.Length == 0)
        {
            return 0;
        }

        return SplitNumbers(input, delimiters).Select(ParseNumber).Where(n => n < 1000).Sum();
    }

    private static int ParseNumber(string s)
    {
        int result = int.Parse(s);
        if (result < 0)
        {
            throw new ArgumentOutOfRangeException();
        }

        return result;
    }

    private string[] SplitNumbers(string input, string[] delimiters)
    {
        return input.Split(delimiters, StringSplitOptions.None);
    }

    private string StripOffDelimiterMetadata(string input)
    {
        if (HasCustomDelimiter(input))
        {
            return input.Substring(input.IndexOf('\n') + 1);
        }

        return input;
    }

    private static bool HasCustomDelimiter(string input)
    {
        return input.StartsWith("//");
    }

    private string[] GetDelimiters(string customDelimiters)
    {
        var delimiters = new List<string> { ",", "\n" };
        if (string.IsNullOrEmpty(customDelimiters))
        {
            return delimiters.ToArray();
        }

        if (customDelimiters.StartsWith("["))
        {
            var longDelimiters = customDelimiters.Replace("[", "]").Split(new[] { ']' }, StringSplitOptions.RemoveEmptyEntries);
            delimiters.AddRange(longDelimiters);
            return delimiters.ToArray();
        }

        delimiters.Add(customDelimiters[0].ToString());
        return delimiters.ToArray();
    }

} 

Reading through the code we can see that there are a lot of string searching,splitting and even replacements.

But it works and all tests passed. Great!!

The challenge

I presented this to a colleague of mine and he said that this is all fine, but he was still waiting to see the stream version. Stream version???...oh wait, you mean that I should read the input string character by character and never read the same character twice.

The description clearly states that no effort should be done to validate for invalid input apart from not accepting negative numbers.

The first thing to do is to forget all about finding custom delimiters and splitting strings, just follow this simple rule:

Read the string from start to end and ignore everything that is not a numeric value.

This is what I ended up with.

public class Calculator : ICalculator
{
    public int Add(string input)
    {
        double sum = 0;            
        double number = 0;
        foreach (var character in input)
        {                
            double numericValue = Char.GetNumericValue(character);                
            if (numericValue != -1.0)
            {                   
                if (number > 0)
                {
                    number = number * 10;
                }                                                
                number = number + numericValue;                   
            }                                
            else
            {                 
                sum = sum + (number >= 1000 ? 0 : number);
                if (character == '-')
                {
                    throw new ArgumentOutOfRangeException();
                }

                number = 0;
            }                
        }

        return (int)sum + (int)(number >= 1000 ? 0 : number);            
    }
}  

All tests are still green.

Just goes to show that there are more than one approach to any given problem :)

Tuesday, September 10, 2013

OSVersion

MarkdownPad Document

Just recently I was involved in a discussion about logging and the need to include the name of the operation system and the service pack level in the application log file.

While you can get to this information in a number of ways, it is important that it will work as expected even for future versions of Windows.

The following code shows a console application that displays the name of the operating system along with the current service pack level.

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Operating System : {0} ({1} bit) ", GetOperationSystemName(), IntPtr.Size == 4 ? "32" : "64");
        Console.WriteLine("Service Pack : {0}", Environment.OSVersion.ServicePack);
    }

    private static string GetOperationSystemName()
    {
        var searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem");
        var managementObject = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
        return managementObject != null ? managementObject["Caption"].ToString() : "Unknown OS";
    }
}

Running the console application displays the following:

.\OSVersion.exe

Operating System : Microsoft Windows 7 Enterprise  (64 bit)
Service Pack : Service Pack 1