Saturday, June 2, 2012

Print Diamond Problem with TDD approach

Problem given at the event :
Given a letter print a diamond starting with 'A' with the supplied letter at the widest point.
Development Approach we need to practice : "Test Driven Development"

During the coderetreat event conducted by  Johannes Brodwall  we practiced the same problem over and over with different devs with different approaches. In this post what I am trying to summarize the things I got through out this valuable event. 
The way I looked at the the problem is more biased in to a geometrical view. My observation is that we can put our elements in a 2D Cartesian space as follows. Following is the example for Letter C.
red dot - Letter A
green dot - Letter B
blue dot - Letter C

So if we can find the equation of the line XY then we can find the points by substituting Y=0,1,2 (We can write a simple logic to get the Y's range)

Equation for line XY :  Y = -X +2
by substituting y values we can get following points.
(0,2),( 1,1) and (2,0)

Similarly you can obtain the equation for line LM.
Equation for line LM :  Y = -X -2
by substituting y values we can get following points.(y=0,-1,-2)
(0,-2), (-1,-1) and (-2,0)

Equation for line XM :  Y = X -2
by substituting y values we can get following points.(y=0,-1,-2)
(0,-2),(1,-1) and (2,0)

Equation for line LY :  Y = X +2
by substituting y values we can get following points.(y=0,-1,-2)
(0,-2),(1,-1) and (2,0) 

If you are familiar with the absolute value function you can write above functions with a single function as follows.

| X | + | Y | = 2

so our task has reduced to find a simple liner equation for a given charactor.
So the general form of the equation is |X| + |Y| = c
When it comes to TDD approach first thing we need to do is write a simple test against our business logic which gives an assertion failure. Test should be smaller enough to fail.  
Following are the test cases I wanted to cover :
1).  Find the interceptor of the equation for a given input character :
@Test
 public void test_the_intercept_of_the_line() {
  Diamond diamond = new Diamond();
  Assert.assertEquals(0, diamond.getInterceptor('A'));
  Assert.assertEquals(1, diamond.getInterceptor('B'));
  Assert.assertEquals(2, diamond.getInterceptor('C'));
 } 
Source for this test case is simple as follows.
public class Diamond {

 public int getInterceptor(char c) {  
  return c-65;
 }
}
In this case what you need to make sure is just write enough code which is capable of passing you test case.

2) As for my second test scenario I used Input character 'B' and test the points list where I need to print characters. 
@Test
 public void test_output_diamond_for_input_charactor_B() {
  Diamond diamond = new Diamond();
  Set pointSet = diamond.getPointSet('B');
  Assert.assertEquals(true, pointSet.contains(new Point(0, 1)));
  Assert.assertEquals(true, pointSet.contains(new Point(0, -1)));
  Assert.assertEquals(true, pointSet.contains(new Point(1, 0)));
  Assert.assertEquals(true, pointSet.contains(new Point(-1, 0)));
  Assert.assertEquals(false, pointSet.contains(new Point(-1, -1)));
 }
In order to pass the test case I used following algorithm which i was planing to apply in above explanation.(Implementation of absolute value function with +/- notations)
public class Diamond {

 public int getInterceptor(char c) {
  return c - 65;
 }

 public Set getPointSet(char c) {
  Set pointsSet = new HashSet();
  int interceptor = getInterceptor(c);

  for (int yIndex = interceptor; yIndex >= 0; yIndex--) {
   int xIndex = interceptor - yIndex;
   pointsSet.add(new Point(xIndex, yIndex));
   pointsSet.add(new Point(xIndex, -yIndex));
   pointsSet.add(new Point(-xIndex, yIndex));
   pointsSet.add(new Point(-xIndex, -yIndex));
  }
  return pointsSet;
 }
}
Since I wanted to apply some OOP concepts I created Point class and implement the constructor which we can pass X,Y coordinates. And also I implement the hash code , toString and equals methods in the Point class.(This can be generated by using eclipse IDE). In addition to that I am going to store relevant character with that point object as follows.
 
public Point(int i, int j) {
  this.setX(i);
  this.setY(j);
  this.setCharacter((char)(Math.abs(j)+65));
 }

