Let’s Go!

What are we building?

Story: Roman Numeral Converter

We need a function that takes an Arabic number and returns its Roman numeral equivalent (as a string).

One, Two, Three, ...

Getting the first test to pass will take us a few minutes, since there’s a good amount of setup work to do (getting the build script in place, adding header includes, and so on). There are also decisions to be made: What are we going to name our test method? What should the interface to our function look like?

We choose to make our conversion a free function. Here’s the first, failing test:

roman/1/RomanConverterTest.cpp
 
TEST(RomanConverter, CanConvertPositiveDigits) {
 
EXPECT_THAT(convert(1), Eq(​"I"​));
 
}

One of the goals for a kata is to minimize our movements. While I’m not enamored with free functions, they’re a fine place to start when we have a purely functional need. When done, we can scope the function as a static class member if needed.

To make things even simpler, we’ll keep the test and convert implementations in the same file for now. Here’s the entire file, including the code to make our first test pass:

roman/2/RomanConverterTest.cpp
 
#include "gmock/gmock.h"
 
 
using​ ​namespace​ ::testing;
 
using​ ​namespace​ std;
 
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
return​ ​"I"​;
 
}
 
 
TEST(RomanConverter, CanConvertPositiveDigits) {
 
EXPECT_THAT(convert(1), Eq(​"I"​));
 
}

Moving right along, converting two seems the next most sensible place to go.

roman/3/RomanConverterTest.cpp
 
TEST(RomanConverter, CanConvertPositiveDigits) {
 
EXPECT_THAT(convert(1), Eq(​"I"​));
 
EXPECT_THAT(convert(2), Eq(​"II"​));
 
}

You might have noticed that we’re using EXPECT_THAT as opposed to my preferred use of ASSERT_THAT. To remind you of the distinction, if an EXPECT_THAT assertion fails, Google Test keeps executing the current test. If an ASSERT_THAT assertion fails, Google Test halts the current test’s execution.

Normally, you want one scenario—a single test case—per test function. With a single case, it usually makes little sense to continue executing the test if an assertion fails—the rest of the test is generally invalid.

With respect to the Roman converter, however, there’s little sense to creating new test methods, given that the externally recognized behavior (“convert a number”) remains unchanged for each new case. Were we to have separate test methods for our two cases so far, our names would be tedious and would add little value: Convert1, Convert2.

Since we have one test function with lots of discrete cases, using EXPECT_THAT makes more sense. If one assertion fails, we still want to know what other cases pass or fail.

To get our second assertion to pass, we treat the new conversion of two like a special case by introducing an if statement. So far, that’s all we know. We have two special cases—one and two—and our code mirrors that knowledge.

roman/3/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
if​ (arabic == 2)
 
return​ ​"II"​;
 
return​ ​"I"​;
 
}

Here’s a third assertion:

roman/4/RomanConverterTest.cpp
 
EXPECT_THAT(convert(3), Eq(​"III"​));

Now we sense a pattern and can take advantage of it with a simple loop.

roman/4/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
string​ roman{​""​};
 
while​ (arabic-- > 0)
 
roman += ​"I"​;
 
return​ roman;
 
}

Simple, simple. We don’t worry about optimization yet; hence, concatenating to a string is good enough for now, and a for seems unnecessary.

Ten Sir!

One, two, three...ready for four? Not necessarily. TDD doesn’t have any rules that force us to construct tests in a certain order. Instead, we always want to apply a bit of thought to what our next test should be.

Four in Roman—IV—is a subtraction of sorts, one (I) less than five (V). We haven’t even hit five (V) yet, so perhaps it makes sense to hold off on four until we at least tackle five. Regarding V, it seems like a simple special case. Perhaps we should think about what is similar to what we already have going. One, two, three results in the progression I, II, III. Let’s look at another similar progression, that of X, XX, XXX.

roman/5/RomanConverterTest.cpp
 
EXPECT_THAT(convert(10), Eq(​"X"​));

Ten is another special case, best dispatched with an if statement.

roman/5/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
string​ roman{​""​};
 
if​ (arabic == 10)
 
return​ ​"X"​;
 
while​ (arabic-- > 0)
 
roman += ​"I"​;
 
return​ roman;
 
}

Eleven promises to be a little more interesting.

roman/6/RomanConverterTest.cpp
 
EXPECT_THAT(convert(11), Eq(​"XI"​));
roman/6/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
string​ roman{​""​};
 
if​ (arabic >= 10)
 
{
 
roman += ​"X"​;
 
arabic -= 10;
 
}
 
