From 9c96e48f203c35520336dd95b115d7999983a85b Mon Sep 17 00:00:00 2001 From: braw-lee <93831198+braw-lee@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:21:34 +0530 Subject: [PATCH] feature: string guarantee for `String::binary()` (#287) * feature: Implemented `faker::isValidGuarantee()` for guarantee validation in string module * feature: Implemented `faker::generateAtleastString()` * docs: Added comments for `faker::generateAtleastString`, named alias faker::GuaranteeMap for easy use of std::map * feature: Implemented String::binary() with guarantee map * feature: Use std::set instead of std::string Using std::set instead of std::string in String::binary() to help with generation guarantee * fix: GCC build fix Casted binary.size() to unsigned to remove gcc build error * tests: Added tests for string guarantees in `String::binary()` * tests: Added more tests for string guarantee in `String::binary()` --- include/faker-cxx/String.h | 52 +++++++++++++- src/modules/string/String.cpp | 84 +++++++++++++++++++++-- src/modules/string/StringTest.cpp | 110 +++++++++++++++++++++++++++++- 3 files changed, 236 insertions(+), 10 deletions(-) diff --git a/include/faker-cxx/String.h b/include/faker-cxx/String.h index 9c350d0a7..ffb82060d 100644 --- a/include/faker-cxx/String.h +++ b/include/faker-cxx/String.h @@ -1,7 +1,10 @@ #pragma once #include +#include +#include #include +#include #include #include @@ -17,6 +20,49 @@ enum class StringCasing Upper }; +struct CharCount +{ + unsigned int atleastCount{std::numeric_limits::min()}; + unsigned int atmostCount{std::numeric_limits::max()}; +}; + +/* + * A std::map where user can specify the count required for specific chars + */ +using GuaranteeMap = std::map; + +/** + * @brief Checks if the given guarantee map is valid for given targetCharacters and length. + * + * @returns a bool. + * + * @param guarantee A std::map that maps the count range of specific characters required + * @param targetCharacters A std::string consisting of all chars available for that string generating function + * @param length The number of characters to generate. + * + * @code + * GuaranteeMap guarantee = {{'0',{5,10}},{'1',{6,10}}}; + * std::string targetCharacters = "01"; + * unsigned int length = 10; + * faker::isValidGuarantee(guarantee,targetCharacters,length) // false + * @endcode + */ +bool isValidGuarantee(GuaranteeMap& guarantee, std::set& targetCharacters, unsigned int length); + +/* + * @brief Generates the least required string for a given guarantee map + * + * @returns least required std::string + * + * @param guarantee A std::map which stores the guarantee specified by the user + * + * @code + * GuaranteeMap guarantee { {'0',{3,10}},{'a',{6,8}} }; // "000aaaaaa" + * faker::generateAtleastString(guarantee); + * @endcode + */ +std::string generateAtleastString(const GuaranteeMap& guarantee); + class String { public: @@ -126,8 +172,8 @@ class String * * @param length The number of characters to generate. Defaults to `1`. * @param casing The casing of the characters. Defaults to `StringCasing::Mixed`. - * @param excludeCharacters The characters to be excluded from alphanumeric characters to generate string from. - * Defaults to ``. + * @param excludeCharacters The characters to be excluded from alphanumeric characters to generate + * string from. Defaults to ``. * * @returns Alphanumeric string. * @@ -186,7 +232,7 @@ class String * String::binary(8) // "0b01110101" * @endcode */ - static std::string binary(unsigned length = 1); + static std::string binary(GuaranteeMap&& guarantee = {}, unsigned length = 1); /** * @brief Generates an octal string. diff --git a/src/modules/string/String.cpp b/src/modules/string/String.cpp index 2e08e1980..d80fff49f 100644 --- a/src/modules/string/String.cpp +++ b/src/modules/string/String.cpp @@ -1,11 +1,16 @@ #include "faker-cxx/String.h" +#include +#include #include #include #include +#include +#include #include "data/Characters.h" #include "faker-cxx/Helper.h" +#include "faker-cxx/Number.h" namespace faker { @@ -32,6 +37,36 @@ const std::map hexPrefixToStringMapping{ }; } +bool isValidGuarantee(GuaranteeMap& guarantee, std::set& targetCharacters, unsigned int length) +{ + unsigned int atleastCountSum{}; + unsigned int atmostCountSum{}; + for (auto& it : guarantee) + { + // if a char in guarantee is not in char set, it is an invalid guarantee + if (targetCharacters.find(it.first) == targetCharacters.end()) + return false; + atleastCountSum += it.second.atleastCount; + atmostCountSum += it.second.atmostCount; + } + // if atleastCount sums up greater than total length of string, it is an invalid guarantee + // if all chars in targetCharacters are mapped in guarantee, we need to check for validity of atmostCount + // if atmostCount sumps up less than total length of string, it in an invalid guarantee + if (atleastCountSum > length || (guarantee.size() == targetCharacters.size() && atmostCountSum < length)) + return false; + return true; +} + +std::string generateAtleastString(const GuaranteeMap& guarantee) +{ + std::string result; + for (auto& it : guarantee) + { + result += std::string(it.second.atleastCount, it.first); + } + return result; +} + std::string String::sample(unsigned int length) { std::string sample; @@ -129,16 +164,53 @@ std::string String::hexadecimal(unsigned int length, HexCasing casing, HexPrefix return hexadecimal; } -std::string String::binary(unsigned int length) +std::string String::binary(GuaranteeMap&& guarantee, unsigned int length) { - std::string binary{"0b"}; - - for (unsigned i = 0; i < length; i++) + std::set targetCharacters{'0', '1'}; + // throw if guarantee is invalid + if (!isValidGuarantee(guarantee, targetCharacters, length)) { - binary += static_cast(Number::integer(1)); + throw std::invalid_argument{"Invalid guarantee."}; } - return binary; + 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 + std::mt19937 gen(std::random_device{}()); + std::sample(targetCharacters.begin(), targetCharacters.end(), &generatedChar, 1, gen); + + 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; } std::string String::octal(unsigned int length) diff --git a/src/modules/string/StringTest.cpp b/src/modules/string/StringTest.cpp index 069c2c91e..5b55236f0 100644 --- a/src/modules/string/StringTest.cpp +++ b/src/modules/string/StringTest.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "gtest/gtest.h" @@ -354,7 +355,7 @@ TEST_F(StringTest, shouldGenerateBinary) { const auto binaryLength = 8; - const auto binary = String::binary(binaryLength); + const auto binary = String::binary({}, binaryLength); const auto prefix = binary.substr(0, 2); const auto binaryNumber = binary.substr(2); @@ -365,6 +366,113 @@ TEST_F(StringTest, shouldGenerateBinary) { return std::string("01").find(binaryNumberCharacter) != std::string::npos; })); } +TEST_F(StringTest, shouldGenerateBinaryWithGuarantee1) +{ + const auto binaryLength = 9; + + // atleast 3 '0' and 2 '1' + // atmost 7 '0' and 7 '1' + faker::GuaranteeMap guarantee{{'0', {3, 7}}, {'1', {2, 7}}}; + const auto binary = String::binary(std::move(guarantee), binaryLength); + + const auto prefix = binary.substr(0, 2); + const auto binaryNumber = binary.substr(2); + + ASSERT_EQ(binaryNumber.size(), binaryLength); + ASSERT_EQ(prefix, "0b"); + ASSERT_TRUE(std::ranges::any_of(binaryNumber, [](char binaryNumberCharacter) + { return std::string("01").find(binaryNumberCharacter) != std::string::npos; })); + auto count_0 = std::count(binaryNumber.begin(), binaryNumber.end(), '0'); + auto count_1 = std::count(binaryNumber.begin(), binaryNumber.end(), '1'); + ASSERT_TRUE(count_0 >= 3 && count_0 <= 7); + ASSERT_TRUE(count_1 >= 2 && count_1 <= 7); +} +TEST_F(StringTest, shouldGenerateBinaryWithGuarantee2) +{ + const auto binaryLength = 10; + + // exactly 8 '0' and 2 '1' + faker::GuaranteeMap guarantee{{'0', {8, 8}}, {'1', {2, 2}}}; + const auto binary = String::binary(std::move(guarantee), binaryLength); + + const auto prefix = binary.substr(0, 2); + const auto binaryNumber = binary.substr(2); + + ASSERT_EQ(binaryNumber.size(), binaryLength); + ASSERT_EQ(prefix, "0b"); + ASSERT_TRUE(std::ranges::any_of(binaryNumber, [](char binaryNumberCharacter) + { return std::string("01").find(binaryNumberCharacter) != std::string::npos; })); + auto count_0 = std::count(binaryNumber.begin(), binaryNumber.end(), '0'); + auto count_1 = std::count(binaryNumber.begin(), binaryNumber.end(), '1'); + ASSERT_TRUE(count_0 == 8); + ASSERT_TRUE(count_1 == 2); +} +TEST_F(StringTest, shouldGenerateBinaryWithGuarantee3) +{ + const auto binaryLength = 10; + + // atleast 10 '0' + faker::GuaranteeMap guarantee{{'0', {10}}}; + const auto binary = String::binary(std::move(guarantee), binaryLength); + + const auto prefix = binary.substr(0, 2); + const auto binaryNumber = binary.substr(2); + + ASSERT_EQ(binaryNumber.size(), binaryLength); + ASSERT_EQ(prefix, "0b"); + ASSERT_TRUE(std::ranges::any_of(binaryNumber, [](char binaryNumberCharacter) + { return std::string("01").find(binaryNumberCharacter) != std::string::npos; })); + auto count_0 = std::count(binaryNumber.begin(), binaryNumber.end(), '0'); + ASSERT_TRUE(count_0 == 10); +} + +TEST_F(StringTest, shouldGenerateBinaryWithGuarantee4) +{ + const auto binaryLength = 10; + + // atmost 0 '0' + faker::GuaranteeMap guarantee{{'0', {0, 0}}}; + const auto binary = String::binary(std::move(guarantee), binaryLength); + + const auto prefix = binary.substr(0, 2); + const auto binaryNumber = binary.substr(2); + + ASSERT_EQ(binaryNumber.size(), binaryLength); + ASSERT_EQ(prefix, "0b"); + ASSERT_TRUE(std::ranges::any_of(binaryNumber, [](char binaryNumberCharacter) + { return std::string("01").find(binaryNumberCharacter) != std::string::npos; })); + auto count_0 = std::count(binaryNumber.begin(), binaryNumber.end(), '0'); + ASSERT_TRUE(count_0 == 0); +} + +TEST_F(StringTest, invalidGuaranteeForBinary1) +{ + const auto binaryLength = 10; + + // atleast 6 '0' and 6 '1' // invalid // total string size will be 12 which is wrong + // atleast 10 '0' and 10 '1' + faker::GuaranteeMap guarantee{{'0', {6, 10}}, {'1', {6, 10}}}; + EXPECT_THROW(String::binary(std::move(guarantee), binaryLength), std::invalid_argument); +} + +TEST_F(StringTest, invalidGuaranteeForBinary2) +{ + const auto binaryLength = 20; + + // atleast 6 '0' and 6 '1' + // atleast 10 '0' and 8 '1' // invalid // total string size won't exceed 18 which is wrong + faker::GuaranteeMap guarantee{{'0', {6, 10}}, {'1', {6, 8}}}; + EXPECT_THROW(String::binary(std::move(guarantee), binaryLength), std::invalid_argument); +} + +TEST_F(StringTest, invalidGuaranteeForBinary3) +{ + const auto binaryLength = 10; + // atleast 4 '0' and 3 'a' // invalid // binary string should consist of only '0' and '1' + faker::GuaranteeMap guarantee{{'0', {4}}, {'a', {3}}}; + EXPECT_THROW(String::binary(std::move(guarantee), binaryLength), std::invalid_argument); +} + TEST_F(StringTest, shouldGenerateOctalWithPrefix) { const auto octalLength = 8;