3) During the workshop I heard that some guys  were taking about write test cases to figure out whether the points are located with in the boundary. But in this approach we don't need such kind of a test since we are substituting the correct range of Y values. That range is indirectly tested by test_interceptor_of_the_line method. I found that it is not compulsory to write a test for figure out the boundary of the grid.
At this point I know my algorithm is working properly. And then I need to print the character pattern in to the console output. In order to do so I just had to write following simple logic.

public void printDiamond(char c) {
  int interceptor = getInterceptor(c);
  int gridArea = 2 * interceptor + 1;
  // Create an array with all 'X' values
  char[][] diamond = new char[gridArea][gridArea];
  for (char[] ds : diamond) {
   for (int i = 0; i < ds.length; i++) {
    ds[i] = 'X';
   }
  }
  Set pointSet = getPointSet(c);

  for (Point point : pointSet) {
   diamond[point.getX() + interceptor][point.getY() + interceptor] = point
     .getCharacter();
  }

  for (int row = 0; row < gridArea; row++) {
   for (int column = 0; column < gridArea; column++) {
    System.out.print(String.valueOf(diamond[row][column]));
   }
   System.out.println();
  }

 }
It will gives the expected diamond for a given input character.

Since my tests are passing I thought of having some refactoring to Dimond class. for and example I do not need to expose getInterceptor method to public. So i can make it private.

Another self finding of this workshop is that we should think the test case as a opposition party  and always write the test cases which will give the maximum pain for the source code end. (Class under Test). At the same time you need to make sure to write a simple test which gives enough potential to fail.

So its about how to treat our code. Overall it was a wonderful event and  I could deal with the people who has different kind of mind sets. At the end of the day what I had is a green color sticky note which says that you should practice some code kata with TDD until you will get the right skill. Kudos for Johannes.

Note : In the case where you need to have a closer look in to the code you can get it from here.

5 comments:

  1. Thank you for participating in the code event and for the positive feedback, Eshan.

    I especially like the fact that you introduce the Point class so there's a more object oriented approach.

    One thing that I find disturbing with most solutions, including yours is the fact that you have to create and initialize the grid. This is an artifact of a structural, rather than functional approach.

    If you want a challenge to your code, how about trying to remove the initialization of code by having the pointset include the empty characters.

    ReplyDelete
    Replies
    1. Hi Johannes,
      Thanks for the quick feed back on this. And I agree with your idea.
      I thought of having an approach like bellow..

      public void printDiamond(char c) {
      int interceptor = getInterceptor(c);
      int gridArea = 2 * interceptor + 1;

      for (int row = 0; row < gridArea; row++) {
      for (int column = 0; column < gridArea; column++) {
      System.out.print(String.valueOf(getPoint(row, column,
      interceptor).getCharacter()));

      }
      System.out.println("");
      }

      }

      private Point getPoint(int xIndex, int yIndex, int c) {
      if (xIndex + yIndex == c)
      return new Point(yIndex - c);
      else if (xIndex + yIndex == 3 * c)
      return new Point(yIndex - c);
      else if (-xIndex + yIndex == -c)
      return new Point(yIndex - c);
      else if (-xIndex + yIndex == c)
      return new Point(yIndex - c);
      return new Point();
      }

      Delete
    2. Cool.

      Now, what would happen if you let the for-loops go from row=-interceptor; row<=interceptor (and the same for column)?

      Delete
    3. Yes. You are correct. And again thanks for helping me to improve my code. Finally I could ended up with a code as bellow.

      public void printDiamond(char c) {
      int interceptor = getInterceptor(c);
      for (int row = -interceptor; row <= interceptor; row++) {
      for (int column = -interceptor; column <= interceptor; column++) {

      if (shouldPrintChar(row, column, interceptor)) {
      System.out .print(String.valueOf((char) (Math.abs(row) + 65)));

      } else {
      System.out.print(" ");
      }
      }
      System.out.println("");
      }

      }

      private boolean shouldPrintChar(int xIndex, int yIndex, int interceptor) {

      return (Math.abs(xIndex) + Math.abs(yIndex) == interceptor);

      }

      Working with you is always teach me something new. Are you involve with some open source development so that I can work as a contributor and learn the stuff. (from the right person :-))

      Delete