By kubotake [CC BY 2.0 (http://creativecommons.org/licenses/by/2.0)], via Wikimedia Commons

the problem: the diamond kata

I use Seb Rose description as it is simple and efficient.

Given a letter, print a diamond starting with ‘A’ with the supplied letter at the widest point.

For example: print-diamond ‘C’ prints

  A
 B B
C   C
 B B
  A

I heard a lot about this kata. Recently1, it was the main subject of a two hours workshop at the awesome NCrafts conference. So long time after the initial battle2, I decided to give it a try.

my solution

the idea

Well, as usual, when I’m told that something is difficult or impossible, I just don’t care and do it as I will have if no one told me anything. So I apply TDD as I usually do: red, green, refactor. And that’s all.

Write a test, make it pass, clean the code. One step at a time.

the first test

So let’s start with the first test.

public class DiamondTest {
    @Test public void 
    should_draw_diamond_A() {
        assertEquals("A", Diamond.create('A'));
    }
}

Test is Red, it’s time to make it go to green:

public class Diamond {

    public static String create(Character c) {
        return "A";
    }   
}

Yep, that’s a stupid implementation. But it works :)

And quite clean by itself. So let’s add another test.

a second test

    @Test public void 
    should_draw_diamond_B() {
        assertEquals(" A \nB B\n A ", Diamond.create('B'));
    }

And make it pass stupidly.

    public static String create(Character c) {
        if (c.equals('B')) {
            return " A \n"
                 + "B B\n"
                 + " A ";
        }
        
        return "A";
    }       

And it’s green, so it’s time to refactor. But I can’t see duplication yet.

Well, there are twice the same “ A “ line in the ‘B’ diamond. But I can’t give a meaning to it. I probably don’t have enough data.

So, I’ll keep things like that for now, and add a third test.

and the third

@Test public void 
should_draw_diamond_C() {
    assertEquals("  A  \n"
               + " B B \n"
               + "C   C\n"
               + " B B \n"
               + "  A  ", Diamond.create('C'));
}

As I try to be coherent, my way to make it green won’t differ from the previous one :)

public static String create(Character c) {
    if (c.equals('C')) {
        return "  A  \n"
             + " B B \n"
             + "C   C\n"
             + " B B \n"
             + "  A  ";
    }

    if (c.equals('B')) {
        return " A \n"
             + "B B\n"
             + " A ";
    }
    
    return "A";
}

Yep, again that is a stupid implementation. Because at this moment I don’t care being clever, doing nice, clean things. At this moment I just want to be sure that the test I wrote is the one I intended to write.

By making it goes green with a simple solution, there are very few risks I introduced a mistake in the implementation. So when I see the test going from red to green, I can be confident on my test.

That’s my way to test my tests.

So now, I may have enough materials to see if I can make some interesting things appear.

time to start refactoring

There should be some logic in the letters and spaces distribution. As I don’t understand it yet, I will artificially separate them. Yes, I’m going to create kind of artificial duplication.

public static String create(Character c) {
    if (c.equals('C')) {
        return "  " +        "A"  +       "  " + "\n"
             +  " " + "B" +  " "  + "B" + " "  + "\n"
             +   "" + "C" + "   " + "C" + ""   + "\n"
             +  " " + "B" +  " "  + "B" + " "  + "\n"
             + "  " +        "A"  +       "  ";
    }
    
    if (c.equals('B')) {
        return " " + "A" + " " + "\n"
             + "B" + " " + "B" + "\n"
             + " " + "A" + " ";
    }
    
    return "A";
}

Now I can notice that first and last lines are z spaces - A - z spaces. If a diamond ‘A’ has a size of 1, ‘B’ of 2, ‘C’ 3 and so on, then z = (size - 1)

Great, lets put that logic in a method.

public static String create(Character c) {
    int size = c - 'A' + 1;

    if (c.equals('C')) {
        return diamondTip(size) + "\n"
             +  " " + "B" +  " "  + "B" + " "  + "\n"
             +   "" + "C" + "   " + "C" + ""   + "\n"
             +  " " + "B" +  " "  + "B" + " "  + "\n"
             + diamondTip(size);
    }
    
    if (c.equals('B')) {
        return diamondTip(size) + "\n"
             + "B" + " " + "B" + "\n"
             + diamondTip(size);
    }
    
    return "A";
}

private static String diamondTip(int size) {
    return manySpaces(size -1) + "A" + manySpaces(size - 1);
}

