Roman numbers kata

This exercise is from vNext_OC mob programming session today. The goal of this kata is to convert a roman number (XLVII) to a decimal number (48). In the spirit of TDD I will start with a test for the number one and the simplest possible code that satisfies that test:

[TestMethod]
public void I()
{
    Assert.AreEqual(1, Roman.ToDecimal("I"));
}

public class Roman
{
    public static int ToDecimal(string roman)
    {
        return 1;
    }
}

The second test I’ll write is for the number two:

[TestMethod]
public void II()
{
    Assert.AreEqual(2, Roman.ToDecimal("II"));
}

That test fails. One possible solution is to return the length of the passed string:

public class Roman
{
    public static int ToDecimal(string roman)
    {
        return roman.Length;
    }
}

Test succeeds. Next test – three. That succeeds as well. Next one – four. Fails. I don’t quite know what to do with four yet so I’ll do the simplest possible thing – will explicitly check for IV:

public class Roman
{
    public static int ToDecimal(string roman)
    {
        if (roman = "IV") return 4;

        return roman.Length;
    }
}

Green. Next one – five. I have 2 different digits now. What I’ll do is I’ll assign value 1 to I, value 5 to V and will add those values:

public class Roman
{
    public static int ToDecimal(string roman)
    {
        if (roman = "IV") return 4;

        int result = 0;

        foreach (var c in roman)
        {
            switch(c)
            {
                case "I": 
                    result += 1;
                    break;
                case "V":
                    result += 5;
                    break;
            }
        }

        return result;
    }
}

All tests are green again. VI, VII, VIII are green too.

Now is a good time to introduce ApprovalTests. I’ll install the package from nuget and I’ll write a test like this:

[TestMethod]
public void TestAll()
{
    var romans = new[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X" };
    var results = romans.Select(r => new { roman = r, number = Roman.ToDecimal(r) });
    Approvals.VerifyAll(results, "");
}

That test will fail but will bring a file that looks like this:

[0] => { roman: "I", number: 1 }
[1] => { roman: "II", number: 2 }
[2] => { roman: "III", number: 3 }
[3] => { roman: "IV", number: 4 }
[4] => { roman: "V", number: 5 }
[5] => { roman: "VI", number: 6 }
[6] => { roman: "VII", number: 7 }
[7] => { roman: "VIII", number: 8 }
[8] => { roman: "IX", number: 1 }
[9] => { roman: "X", number: 0 }

I will fix the last two results of to be 9 and 10 and save the approved “gold master”. Test fails again (as expected) but we can replace all previous tests with just one more powerful and more visual test. To satisfy the test I’ll add another “exception” for IX and I’ll add a value for the digit 10.

public class Roman
{
    public static int ToDecimal(string roman)
    {
        if (roman = "IV") return 4;
        if (roman = "IX") return 9;

        int result = 0;

        foreach (var c in roman)
        {
            switch(c)
            {
                case "I": 
                    result += 1;
                    break;
                case "V":
                    result += 5;
                    break;
                case "X":
                    result += 10;
                    break;
            }
        }

        return result;
    }
}

Green. I’ll refactor the code now to include two dictionaries for the digits and the exceptions:

public class Roman
{
    static var exceptions = new Dictionary() { { "IV", 4 }, { "IX", 9 } }; 

    static var digits = new Dictionary() { { 'I', 1 }, { 'V', 5 }, { 'X', 10 } };

    public static int ToDecimal(string roman)
    {
        int result = 0;

        foreach (var x in exceptions)
        {
            if (roman.Contains(x.Key))
            {
                result += x.Value;
                roman = roman.Replace(x.Key, "");
            }
        }

        foreach (var c in roman)
        {
            if (digits.ContainsKey(c))
            {
                result += digits[c];
            }
        }

        return result;
    }
}

Run the test – all green. Now I’ll add the numbers from XI to XX to the test. First time it will fail. I’ll approve the result (since it is correct) and run the test again. Green. Repeat with XXI to XXX. Red, approve, green. XXXI to XL – all looks good except the number 40. I’ll manually edit the result file and approve it. Run the test again – red. To fix the code I need to add XL to the list of exceptions. Green. XL to L – to make the test pass need to add L to the digits. Then we are good all the way to XC. Adding XC to the exceptions and C to the digits and we have all the numbers to 100 working.

I’ll stop here. There is room to refactor but this is a good demonstration of the power of TDD. Without sitting down and designing the one algorithm that solves all roman numbers, in half an hour we have a very simple code that solves the problem for all numbers up to 100. Good enough to translate the number of the superbowl (XLVIII) that is coming next weekend.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s