Evolution of the behavior through tests

The TDD methodology is based in work cycles with which we define a desired behavior in the form of a test, we make changes in the production code to implement it, and we refactor the solution once we know that it works.

While we have specific tooling to detect situations in need of refactoring, and even well-define methods to carry it out, we don’t have specific resources that guide the necessary code transformations in a similar manner. That is, is there any process that help us decide what kind of change to apply to the code in order to implement a behavior?

The Transformation Priority Premise1 is an article that suggests a useful framework in this sense. Starting from the idea that as the tests become more specific the code becomes more general, it proposes a sequence of the type of transformations that we can apply every time that we’re in the implementation phase, in the transition from red to green.

The development through TDD would have two main parts:

  • In the first one we build the class’ public interface, defining how we’re going to communicate with it, and how it’s going to answer us. We analyze this question in it’s most generic way, which would be the data type that it returns.
  • In the second part we develop the behavior, starting from the most general cases, and introducing the more specific ones later.

Let’s see this with a practical example. We’ll perform the Roman Numerals kata paying attention to how the tests help up guide these two parts.

Constructing the public interface of a test driven class

We’ll always start with a test that forces us to define the class, as for now we don’t need anything else than an object with which to interact.

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 
 5 class RomanNumeralsTest {proposes
 6 
 7     @Test
 8     fun `Should convert numbers to roman` () {
 9         RomanNumerals()
10     }
11 }

We run the test to see it fail, and the, we write the empty class definition, the minimum necessary to pass the test.

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4 
5 }

If we’ve created it in the same file as the test, now we can move it to its place during the refactoring phase.

We can already think about the second test, which we need to define the public interface, that it: how we’re going to communicate with the object once it’s instantiated, and what messages it’s going to be able to understand:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 
 5 class RomanNumeralsTest {
 6 
 7     @Test
 8     fun `It converts numbers to roman` () {
 9         RomanNumerals().toRoman()
10     }
11 }

We’re modifying the first test. Now that we have some fluency we can afford this kind of license, so writing new tests is more inexpensive. Well, we check that it fails for the reason that it has to fail (the toRoman message is not defined), and next we write the necessary code to make it pass. The compiler helps us: if we run the test we’ll see that it throws an exception that tells us that the method exists but it’s not implemented. And probably the IDE tells us something about it too, one way or another. Kotlin, which is the language that we’re working with here, ask us directly to implement it:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman() {
5         TODO("Not yet implemented")
6     }
7 }

For now, we remove these indications introduced by the IDE:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman() {
5     }
6 }

And this passes the test. We already have the message with which we’re going to ask RomanNumerals to do the conversion. The next step can be to define that the response we expect should be a String. If we work with dynamic typing or Duck Typing we’ll need a specific test. However, in Kotlin we can do it without tets. It’s enough to specify the return type of the function:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman(): String {
5     }
6 }

This won’t compile and our current test will fail, so the way to make it pass would be to return any String. Even an empty one.

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman(): String {
5         return "";
6     }
7 }

We may consider this as a refactoring up to a certain point, but we can apply it as if it were a test.

Now we’re going to think about how to use this code to convert arabic numbers to roman notation. Since there is no zero in the latter, we have to start with number 1.

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 
 5 class RomanNumeralsTest {
 6 
 7     @Test
 8     fun `Should convert numbers to roman` () {
 9         RomanNumerals().toRoman(1)
10     }
11 }

When we run the test, we can see that it fails because the function doesn’t expect an argument, so we add it:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman(decimal: Int): String {
5         return "";
6     }
7 }

And this passes the test. The public interface has been defined, but we still don’t have any behavior.

Drive the development of a behavior through examples

Once we’ve established the public interface of the class that we’re developing, we’ll want to start implementing its behavior. We need a first example, which for this exercise will be to convert the 1 into I.

To do this, we already need to assign the value to a variable and use an assertion. The test will end up like this:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         var roman = RomanNumerals().toRoman(1)
11 
12         assertEquals("I", roman)
13     }
14 }

From null to constant

Right now, RomanNumerals().toRoman(1) returns "", which for all intents and purposes is equivalent to returning null.