while​ (arabic-- > 0)
 
roman += ​"I"​;
 
return​ roman;
 
}

If our converter is passed a number greater than ten, we append an X, subtract 10 from the number, and drop through to the same while loop. We figure that assertions for twelve and thirteen should pass automatically.

roman/6/RomanConverterTest.cpp
 
EXPECT_THAT(convert(12), Eq(​"XII"​));
 
EXPECT_THAT(convert(13), Eq(​"XIII"​));

Both assertions indeed pass. A new assertion for twenty fails.

roman/7/RomanConverterTest.cpp
 
EXPECT_THAT(convert(20), Eq(​"XX"​));

Getting it to pass is as simple as changing the keyword if to while.

roman/7/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
string​ roman{​""​};
 
while​ (arabic >= 10)
 
{
 
roman += ​"X"​;
 
arabic -= 10;
 
}
 
while​ (arabic-- > 0)
 
roman += ​"I"​;
 
return​ roman;
 
}

Duplicating Almost-Duplication to Eliminate It

We have an implementation with what appears to be near-duplicate code. The while loops are similar. Faced with “almost duplication,” our next step is to make things look as much alike as possible.

roman/8/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
string​ roman{​""​};
 
while​ (arabic >= 10)
 
{
 
roman += ​"X"​;
 
arabic -= 10;
 
}
 
while​ (arabic >= 1)
 
{
 
roman += ​"I"​;
 
arabic -= 1;
 
}
 
return​ roman;
 
}

After refactoring, we have two loops with the same logic, varying only in data. We could extract a common function to eliminate the duplication, but it seems a less-than-stellar choice since two separate elements are changing (the Arabic total and the Roman string). Let’s instead extract a conversion table:

roman/9/RomanConverterTest.cpp
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
const​ ​auto​ arabicToRomanConversions = {
 
make_pair(10u, ​"X"​),
 
make_pair(1u, ​"I"​) };
 
string​ roman{​""​};
 
for​ (​auto​ arabicToRoman: arabicToRomanConversions)
 
while​ (arabic >= arabicToRoman.first)
 
{
 
roman += arabicToRoman.second;
 
arabic -= arabicToRoman.first;
 
}
 
return​ roman;
 
}

The duplicate loops collapse into a generalized loop that in turn is part of an iteration over the conversion pairs.

Finishing Up

Our algorithm paraphrased goes like this: for each pair representing an Arabic-to-Roman digit mapping, subtract the Arabic value and then append the corresponding Roman value until the remaining total is less than the Arabic digit. (We could use a different data structure, as long as we ensure that the conversion mappings get iterated in order, descending from largest to smallest.)

A bit of thought suggests that the algorithm should work for any digit mapping, as long as we add it to the table. Let’s try five, which we previously considered a special case.

roman/10/RomanConverterTest.cpp
 
EXPECT_THAT(convert(5), Eq(​"V"​));

Getting the test to pass requires only adding an entry to the conversion table.

roman/10/RomanConverterTest.cpp
 
const​ ​auto​ arabicToRomanConversions = {
 
make_pair(10u, ​"X"​),
*
make_pair(5u, ​"V"​),
 
make_pair(1u, ​"I"​) };

We’re now writing tests mostly for confidence purposes (see Testing for Confidence), since we’re doing little other than adding data to a table, but that’s OK. Let’s add a few more confidence measures. What about fifty, one hundred, and combinations thereof?

roman/11/RomanConverterTest.cpp
 
EXPECT_THAT(convert(50), Eq(​"L"​));
 
EXPECT_THAT(convert(80), Eq(​"LXXX"​));
 
EXPECT_THAT(convert(100), Eq(​"C"​));
 
EXPECT_THAT(convert(288), Eq(​"CCLXXXVIII"​));

We find they all easily pass, too, with requisite additions to the conversion table.

roman/11/RomanConverterTest.cpp
 
const​ ​auto​ arabicToRomanConversions = {
 
make_pair(100u, ​"C"​),
 
make_pair(50u, ​"L"​),
 
make_pair(10u, ​"X"​),
 
make_pair(5u, ​"V"​),
 
make_pair(1u, ​"I"​) };

Finally, we’re again faced with the challenge we’ve been avoiding. What about four?

roman/12/RomanConverterTest.cpp
 
EXPECT_THAT(convert(4), Eq(​"IV"​));

We could try supporting four by introducing a bit of subtraction logic. But doing so seems like it would involve some rather convoluted contortions and conditionals in our code. Hmm.

