Skip to content

Commit

Permalink
feature: string guarantee for String::binary() (#287)
Browse files Browse the repository at this point in the history
* 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<char, CharCount>

* feature: Implemented String::binary() with guarantee map

* feature: Use std::set<char> instead of std::string

Using std::set<char> 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()`
  • Loading branch information
braw-lee authored Nov 21, 2023
1 parent 119d752 commit 9c96e48
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 10 deletions.
52 changes: 49 additions & 3 deletions include/faker-cxx/String.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#pragma once

#include <iostream>
#include <limits>
#include <map>
#include <random>
#include <set>
#include <sstream>
#include <string>

Expand All @@ -17,6 +20,49 @@ enum class StringCasing
Upper
};

struct CharCount
{
unsigned int atleastCount{std::numeric_limits<unsigned int>::min()};
unsigned int atmostCount{std::numeric_limits<unsigned int>::max()};
};

/*
* A std::map where user can specify the count required for specific chars
*/
using GuaranteeMap = std::map<char, CharCount>;

/**
* @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<char>& 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<char,CharCount> 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:
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down
84 changes: 78 additions & 6 deletions src/modules/string/String.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#include "faker-cxx/String.h"

#include <algorithm>
#include <cassert>
#include <iostream>
#include <map>
#include <random>
#include <stdexcept>
#include <string>

#include "data/Characters.h"
#include "faker-cxx/Helper.h"
#include "faker-cxx/Number.h"

namespace faker
{
Expand All @@ -32,6 +37,36 @@ const std::map<HexPrefix, std::string> hexPrefixToStringMapping{
};
}

bool isValidGuarantee(GuaranteeMap& guarantee, std::set<char>& 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;
Expand Down Expand Up @@ -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<char> targetCharacters{'0', '1'};
// throw if guarantee is invalid
if (!isValidGuarantee(guarantee, targetCharacters, length))
{
binary += static_cast<char>(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<unsigned>(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)
Expand Down
110 changes: 109 additions & 1 deletion src/modules/string/StringTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <algorithm>
#include <random>
#include <stdexcept>

#include "gtest/gtest.h"

Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down

0 comments on commit 9c96e48

Please sign in to comment.