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.
18.117.93.0