The Soundex Class

Since we’re ready to check in, let’s take a look at our solution. We decide that we don’t yet have a compelling reason to split out an implementation (.cpp) file, though that might be an essential part of making it production-ready in your system.

c2/40/SoundexTest.cpp
 
#include "gmock/gmock.h"
 
#include "Soundex.h"
 
using​ ​namespace​ testing;
 
 
class​ SoundexEncoding: ​public​ Test {
 
public​:
 
Soundex soundex;
 
};
 
 
TEST_F(SoundexEncoding, RetainsSoleLetterOfOneLetterWord) {
 
ASSERT_THAT(soundex.encode(​"A"​), Eq(​"A000"​));
 
}
 
 
TEST_F(SoundexEncoding, PadsWithZerosToEnsureThreeDigits) {
 
ASSERT_THAT(soundex.encode(​"I"​), Eq(​"I000"​));
 
}
 
 
TEST_F(SoundexEncoding, ReplacesConsonantsWithAppropriateDigits) {
 
ASSERT_THAT(soundex.encode(​"Ax"​), Eq(​"A200"​));
 
}
 
 
TEST_F(SoundexEncoding, IgnoresNonAlphabetics) {
 
ASSERT_THAT(soundex.encode(​"A#"​), Eq(​"A000"​));
 
}
 
 
TEST_F(SoundexEncoding, ReplacesMultipleConsonantsWithDigits) {
 
ASSERT_THAT(soundex.encode(​"Acdl"​), Eq(​"A234"​));
 
}
 
 
TEST_F(SoundexEncoding, LimitsLengthToFourCharacters) {
 
ASSERT_THAT(soundex.encode(​"Dcdlb"​).length(), Eq(4u));
 
}
 
 
TEST_F(SoundexEncoding, IgnoresVowelLikeLetters) {
 
ASSERT_THAT(soundex.encode(​"BaAeEiIoOuUhHyYcdl"​), Eq(​"B234"​));
 
}
 
 
TEST_F(SoundexEncoding, CombinesDuplicateEncodings) {
 
ASSERT_THAT(soundex.encodedDigit(​'b'​), Eq(soundex.encodedDigit(​'f'​)));
 
ASSERT_THAT(soundex.encodedDigit(​'c'​), Eq(soundex.encodedDigit(​'g'​)));
 
ASSERT_THAT(soundex.encodedDigit(​'d'​), Eq(soundex.encodedDigit(​'t'​)));
 
 
ASSERT_THAT(soundex.encode(​"Abfcgdt"​), Eq(​"A123"​));
 
}
 
 
TEST_F(SoundexEncoding, UppercasesFirstLetter) {
 
ASSERT_THAT(soundex.encode(​"abcd"​), StartsWith(​"A"​));
 
}
 
 
TEST_F(SoundexEncoding, IgnoresCaseWhenEncodingConsonants) {
 
ASSERT_THAT(soundex.encode(​"BCDL"​), Eq(soundex.encode(​"Bcdl"​)));
 
}
 
 
TEST_F(SoundexEncoding, CombinesDuplicateCodesWhen2ndLetterDuplicates1st) {
 
ASSERT_THAT(soundex.encode(​"Bbcd"​), Eq(​"B230"​));
 
}
 
 
TEST_F(SoundexEncoding, DoesNotCombineDuplicateEncodingsSeparatedByVowels) {
 
ASSERT_THAT(soundex.encode(​"Jbob"​), Eq(​"J110"​));
 
}
c2/40/Soundex.h
 
#ifndef Soundex_h
 
#define Soundex_h
 
 
#include <string>
 
#include <unordered_map>
 
 
#include "CharUtil.h"
 
#include "StringUtil.h"
 
 
class​ Soundex
 
{
 
public​:
 
static​ ​const​ size_t MaxCodeLength{4};
 
 
std::​string​ encode(​const​ std::​string​& word) ​const​ {
 
return​ stringutil::zeroPad(
 
stringutil::upperFront(stringutil::head(word)) +
 
stringutil::tail(encodedDigits(word)),
 
MaxCodeLength);
 
}
 
 
std::​string​ encodedDigit(​char​ letter) ​const​ {
 
const​ std::unordered_map<​char​, std::​string​> encodings {
 
{​'b'​, ​"1"​}, {​'f'​, ​"1"​}, {​'p'​, ​"1"​}, {​'v'​, ​"1"​},
 
{​'c'​, ​"2"​}, {​'g'​, ​"2"​}, {​'j'​, ​"2"​}, {​'k'​, ​"2"​}, {​'q'​, ​"2"​},
 
{​'s'​, ​"2"​}, {​'x'​, ​"2"​}, {​'z'​, ​"2"​},
 
{​'d'​, ​"3"​}, {​'t'​, ​"3"​},
 
{​'l'​, ​"4"​},
 
{​'m'​, ​"5"​}, {​'n'​, ​"5"​},
 
{​'r'​, ​"6"​}
 
};
 
auto​ it = encodings.find(charutil::lower(letter));
 
return​ it == encodings.end() ? NotADigit : it->second;
 
}
 
 
private​:
 
const​ std::​string​ NotADigit{​"*"​};
 
 
std::​string​ encodedDigits(​const​ std::​string​& word) ​const​ {
 
std::​string​ encoding;
 
encodeHead(encoding, word);
 
encodeTail(encoding, word);
 
return​ encoding;
 
}
 
 
void​ encodeHead(std::​string​& encoding, ​const​ std::​string​& word) ​const​ {
 
encoding += encodedDigit(word.front());
 
}
 
 
void​ encodeTail(std::​string​& encoding, ​const​ std::​string​& word) ​const​ {
 
for​ (​auto​ i = 1u; i < word.length(); i++)
 
if​ (!isComplete(encoding))
 
encodeLetter(encoding, word[i], word[i - 1]);
 
}
 
void​ encodeLetter(std::​string​& encoding, ​char​ letter, ​char​ lastLetter) ​const​ {
 
auto​ digit = encodedDigit(letter);
 
if​ (digit != NotADigit &&
 
(digit != lastDigit(encoding) || charutil::isVowel(lastLetter)))
 
encoding += digit;
 
}
 
 
std::​string​ lastDigit(​const​ std::​string​& encoding) ​const​ {
 
if​ (encoding.empty()) ​return​ NotADigit;
 
return​ std::​string​(1, encoding.back());
 
}
 
 
bool​ isComplete(​const​ std::​string​& encoding) ​const​ {
 
return​ encoding.length() == MaxCodeLength;
 
}
 
};
 
 
#endif

Wait...some things have changed! Where are the head, tail, and zeroPad functions? Where are isVowel and upper? And lastDigit looks different!

While you were busy reading, I did a bit of additional refactoring. Those missing functions, once defined in Soundex, now appear as free functions declared in StringUtil.h and CharUtil.h. With a small bit of refactoring, they now represent highly reusable functions.

Simply moving the functions out of Soundex isn’t quite sufficient. The functions in their new home as public utilities need to be aptly described so other programmers can understand their intent and use. That means they need tests to show by example how a programmer might use them in their own client code. You can find both the utility functions and their tests in the downloadable source code for this book.

We test-drove one possible solution for Soundex. How a solution evolves is entirely up to you. The more you practice TDD, the more likely your style will evolve. Code I test-drove only two years ago differs dramatically from code I test-drive today.

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

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