What is the simplest transformation that we can make to make the test pass? In few words, we go from not returning anything to returning something, and to pass the test, that something ought to be the value “I”. That is, a constant:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman(number: Int): String {
5         return "I"
6     }
7 }

The test passes. This solution might shock you if it’s your first time peeking at TDD, although if you’re reading this book you’ll have already seen more examples of this. But this solution is not stupid.

In fact, this is the best solution for the current state of the test. We may know that we want to build an arabic to roman numeral converter, but what the test specifies here and now is just that we expect our code to convert the integer 1 to the String I. And that’s exactly what it does.

Therefore, the implementation has exactly the necessary complexity and specificity level. What we’re going to do next will be to question it with another example.

But first, we should do a refactoring.

We’ll do it to prepare for what comes next. When we change the example, the response will have to change as well. So, we’re going to do two things: use the parameter that we receive, and, at the same time, ensure that this test will always pass:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman(number: Int): String {
5         if (number == 1) return "I"
6         return ""
7     }
8 }

We run the test, which should pass without any issues. Moreover, we’ll make a small adjustment to the test itself:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10 
11         assertEquals("I", RomanNumerals().toRoman(1))
12     }
13 }

The test continues to pass and we are already left with nothing to do, so we’re going to introduce a new example (something that is now easier to do):

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertEquals("I", RomanNumerals().toRoman(1))
11         assertEquals("II", RomanNumerals().toRoman(2))
12     }
13 }

When we run the test we check that it fails because it doesn’t return the expected II. A way to make it pass is the following:

1 package org.talkingbit.kata
2 
3 class RomanNumerals {
4     fun toRoman(number: Int): String {
5         if (number == 2) return "II"
6         if (number == 1) return "I"
7         return ""
8     }
9 }

Note that, for now, we’re returning constants in all cases.

Let’s refactor, as we’re in green. First we refactor the test to make it even more compact, and easier to read and extend with examples.

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12     }
13 
14     private fun assertConvertsToRoman(arabic: Int, roman: String) {
15         assertEquals(roman, RomanNumerals().toRoman(arabic))
16     }
17 }

We add yet another test. Now it’s even easier:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13     }
14 
15     private fun assertConvertsToRoman(arabic: Int, roman: String) {
16         assertEquals(roman, RomanNumerals().toRoman(arabic))
17     }
18 }

We see it fail, and, to make it pass, we add a new constant:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         if (number == 3) return "III"
 6         if (number == 2) return "II"
 7         if (number == 1) return "I"
 8         return ""
 9     }
10 }

And now, expressing the same, but in a different manner and using only one constant:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         if (number == 3) return "I" + "I" + "I"
 6         if (number == 2) return "I" + "I"
 7         if (number == 1) return "I"
 8         return ""
 9     }
10 }

We could extract it:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         val i = "I"
 6         if (number == 3) roman = i + i + i
 7         if (number == 2) roman = i + i
 8         if (number == 1) roman = i
 9         return ""
10     }
11 }

And now it’s easy to see how we could introduce a new transformation.

From constant to variable

This transformation consists in using a variable to generate the response. That is, now instead of returning a fixed value for each example, we’re going to calculate the appropriate response. Basically, we’ve started to build an algorithm.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         val i = "I"
 6         var roman = ""
 7         if (number == 3) roman = i + i + i
 8         if (number == 2) roman = i + i
 9         if (number == 1) roman = i
10 
11         return roman
12     }
13 }

This transformation makes it clear that the algorithm consists in piling up as many I as number indicates. A way of seeing it:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         val i = "I"
 6         var roman = ""
 7         for (counter in 1..number) {
 8             roman += i
 9         }
10         return roman
11     }
12 }

This for loop could be better expressed as a while, but first we have to make a change. It should be noted that the parameters in Kotlin are final, so we can’t modify them. For this reason, we’ve had to introduce a variable and initialize it to the value of the parameter.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         val i = "I"
 7         var roman = ""
 8         while (arabic >= 1) {
 9             roman += i
10             arabic -= 1
11         }
12 
13         return roman
14     }
15 }