Remember, TDD isn’t a mindless exercise (see Thinking and TDD). At each step, you need to think about many things, including what the next step should be. So far, seeking the tests that drive in simpler implementations has worked well for us, as has seeking tests that look to represent similar patterns. And we improved our ability to follow this simple-and-simpler trajectory through the tests by ensuring our code was appropriately refactored at each step.

From time to time, you’ll also need a few critical insights in order to best succeed. Sometimes they come easily, sometimes they don’t. The more you program and the more you seek to experiment with things, the more often the bulbs will light in your head. A fantastic aspect of TDD is that it affords such experimentations. Try something clever, and you’ll know in a few minutes whether it works. If not, revert and try something else.

What if we consider the Roman representation of four as a single digit, despite that it requires two of our alphabet’s characters (IV)? Imagine, for example, that the Romans used a single special character to represent it. At that point, it’s simply another entry in the conversion table.

roman/12/RomanConverterTest.cpp
 
const​ ​auto​ arabicToRomanConversions = {
 
make_pair(100u, ​"C"​),
 
make_pair(50u, ​"L"​),
 
make_pair(10u, ​"X"​),
 
make_pair(5u, ​"V"​),
*
make_pair(4u, ​"IV"​),
 
make_pair(1u, ​"I"​) };

The algorithm remains untouched. We can finish up with a couple final assertions that verify our algorithm supports all the other subtraction-oriented numbers, including 9, 40, 90, 400, and 900, as well as the other couple digits we haven’t yet added (500 and 1,000). For these assertions, I entered convert n to roman (where n is the number to convert) as a Google search and used the answer it returned as my assertion expectation.

roman/13/RomanConverterTest.cpp
 
#include "gmock/gmock.h"
 
 
#include <vector>
 
#include <string>
 
 
using​ ​namespace​ ::testing;
 
using​ ​namespace​ std;
 
 
string​ convert(​unsigned​ ​int​ arabic)
 
{
 
const​ ​auto​ arabicToRomanConversions = {
 
make_pair(1000u, ​"M"​),
 
make_pair(900u, ​"CM"​),
 
make_pair(500u, ​"D"​),
 
make_pair(400u, ​"CD"​),
 
make_pair(100u, ​"C"​),
 
make_pair(90u, ​"XC"​),
 
make_pair(50u, ​"L"​),
 
make_pair(40u, ​"XL"​),
 
make_pair(10u, ​"X"​),
 
make_pair(9u, ​"IX"​),
 
make_pair(5u, ​"V"​),
 
make_pair(4u, ​"IV"​),
 
make_pair(1u, ​"I"​) };
 
 
string​ roman{​""​};
 
for​ (​auto​ arabicToRoman: arabicToRomanConversions)
 
while​ (arabic >= arabicToRoman.first)
 
{
 
roman += arabicToRoman.second;
 
arabic -= arabicToRoman.first;
 
}
 
return​ roman;
 
}
 
 
TEST(RomanConverter, CanConvertPositiveDigits) {
 
EXPECT_THAT(convert(1), Eq(​"I"​));
 
EXPECT_THAT(convert(2), Eq(​"II"​));
 
EXPECT_THAT(convert(3), Eq(​"III"​));
 
EXPECT_THAT(convert(4), Eq(​"IV"​));
 
EXPECT_THAT(convert(5), Eq(​"V"​));
 
EXPECT_THAT(convert(10), Eq(​"X"​));
 
EXPECT_THAT(convert(11), Eq(​"XI"​));
 
EXPECT_THAT(convert(12), Eq(​"XII"​));
 
EXPECT_THAT(convert(13), Eq(​"XIII"​));
 
EXPECT_THAT(convert(20), Eq(​"XX"​));
 
EXPECT_THAT(convert(50), Eq(​"L"​));
 
EXPECT_THAT(convert(80), Eq(​"LXXX"​));
 
EXPECT_THAT(convert(100), Eq(​"C"​));
 
EXPECT_THAT(convert(288), Eq(​"CCLXXXVIII"​));
 
EXPECT_THAT(convert(2999), Eq(​"MMCMXCIX"​));
 
EXPECT_THAT(convert(3447), Eq(​"MMMCDXLVII"​));
 
EXPECT_THAT(convert(1513), Eq(​"MDXIII"​));
 
}

And...done (except for enforcing the constraint that the number must be from 1 to 4,000).

We ended up with a short, simple, and concise implementation for our algorithm. A bit of web searching will show you many other solutions that have far more complexity for no additional benefit. Following TDD and refactoring appropriately can often guide you toward an optimal solution.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.210.205