Converting Numbers to Words

The Hello World application that we did last time doesn't see much use in real life. So let's create something more useful -- an application that converts numbers to words!

Let's create a bare Number class with the static method toWords, and the matching JUnit test class for it.

Number.java

public class Number {
    public static String toWords(int i) {
        return "";
    }
}

NumberTest.java

public class NumberTest {
    public NumberTest() {
    }
}

We could start typing away an elegant implementation of the conversion logic at this point. But unless you have an algorithm cheat sheet, I'm almost sure your initial version of code would have some nasty bugs in it. So let's keep it simple and try to build it incrementally.

First, we add the test case for 1:

@Test
public void testToWords_1() {
System.out.println("toWords(1)");
    int i = 1;
    String expResult = "one";
    String result = Number.toWords(i);
    assertEquals(expResult, result);
}

Running this test case would of course result in failure since we haven't added any code to do the conversion yet.

Again, keeping it simple and building incrementally, we add just enough code to pass the test:

public static String toWords(int i) {
    return "one";
}

Okay, so now you're thinking "This can't be right!" But in the limited range of parameters with only 1 as the valid value, this implementation is correct.

Let's add one more test, now for the value 2:

@Test
public void testToWords_2() {
    System.out.println("toWords(2)");
    int i = 2;
    String expResult = "two";
    String result = Number.toWords(i);
    assertEquals(expResult, result);
}

This would fail because our conversion logic is hard-wired to return the result for 1. So we add just a little bit of code to make it succeed:


public static String toWords(int i) {
    if (1 == i)
        return "one";
    else
        return "two";
}

We see a pattern at this point, so we can try to write a bunch of test cases plus the code to make them succeed and hopefully not get lost.

@Test
public void testToWords_3() {
    System.out.println("toWords(3)");
    int i = 3;
    String expResult = "three";
    String result = Number.toWords(3);
    assertEquals(expResult, result);
}

@Test
public void testToWords_4() {
    System.out.println("toWords(4)");
    int i = 4;
    String expResult = "fout";
    String result = Number.toWords(i);
    assertEquals(expResult, result);
}
		:
		:
@Test
public void testToWords_10() {
    System.out.println("toWords(10)");
    int i = 10;
    String expResult = "ten";
    String result = Number.toWords(i);
    assertEquals(expResult, result);
}

Our conversion logic will now be:

public static String toWords(int i) {
    if (1 == i)
        return "one";
    else if(2 == i)
        return "two";
    else if(3 == i)
        return "three";
    else if(4 == i)
        return "four";
    else if(5 == i)
        return "five";
    else if(6 == i)
        return "six";
    else if(7 == i)
        return "seven";
    else if(8 == i)
        return "eight";
    else if(9 == i)
        return "nine";
    else
        return "ten";
}

Hmm. Only 9 out of ten test cases passed. The test case testToWords_4 caused an error.

The conversion logic looks correct. Let's take another look at our test code.

@Test
public void testToWords_4() {
    System.out.println("toWords(4)");
    int i = 4;
    String expResult = "fout";
    String result = Number.toWords(i);
    assertEquals(expResult, result);
}

Found it! There's a typo on the expected result string. Correcting "fout" to "four" solves the problem and allows us to pass all tests. Lesson learned here is that sometimes our tests fail because the test itself is wrong!

Our brute force if ... else implementation works well and cannot be any simpler. However, several months from now, you would probably wonder what this does when you look at it again. And of course, it would hurt your geek sensibilities seeing such inelegant code!

Let's rewrite it to something more compact and easier to understand:

public static String toWords(int i) {
    String words[] = { "one", "two", "three", "four", "five", "six",
                             "seven", "eight", "nine", "ten"};
    return words[i];
}

Wow! The 20 lines of code are now reduced to two. And since I already have my test cases, I can do a regression test on this drastically rewritten code.

FAIL. All my tests failed.

The words that my newly rewritten code is returning are shifted by one: for 1, it was returning "two"; for 2, it was returning "three"; and so on. It turns out that I made a mistake with my array index and I was off by 1.

The code should be:

public static String toWords(int i) {
    String words[] = { "one", "two", "three", "four", "five", "six",
                             "seven", "eight", "nine", "ten"};
    return words[i-1];
}

The test cases that I have invested in writing at the start helped me quickly find the bugs soon after they were introduced. They have also allowed me to confidently rewrite my code and prove that it still works.

Well, in a world where you can count only up to ten, anyway.

Share