On the other hand, since the i constant is only used once and its meaning is pretty evident, we’re going to remove it.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7         while (arabic >= 1) {
 8             roman += "I"
 9             arabic -= 1
10         }
11 
12         return roman
13     }
14 }

This way we’ve started to build a more general solution to the algorithm, at least up to the point that’s currently defined by the tests. As we know, it’s not “legal” to accumulate more than 3 equal symbols in the roman notation, so in it’s current state, the algorithm will generate the wrong roman representations if we use it on any number larger than 3.

This indicates that we need a new test to be able to incorporate a new behavior and develop the algorithm further, which is still very specific.

But, what is the next example that we could implement?

From unconditional to conditional

In the first first place we got number 4, which in roman notation is expressed as IV. It introduces a new symbol, which is a combination of symbols in itself. For all we know it’s just a particular case, so we introduce a conditional to separate the flow into two branches: one for the behavior that we already know, and other for the new one.

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14     }
15 
16     private fun assertConvertsToRoman(arabic: Int, roman: String) {
17         assertEquals(roman, RomanNumerals().toRoman(arabic))
18     }
19 }

The test will fail because it tries to convert the number 4 to IIII. We introduce the conditional to handle this particular case.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic == 4) {
 9             roman = "IV"
10         }
11 
12         while (arabic >= 1) {
13             roman += "I"
14             arabic -= 1
15         }
16 
17         return roman
18     }
19 }

Oops. The test fails because we have forgotten to subtract the consumed value. We fix it like this, and we leave a note for our future selves:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic == 4) {
 9             roman = "IV"
10             arabic -= 4
11         }
12 
13         while (arabic >= 1) {
14             roman += "I"
15             arabic -= 1
16         }
17 
18         return roman
19     }
20 }

We advance to a new number:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15     }
16 
17     private fun assertConvertsToRoman(arabic: Int, roman: String) {
18         assertEquals(roman, RomanNumerals().toRoman(arabic))
19     }
20 }

We check that the test fails for the expected reasons and we get IIIII as a result. To make it pass we’ll take another path, introducing a new conditional because it’s a new different case. This time we don’t forget to subtract the value of 5.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic == 5) {
 9             roman = "V"
10             arabic -= 5
11         }
12 
13         if (arabic == 4) {
14             roman = "IV"
15             arabic -= 4
16         }
17 
18         while (arabic >= 1) {
19             roman += "I"
20             arabic -= 1
21         }
22 
23         return roman
24     }
25 }

The truth is that we had already used conditional before, when our responses were constant, to choose “which constant to return”, so to speak. Now we introduce the conditional in order to be able to handle new case families, as we’ve already exhausted the capabilities of the existing code to solve the new cases that we’re introducing. And within that execution branch that didn’t exist before, we resort to a constant again in order to solve it.

We introduce a new failing test to force another algorithm advance:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(3, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16     }
17 
18     private fun assertConvertsToRoman(arabic: Int, roman: String) {
19         assertEquals(roman, RomanNumerals().toRoman(arabic))
20     }
21 }

This case is especially interesting to see fail:

1 expected:<[V]I> but was:<[IIIII]I>
2 Expected :VI
3 Actual   :IIIIII

We need to include the “V” symbol, something that we can do in a very simple way by changing the == for a >=.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic >= 5) {
 9             roman = "V"
10             arabic -= 5
11         }
12 
13         if (arabic == 4) {
14             roman = "IV"
15             arabic -= 4
16         }
17 
18         while (arabic >= 1) {
19             roman += "I"
20             arabic -= 1
21         }
22 
23         return roman
24     }
25 }

A minimal change has sufficed to make the test pass. The next two examples pass without any extra effort:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18     }
19 
20     private fun assertConvertsToRoman(arabic: Int, roman: String) {
21         assertEquals(roman, RomanNumerals().toRoman(arabic))
22     }
23 }

This happens because our current algorithm is already general enough to be able to handle these cases. However, when we introduce the 9, we face a different casuistry:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19     }
20 
21     private fun assertConvertsToRoman(arabic: Int, roman: String) {
22         assertEquals(roman, RomanNumerals().toRoman(arabic))
23     }
24 }

The result is:

1 expected:<I[X]> but was:<I[V]>
2 Expected :IX
3 Actual   :IV