private static String manySpaces(int nbSpaces) {
    StringBuilder builder = new StringBuilder();
    
    for (int i = 0; i < nbSpaces; i++) {
        builder.append(" ");
    }
    return builder.toString();
}

I also notice that other lines follow the same pattern : x spaces - a char - y spaces - same char - x spaces So lets make that appear in the code.

public static String create(Character c) {
    ...
    if (c.equals('C')) {
        return diamondTip(size) + "\n"
             +  manySpaces(1) + "B" + manySpaces(1) + "B" + manySpaces(1) + "\n"
             +  manySpaces(0) + "C" + manySpaces(3) + "C" + manySpaces(0) + "\n"
             +  manySpaces(1) + "B" + manySpaces(1) + "B" + manySpaces(1) + "\n"
             + diamondTip(size);
    }

    if (c.equals('B')) {
        return diamondTip(size) + "\n"
             + "B" + manySpaces(1) + "B" + "\n"
             + diamondTip(size);
    }
    ...
}

There is definitely something that tie everything, but i don’t see it yet.

a fourth test

Let’s add the ‘D’ diamond. I won’t show you the test, it the same than previous but with a ‘D’ diamond. Make it pass by adding this in our current solution.

public static String create(Character c) {
    ...
    if (c.equals('D')) {
        return diamondTip(size) + "\n"
                + manySpaces(2) + "B" + manySpaces(1) + "B" + manySpaces(2) + "\n"
                + manySpaces(1) + "C" + manySpaces(3) + "C" + manySpaces(1) + "\n"
                + manySpaces(0) + "D" + manySpaces(5) + "D" + manySpaces(0) + "\n"
                + manySpaces(1) + "C" + manySpaces(3) + "C" + manySpaces(1) + "\n"
                + manySpaces(2) + "B" + manySpaces(1) + "B" + manySpaces(2) + "\n"
                + diamondTip(size);
    }
    ...
}

From now, I will only show you the refactoring on the ‘D’ diamond.

All those integers must have some kind of relationship. Maybe we can figure it by rearranging them.

Let’s introduce the width of the diamond. I draw some diamond on a paper and manage to figure quite rapidly that the width is size * 2 - 1

public static String create(Character c) {
    int size = c - 'A' + 1;
    int width = size * 2 - 1;
    
    if (c.equals('D')) {
        return diamondTip(size) + "\n"
            + manySpaces(2) + "B" + manySpaces(width - 2*3) + "B" + manySpaces(2) + "\n"
            + manySpaces(1) + "C" + manySpaces(width - 2*2) + "C" + manySpaces(1) + "\n"
            + manySpaces(0) + "D" + manySpaces(width - 2*1) + "D" + manySpaces(0) + "\n"
            + manySpaces(1) + "C" + manySpaces(width - 2*2) + "C" + manySpaces(1) + "\n"
            + manySpaces(2) + "B" + manySpaces(width - 2*3) + "B" + manySpaces(2) + "\n"
            + diamondTip(size);
     }
     ...
}

Well, I think I got it. Let make appear a floor level.

public static String create(Character c) {
    ...
    if (c.equals('D')) {
        String diamond = diamondTip(size) + "\n";

        int floor = 3;
        diamond += manySpaces(floor - 1) + "B" + manySpaces(width - 2*floor) + "B" + manySpaces(floor - 1) + "\n";
        floor = 2;
        diamond += manySpaces(floor - 1) + "C" + manySpaces(width - 2*floor) + "C" + manySpaces(floor - 1) + "\n";
        floor = 1;
        diamond += manySpaces(floor - 1) + "D" + manySpaces(width - 2*floor) + "D" + manySpaces(floor - 1) + "\n";
        floor = 2;
        diamond += manySpaces(floor - 1) + "C" + manySpaces(width - 2*floor) + "C" + manySpaces(floor - 1) + "\n";
        floor = 3;
        diamond += manySpaces(floor - 1) + "B" + manySpaces(width - 2*floor) + "B" + manySpaces(floor - 1) + "\n";

        diamond += diamondTip(size);
                
        return diamond;
    }
    ...
}

Yes, every lines really look like the same now, we can give it a name.

public static String create(Character c) {
    ...
    if (c.equals('D')) {
        String diamond = diamondTip(size) + "\n";
        diamond += diamondWall(size, 3, "B") + "\n";
        diamond += diamondWall(size, 2, "C") + "\n";
        diamond += diamondWall(size, 1, "D") + "\n";
        diamond += diamondWall(size, 2, "C") + "\n";
        diamond += diamondWall(size, 3, "B") + "\n";
        diamond += diamondTip(size);
                                     
        return diamond;
    }

    ...
}

