diff --git a/.gitignore b/.gitignore index e9178dad4..7c8da29c1 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ build-linux-gxx build-linux-clang #clangd index files .cache/ +#vim swap files +*.sw? diff --git a/include/faker-cxx/String.h b/include/faker-cxx/String.h index ffb82060d..b0c840bd5 100644 --- a/include/faker-cxx/String.h +++ b/include/faker-cxx/String.h @@ -65,6 +65,10 @@ std::string generateAtleastString(const GuaranteeMap& guarantee); class String { +private: + static std::string generateStringWithGuarantee(GuaranteeMap& guarantee, std::set& targetCharacters, + unsigned int length); + public: /** * @brief Generates an Universally Unique Identifier with version 4. @@ -224,12 +228,13 @@ class String /** * @brief Generates a binary string. * + * @param guarantee A map specifying char count constraints if any * @param length The number of digits to generate. Defaults to `1`. * * @returns Binary string. * * @code - * String::binary(8) // "0b01110101" + * String::binary({}, 8) // "0b01110101" * @endcode */ static std::string binary(GuaranteeMap&& guarantee = {}, unsigned length = 1); @@ -237,14 +242,15 @@ class String /** * @brief Generates an octal string. * + * @param guarantee A map specifying char count constraints if any * @param length The number of digits to generate. Defaults to `1`. * * @returns Octal string. * * @code - * String::octal(8) // "0o52561721" + * String::octal({}, 8) // "0o52561721" * @endcode */ - static std::string octal(unsigned length = 1); + static std::string octal(GuaranteeMap&& guarantee = {}, unsigned length = 1); }; } diff --git a/src/modules/string/String.cpp b/src/modules/string/String.cpp index 4a118e009..1a2e2e6cd 100644 --- a/src/modules/string/String.cpp +++ b/src/modules/string/String.cpp @@ -67,6 +67,48 @@ std::string generateAtleastString(const GuaranteeMap& guarantee) return result; } +std::string String::generateStringWithGuarantee(GuaranteeMap& guarantee, std::set& targetCharacters, + unsigned int length) +{ + std::string output{}; + output += generateAtleastString(guarantee); + // string with least required chars cannot be greater than the total length + assert(output.size() <= length); + // we will generate chars for remaining length only + length -= static_cast(output.size()); + for (unsigned i = 0; i < length; ++i) + { + char generatedChar; + // generate chars till we find a usable char + while (true) + { + // pick random char from targetCharacters + generatedChar = Helper::setElement(targetCharacters); + + auto it = guarantee.find(generatedChar); + // if no constraint on generated char, break out of loop + if (it == guarantee.end()) + break; + auto remainingUses = it->second.atmostCount - it->second.atleastCount; + if (remainingUses > 0) + { + // decrement no of possible uses as we will use it right now + --it->second.atmostCount; + break; + } + // remove this char from targetCharacters as it is no longer valid and regenerate char + else + { + targetCharacters.erase(it->first); + } + } + output += generatedChar; + } + // shuffle the generated string as the atleast string generated earlier was not generated randomly + output = Helper::shuffleString(output); + return output; +} + std::string String::sample(unsigned int length) { std::string sample; @@ -166,6 +208,7 @@ std::string String::hexadecimal(unsigned int length, HexCasing casing, HexPrefix std::string String::binary(GuaranteeMap&& guarantee, unsigned int length) { + // numbers used by binary representation std::set targetCharacters{'0', '1'}; // throw if guarantee is invalid if (!isValidGuarantee(guarantee, targetCharacters, length)) @@ -173,54 +216,19 @@ std::string String::binary(GuaranteeMap&& guarantee, unsigned int length) throw std::invalid_argument{"Invalid guarantee."}; } - std::string binary{}; - binary += generateAtleastString(guarantee); - // string with least required chars cannot be greater than the total length - assert(binary.size() <= length); - // we will generate chars for remaining length only - length -= static_cast(binary.size()); - for (unsigned i = 0; i < length; ++i) - { - char generatedChar; - // generate chars till we find a usable char - while (true) - { - // pick random char from targetCharacters - generatedChar = Helper::setElement(targetCharacters); - - auto it = guarantee.find(generatedChar); - // if no constraint on generated char, break out of loop - if (it == guarantee.end()) - break; - auto remainingUses = it->second.atmostCount - it->second.atleastCount; - if (remainingUses > 0) - { - // decrement no of possible uses as we will use it right now - --it->second.atmostCount; - break; - } - // remove this char from targetCharacters as it is no longer valid and regenerate char - else - { - targetCharacters.erase(it->first); - } - } - binary += generatedChar; - } - // shuffle the generated string as the atleast string generated earlier was not generated randomly - binary = Helper::shuffleString(binary); - return "0b" + binary; + return "0b" + generateStringWithGuarantee(guarantee, targetCharacters, length); } -std::string String::octal(unsigned int length) +std::string String::octal(GuaranteeMap&& guarantee, unsigned int length) { - std::string octal{"0o"}; - - for (unsigned i = 0; i < length; i++) + // numbers used by octal representation + std::set targetCharacters{'0', '1', '2', '3', '4', '5', '6', '7'}; + // throw if guarantee is invalid + if (!isValidGuarantee(guarantee, targetCharacters, length)) { - octal += static_cast(Number::integer(7)); + throw std::invalid_argument{"Invalid guarantee."}; } - return octal; + return "0o" + generateStringWithGuarantee(guarantee, targetCharacters, length); } } diff --git a/src/modules/string/StringTest.cpp b/src/modules/string/StringTest.cpp index 5b55236f0..797162ee0 100644 --- a/src/modules/string/StringTest.cpp +++ b/src/modules/string/StringTest.cpp @@ -477,7 +477,7 @@ TEST_F(StringTest, shouldGenerateOctalWithPrefix) { const auto octalLength = 8; - const auto octal = String::octal(octalLength); + const auto octal = String::octal({}, octalLength); const auto prefix = octal.substr(0, 2); const auto octalNumber = octal.substr(2); @@ -488,3 +488,103 @@ TEST_F(StringTest, shouldGenerateOctalWithPrefix) std::ranges::any_of(octal, [](char octalNumberCharacter) { return std::string("01234567").find(octalNumberCharacter) != std::string::npos; })); } + +TEST_F(StringTest, shouldGenerateOctalWithGuarantee1) +{ + const auto octalLength = 10; + // exactly 2 '3' - 0 '5' + // atleast 2 '0' - 3 '6' - 1 '7' + // atmost 10 '6' - 10 '7' + GuaranteeMap guarantee{{'0', {2}}, {'3', {2, 2}}, {'5', {0, 0}}, {'6', {3, 10}}, {'7', {1, 10}}}; + // it is a random function so lets test for 20 random generations + for (int i = 0; i < 20; ++i) + { + const auto octal = String::octal(std::move(guarantee), octalLength); + + const auto prefix = octal.substr(0, 2); + const auto octalNumber = octal.substr(2); + + ASSERT_EQ(octalNumber.size(), octalLength); + ASSERT_EQ(prefix, "0o"); + ASSERT_TRUE( + std::ranges::any_of(octal, [](char octalNumberCharacter) + { return std::string("01234567").find(octalNumberCharacter) != std::string::npos; })); + auto count_0 = std::ranges::count(octalNumber, '0'); + auto count_3 = std::ranges::count(octalNumber, '3'); + auto count_5 = std::ranges::count(octalNumber, '5'); + auto count_6 = std::ranges::count(octalNumber, '6'); + auto count_7 = std::ranges::count(octalNumber, '7'); + ASSERT_TRUE(count_0 >= 2); + ASSERT_TRUE(count_3 == 2); + ASSERT_TRUE(count_5 == 0); + ASSERT_TRUE(count_6 >= 3 && count_6 <= 10); + ASSERT_TRUE(count_7 >= 1 && count_7 <= 10); + } +} + +TEST_F(StringTest, shouldGenerateOctalWithGuarantee2) +{ + const auto octalLength = 20; + // exactly 0 '2' '3' '4' '5' '6' '7' + // atleast 18 '0' + GuaranteeMap guarantee{{'0', {18}}, {'2', {0, 0}}, {'3', {0, 0}}, {'4', {0, 0}}, + {'5', {0, 0}}, {'6', {0, 0}}, {'7', {0, 0}}}; + // it is a random function so lets test for 20 random generations + for (int i = 0; i < 20; ++i) + { + const auto octal = String::octal(std::move(guarantee), octalLength); + + const auto prefix = octal.substr(0, 2); + const auto octalNumber = octal.substr(2); + + ASSERT_EQ(octalNumber.size(), octalLength); + ASSERT_EQ(prefix, "0o"); + ASSERT_TRUE( + std::ranges::any_of(octal, [](char octalNumberCharacter) + { return std::string("01234567").find(octalNumberCharacter) != std::string::npos; })); + auto count_0 = std::ranges::count(octalNumber, '0'); + auto count_2 = std::ranges::count(octalNumber, '2'); + auto count_3 = std::ranges::count(octalNumber, '3'); + auto count_4 = std::ranges::count(octalNumber, '4'); + auto count_5 = std::ranges::count(octalNumber, '5'); + auto count_6 = std::ranges::count(octalNumber, '6'); + auto count_7 = std::ranges::count(octalNumber, '7'); + + ASSERT_TRUE(count_0 >= 18); + ASSERT_TRUE(count_2 == 0); + ASSERT_TRUE(count_3 == 0); + ASSERT_TRUE(count_4 == 0); + ASSERT_TRUE(count_5 == 0); + ASSERT_TRUE(count_6 == 0); + ASSERT_TRUE(count_7 == 0); + } +} + +TEST_F(StringTest, invalidGuaranteeForOctal1) +{ + const auto octalLength = 10; + // exactly 0 '4' + // atleast 8 '0' - 9 '2' 9 '3' // invalid // total string size will be atleast 26 which is wrong + // atmost + GuaranteeMap guarantee{{'0', {8}}, {'2', {9}}, {'3', {9}}, {'4', {0, 0}}}; + ASSERT_THROW(String::octal(std::move(guarantee), octalLength), std::invalid_argument); +} + +TEST_F(StringTest, invalidGuaranteeForOctal2) +{ + const auto octalLength = 20; + // atmost 2 '0' '1' '2' '3' '4' '5' '6' '7' // invalid // octal string won't exceed 16 which is wrong + GuaranteeMap guarantee{{'0', {0, 2}}, {'1', {0, 2}}, {'2', {0, 2}}, {'3', {0, 2}}, + {'4', {0, 2}}, {'5', {0, 2}}, {'6', {0, 2}}, {'7', {0, 2}}}; + ASSERT_THROW(String::octal(std::move(guarantee), octalLength), std::invalid_argument); +} + +TEST_F(StringTest, invalidGuaranteeForOctal3) +{ + const auto octalLength = 20; + + // atleast 2 '8' // invalid // octal numbers cannot have '8' + // atmost 3 '8' + GuaranteeMap guarantee{{'0', {0, 2}}, {'1', {0, 2}}, {'8', {2, 3}}, {'2', {0, 2}}, {'3', {0, 2}}}; + ASSERT_THROW(String::octal(std::move(guarantee), octalLength), std::invalid_argument); +}