We need a specific treatment, so we add a conditional for the new case:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic == 9) {
 9             roman = "IX"
10             arabic -= 9
11         }
12 
13         if (arabic >= 5) {
14             roman = "V"
15             arabic -= 5
16         }
17 
18         if (arabic == 4) {
19             roman = "IV"
20             arabic -= 4
21         }
22 
23         while (arabic >= 1) {
24             roman += "I"
25             arabic -= 1
26         }
27 
28         return roman
29     }
30 }

We keep running through the examples:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20     }
21 
22     private fun assertConvertsToRoman(arabic: Int, roman: String) {
23         assertEquals(roman, RomanNumerals().toRoman(arabic))
24     }
25 }

Being it a new symbol, we handle it in a special manner:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic == 10) {
 9             roman = "X"
10             arabic -= 10
11         }
12 
13         if (arabic == 9) {
14             roman = "IX"
15             arabic -= 9
16         }
17 
18         if (arabic >= 5) {
19             roman = "V"
20             arabic -= 5
21         }
22 
23         if (arabic == 4) {
24             roman = "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

If we take a look at the production code we can identify structures that are similar between them, but we can’t clearly see a pattern that we could refactor and generalize. Maybe we need more information. Let’s proceed to the next case:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20         assertConvertsToRoman(11, "XI")
21     }
22 
23     private fun assertConvertsToRoman(arabic: Int, roman: String) {
24         assertEquals(roman, RomanNumerals().toRoman(arabic))
25     }
26 }

This test results in:

1 expected:<[X]I> but was:<[VIIIII]I>
2 Expected :XI
3 Actual   :VIIIIII

To begin, we need to enter the “X” symbol’s conditional, so we make this change:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic >= 10) {
 9             roman = "X"
10             arabic -= 10
11         }
12 
13         if (arabic == 9) {
14             roman = "IX"
15             arabic -= 9
16         }
17 
18         if (arabic >= 5) {
19             roman = "V"
20             arabic -= 5
21         }
22 
23         if (arabic == 4) {
24             roman = "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

And this is enough to make the test pass. With numbers 12 and 13 the test continues to pass, but when we reach 14, something happens:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20         assertConvertsToRoman(11, "XI")
21         assertConvertsToRoman(12, "XII")
22         assertConvertsToRoman(13, "XIII")
23         assertConvertsToRoman(14, "XIV")
24     }
25 
26     private fun assertConvertsToRoman(arabic: Int, roman: String) {
27         assertEquals(roman, RomanNumerals().toRoman(arabic))
28     }
29 }

The result is:

1 expected:<[X]IV> but was:<[]IV>
2 Expected :XIV
3 Actual   :IV

This happens because we’re not accumulating the roman notation in the return variable, so in some cases we crush the existing result. Let’s change from a simple assignment to an expression:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic >= 10) {
 9             roman = "X"
10             arabic -= 10
11         }
12 
13         if (arabic == 9) {
14             roman = "IX"
15             arabic -= 9
16         }
17 
18         if (arabic >= 5) {
19             roman = "V"
20             arabic -= 5
21         }
22 
23         if (arabic == 4) {
24             roman += "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

This discovery hints that we could try some specific examples with which to manifest this problem and solve it for other numbers, such as 15.

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20         assertConvertsToRoman(11, "XI")
21         assertConvertsToRoman(12, "XII")
22         assertConvertsToRoman(13, "XIII")
23         assertConvertsToRoman(14, "XIV")
24         assertConvertsToRoman(15, "XV")
25     }
26 
27     private fun assertConvertsToRoman(arabic: Int, roman: String) {
28         assertEquals(roman, RomanNumerals().toRoman(arabic))
29     }
30 }