private static String diamondWall(int size, int floor, String wall) {
    int width = size * 2 - 1;
    return manySpaces(floor - 1) + wall + manySpaces(width - 2*floor) + wall + manySpaces(floor - 1);
}

That’s better, but we are not done yet. There is a pattern in these calls. Let’s see what happend if we renumber floors so they become a sequence, from +size to -size

public static String create(Character c) {
    ...
    if (c.equals('D')) {
        String diamond = diamondTip(size) + "\n";
        diamond += diamondWall(size, 2, "B") + "\n";
        diamond += diamondWall(size, 1, "C") + "\n";
        diamond += diamondWall(size, 0, "D") + "\n";
        diamond += diamondWall(size, -1, "C") + "\n";
        diamond += diamondWall(size, -2, "B") + "\n";
        diamond += diamondTip(size);
                             
        return diamond;
    }
    ...
}

private static String diamondWall(int size, int floor, String wall) {
    int width = size * 2 - 1;
    int absoluteFloor = Math.abs(floor);

    return manySpaces(absoluteFloor) + wall + manySpaces(width - 2*(absoluteFloor + 1)) + wall + manySpaces(absoluteFloor);
}

Well, there seems to have a loop hidden there. The problem to introduce it are those characters in the parameters of diamondWall call. But we can easily get rid of them as the character used in a floor can be compute from the size of the diamond and the current floor.

private static String diamondWall(int size, int floor) {
    int width = size * 2 - 1;
    int absoluteFloor = Math.abs(floor);
        
    Character wall = Character.toChars('A' + size - absoluteFloor -1)[0]; 

    return manySpaces(absoluteFloor) + wall + manySpaces(width - 2*(absoluteFloor + 1)) + wall + manySpaces(absoluteFloor);
}

public static String create(Character c) {
    ...

    if (c.equals('D')) {
        String diamond = diamondTip(size) + "\n";
        diamond += diamondWall(size, 2)  + "\n";
        diamond += diamondWall(size, 1)  + "\n";
        diamond += diamondWall(size, 0)  + "\n";
        diamond += diamondWall(size, -1) + "\n";
        diamond += diamondWall(size, -2) + "\n";
        diamond += diamondTip(size);
                             
        return diamond;
    }
    ...
}

and finally

public static String create(Character c) {
    int size = sizeOfDiamond(c);
    
    if(size == 1) return "A";
    
    String diamond = diamondTip(size) + "\n";
    
    for (int floor = size - 2; floor >= -(size - 2) ; floor --) {
        diamond += diamondWall(size, floor)  + "\n";
    }
    
    diamond += diamondTip(size);
    
    return diamond;
}

thoughts

My first attempt on that kata took me around 2hours in a train3.

As I usually do I’ve made things with very small steps in order to keep my test green all the time. I tried to let the algorithm emerge from the refactoring, and not the opposite.

Of course, it did not emerge by itself. With that kind of problem, I had in mind that there will be some kind of loop in the solution. So my refactorings aimed to make that loop appears: introducing deliberate duplication so that each line looks the same, then unifying indices.

Introducing one change at a time allow my test to be very efficient in helping me to find the errors I’ve made4.

The full code is available on my github. Look in the branches :)5

next

Once there, if it was real code, I will probably give name to size -2 and -(size - 2). Maybe attic and cellar to stick with the building metaphore. And size should probably become height.

I also don’t really like the use of a Character everywhere will in fact it is a kind of diamond. So i’ll introduce a DiamondKind class to hold the Character and size6, width calculation. Maybe other Character operations too7.

Maybe a Floor class too8.

The “A” diamond would probably become a constant with a name.


image : By kubotake CC BY 2.0, via Wikimedia Commons. During a solar eclipse, in french, the third contact is also called “Effet diamant” or diamond effect.

  1. well, it was recent when I started write that post. 

  2. started I think with this post Recycling test in TDD by Seb Rose. 

  3. on my way home from the awesome Ncrafts conference. 

  4. and I made a lot of them. +1/-1 shifts are my personal hell :) 

  5. in the branch 2015.05.23-step_by_step, I made a commit at each step with some explanation of the purpose of that step. Mostly, it is what you find in this post. 

  6. or height 

  7. like a charForFloor 

  8. but currently the use of floor is an implementation detail, not sure if it’s a good idea to give its own class that will become public.