And we apply the same change:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         if (arabic >= 10) {
 9             roman = "X"
10             arabic -= 10
11         }
12 
13         if (arabic == 9) {
14             roman = "IX"
15             arabic -= 9
16         }
17 
18         if (arabic >= 5) {
19             roman += "V"
20             arabic -= 5
21         }
22 
23         if (arabic == 4) {
24             roman += "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

19 also has the same solution. But if we try 20, we’ll see a new error, a rather curious one:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20         assertConvertsToRoman(11, "XI")
21         assertConvertsToRoman(12, "XII")
22         assertConvertsToRoman(13, "XIII")
23         assertConvertsToRoman(14, "XIV")
24         assertConvertsToRoman(15, "XV")
25         assertConvertsToRoman(19, "XIX")
26         assertConvertsToRoman(20, "XX")
27     }
28 
29     private fun assertConvertsToRoman(arabic: Int, roman: String) {
30         assertEquals(roman, RomanNumerals().toRoman(arabic))
31     }
32 }

This is the result:

1 expected:<X[X]> but was:<X[VIIIII]>
2 Expected :XX
3 Actual   :XVIIIII

The problem is that we need to replace all of the 10 that are contained in the number by X.

Changing from if to while

To handle this case, the simplest thing to do is changing the if to a while. whileis an structure that is both a conditional and a loop at the same time. if executes the conditioned branch only once, but while does it as long as the condition continues to be met.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         while (arabic >= 10) {
 9             roman += "X"
10             arabic -= 10
11         }
12 
13         if (arabic == 9) {
14             roman += "IX"
15             arabic -= 9
16         }
17 
18         if (arabic >= 5) {
19             roman += "V"
20             arabic -= 5
21         }
22 
23         if (arabic == 4) {
24             roman += "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

Could we use while in all cases?NOw that we’re in green, we’ll try to change all of the conditions from if to while. And the tests prove that it’s possible to do so:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         while (arabic >= 10) {
 9             roman += "X"
10             arabic -= 10
11         }
12 
13         while (arabic == 9) {
14             roman += "IX"
15             arabic -= 9
16         }
17 
18         while (arabic >= 5) {
19             roman += "V"
20             arabic -= 5
21         }
22 
23         while (arabic == 4) {
24             roman += "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

This is interesting, we can see that the structure get more similar each time. Let’s try changing the cases in which we use an equality to see if we can use >= in its place.

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         while (arabic >= 10) {
 9             roman += "X"
10             arabic -= 10
11         }
12 
13         while (arabic >= 9) {
14             roman += "IX"
15             arabic -= 9
16         }
17 
18         while (arabic >= 5) {
19             roman += "V"
20             arabic -= 5
21         }
22 
23         while (arabic >= 4) {
24             roman += "IV"
25             arabic -= 4
26         }
27 
28         while (arabic >= 1) {
29             roman += "I"
30             arabic -= 1
31         }
32 
33         return roman
34     }
35 }

And the tests keep on passing. This indicates a possible refactoring to unify the code.

Introducing arrays (or collections)

It’s a big refactoring, the one that we’re going to do here in just one step. Basically, it consists in introducing a dictionary structure (Map, in Kotlin), that contains the various conversion rules:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         val symbols = mapOf(
 9                 10 to "X",
10                 9 to "IX",
11                 5 to "V",
12                 4 to "IV",
13                 1 to "I"
14         )
15 
16         for ((subtrahend, symbol) in symbols) {
17             while (arabic >= subtrahend) {
18                 roman += symbol
19                 arabic -= subtrahend
20             }
21         }
22 
23         return roman
24     }
25 }

The tests continue to pass, indication that our refactoring is correct. In fact, we wouldn’t have any error until reaching number 39. Something to be expected, as we introduce a new symbol:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20         assertConvertsToRoman(11, "XI")
21         assertConvertsToRoman(12, "XII")
22         assertConvertsToRoman(13, "XIII")
23         assertConvertsToRoman(14, "XIV")
24         assertConvertsToRoman(15, "XV")
25         assertConvertsToRoman(19, "XIX")
26         assertConvertsToRoman(20, "XX")
27         assertConvertsToRoman(40, "XL")
28     }
29 
30     private fun assertConvertsToRoman(arabic: Int, roman: String) {
31         assertEquals(roman, RomanNumerals().toRoman(arabic))
32     }
33 }

The implementation is simple now:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     fun toRoman(number: Int): String {
 5         var arabic = number
 6         var roman = ""
 7 
 8         val symbols = mapOf(
 9                 40 to "XL",
10                 10 to "X",
11                 9 to "IX",
12                 5 to "V",
13                 4 to "IV",
14                 1 to "I"
15         )
16 
17         for ((subtrahend, symbol) in symbols) {
18             while (arabic >= subtrahend) {
19                 roman += symbol
20                 arabic -= subtrahend
21             }
22         }
23 
24         return roman
25     }
26 }

And now that we’ve checked that it’s working properly, we move it to a better place:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     private val symbols: Map<Int, String> = mapOf(
 5             40 to "XL",
 6             10 to "X",
 7             9 to "IX",
 8             5 to "V",
 9             4 to "IV",
10             1 to "I"
11     )
12 
13     fun toRoman(number: Int): String {
14         var arabic = number
15         var roman = ""
16 
17         for ((subtrahend, symbol) in symbols) {
18             while (arabic >= subtrahend) {
19                 roman += symbol
20                 arabic -= subtrahend
21             }
22         }
23 
24         return roman
25     }
26 }

We could keep adding examples that are not yet covered in order to add the remaining transformation rules, but essentially, this algorithm isn’t going to change anymore, so we’re reached a general solution to convert any natural number to roman notation. In fact, this is how it would end up. The necessary tests, first:

 1 package org.talkingbit.kata
 2 
 3 import org.junit.jupiter.api.Test
 4 import kotlin.test.assertEquals
 5 
 6 class RomanNumeralsTest {
 7 
 8     @Test
 9     fun `Should convert numbers to roman` () {
10         assertConvertsToRoman(1, "I")
11         assertConvertsToRoman(2, "II")
12         assertConvertsToRoman(3, "III")
13         assertConvertsToRoman(4, "IV")
14         assertConvertsToRoman(5, "V")
15         assertConvertsToRoman(6, "VI")
16         assertConvertsToRoman(7, "VII")
17         assertConvertsToRoman(8, "VIII")
18         assertConvertsToRoman(9, "IX")
19         assertConvertsToRoman(10, "X")
20         assertConvertsToRoman(11, "XI")
21         assertConvertsToRoman(12, "XII")
22         assertConvertsToRoman(13, "XIII")
23         assertConvertsToRoman(14, "XIV")
24         assertConvertsToRoman(15, "XV")
25         assertConvertsToRoman(19, "XIX")
26         assertConvertsToRoman(20, "XX")
27         assertConvertsToRoman(40, "XL")
28         assertConvertsToRoman(50, "L")
29         assertConvertsToRoman(90, "XC")
30         assertConvertsToRoman(100, "C")
31         assertConvertsToRoman(400, "CD")
32         assertConvertsToRoman(500, "D")
33         assertConvertsToRoman(900, "CM")
34         assertConvertsToRoman(1000, "M")
35     }
36 
37     private fun assertConvertsToRoman(arabic: Int, roman: String) {
38         assertEquals(roman, RomanNumerals().toRoman(arabic))
39     }
40 }

And the implementation:

 1 package org.talkingbit.kata
 2 
 3 class RomanNumerals {
 4     private val symbols: Map<Int, String> = mapOf(
 5             1000 to "M",
 6             900 to "CM",
 7             500 to "D",
 8             400 to "CD",
 9             100 to "C",
10             90 to "XC",
11             50 to "L",
12             40 to "XL",
13             10 to "X",
14             9 to "IX",
15             5 to "V",
16             4 to "IV",
17             1 to "I"
18     )
19 
20     fun toRoman(number: Int): String {
21         var arabic = number
22         var roman = ""
23 
24         for ((subtrahend, symbol) in symbols) {
25             while (arabic >= subtrahend) {
26                 roman += symbol
27                 arabic -= subtrahend
28             }
29         }
30 
31         return roman
32     }
33 }

We could try several acceptance tests to verify that it’s possible to generate any roman number:

1         assertConvertsToRoman(623, "DCXXIII")
2         assertConvertsToRoman(1714, "MDCCXIV")
3         assertConvertsToRoman(2938, "MMCMXXXVIII")

Small production code transformation can result in big behavioral changes, although to do that we’ll need to spend some time on the refactoring, so that the introduction of changes is as simple as possible.

References