Skip to content

Commit

Permalink
Utility to convert a string (e.g. 100MB) to a number of bytes (#96)
Browse files Browse the repository at this point in the history
* Convert a string (number and measure unit) to a number of bytes

Signed-off-by: tempate <[email protected]>

* Apply suggestions

Signed-off-by: tempate <[email protected]>

* Replace uint64_t with std::uint64_t

Signed-off-by: tempate <[email protected]>

* Apply suggestions

Signed-off-by: tempate <[email protected]>

* Tests

Signed-off-by: tempate <[email protected]>

* Fixes after test

Signed-off-by: tempate <[email protected]>

* Uncrustify

Signed-off-by: tempate <[email protected]>

* Fix test after applying suggestions

Signed-off-by: tempate <[email protected]>

* Windows fix

Signed-off-by: tempate <[email protected]>

---------

Signed-off-by: tempate <[email protected]>
  • Loading branch information
Tempate authored Mar 7, 2024
1 parent 6675c61 commit 14d8de3
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 3 deletions.
16 changes: 15 additions & 1 deletion cpp_utils/include/cpp_utils/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#pragma once

#include <cstdint>
#include <map>
#include <memory>
#include <set>
Expand Down Expand Up @@ -88,9 +89,22 @@ void to_lowercase(
*
* @param [in,out] st : string to modify
*/
CPP_UTILS_DllAPI void to_uppercase(
CPP_UTILS_DllAPI
void to_uppercase(
std::string& st) noexcept;

/**
* @brief Convert a string to a number of bytes.
*
* The string must be a number followed by a magnitude (e.g. 10MB, 0.5GiB).
*
* @param input string to convert
* @return number of bytes
*/
CPP_UTILS_DllAPI
std::uint64_t to_bytes(
const std::string& input);

template <typename T, bool Ptr = false>
std::ostream& element_to_stream(
std::ostream& os,
Expand Down
64 changes: 62 additions & 2 deletions cpp_utils/src/cpp/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@

#include <algorithm>
#include <assert.h>
#include <set>
#include <stdlib.h>
#include <cstdint>
#include <iomanip>
#include <regex>
#include <set>
#include <sstream>
#include <stdexcept>
#include <stdlib.h>

#include <cpp_utils/exception/PreconditionNotMet.hpp>
#include <cpp_utils/Log.hpp>
Expand Down Expand Up @@ -99,6 +102,63 @@ void to_uppercase(
});
}

std::uint64_t to_bytes(
const std::string& input)
{
static const std::map<std::string, std::uint64_t> units = {
{"B", 1},
{"KB", 1000},
{"MB", 1000 * 1000},
{"GB", 1000 * 1000 * 1000},
{"TB", 1000ULL * 1000 * 1000 * 1000},
{"PB", 1000ULL * 1000 * 1000 * 1000 * 1000},
{"KIB", 1024},
{"MIB", 1024 * 1024},
{"GIB", 1024 * 1024 * 1024},
{"TIB", 1024ULL * 1024 * 1024 * 1024},
{"PIB", 1024ULL * 1024 * 1024 * 1024 * 1024}
};

// Find the number and the unit
std::regex pattern("^(\\d+)\\s*([a-zA-Z]+)$");
std::smatch matches;

if (!std::regex_match(input, matches, pattern) || matches.size() != 3)
{
throw std::invalid_argument(
"The quantity is not in the expected format. It should be a natural number followed by a unit (e.g. 10MB).");
}

// Extract the number
std::string number_str = matches[1].str();
double number = std::stod(number_str);

// Extract the unit
std::string unit_str = matches[2].str();
to_uppercase(unit_str);

if (units.find(unit_str) == units.end())
{
throw std::invalid_argument(
"The unit is not valid. The valid units are: B, KB, MB, GB, TB, PB, KiB, MiB, GiB, TiB, PiB.");
}

const auto unit = units.at(unit_str);

// Check whether the product of number * unit overflows
// @note: The parentheses are necessary to distinguish std::max from the max macro (Windows).
if (number > (std::numeric_limits<std::uint64_t>::max)() / unit)
{
throw std::invalid_argument("The number is too large to be converted to bytes.");
}

// The explicit cast to uint64_t is safe since the number has already been checked to fit.
// The product is also safe since the possible overflow has also been checked.
const std::uint64_t bytes = static_cast<std::uint64_t>(number) * unit;

return bytes;
}

void tsnh(
const Formatter& formatter)
{
Expand Down
1 change: 1 addition & 0 deletions cpp_utils/test/unittest/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ set(TEST_LIST
are_set_of_ptr_equal_int
to_lowercase
to_uppercase
to_bytes
tsnh_call
is_file_accessible
combined_file_permissions
Expand Down
86 changes: 86 additions & 0 deletions cpp_utils/test/unittest/utils/utilsTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// limitations under the License.

#include <algorithm>
#include <cstdint>
#include <stdexcept>

#include <cpp_utils/testing/gtest_aux.hpp>
#include <gtest/gtest.h>
Expand Down Expand Up @@ -359,6 +361,90 @@ TEST(utilsTest, to_uppercase)
}
}

/**
* Test \c to_bytes call
*/
TEST(utilsTest, to_bytes)
{
// VALID

// Invariant
{
const std::string bytes_str = "100B";
const std::uint64_t bytes = to_bytes(bytes_str);
const std::uint64_t bytes_expected = 100ULL;
ASSERT_EQ(bytes, bytes_expected);
}
// Lowercase
{
const std::string bytes_str = "123kb";
const std::uint64_t bytes = to_bytes(bytes_str);
const std::uint64_t bytes_expected = 123ULL * 1000;
ASSERT_EQ(bytes, bytes_expected);
}
// Uppercase
{
const std::string bytes_str = "100MB";
const std::uint64_t bytes = to_bytes(bytes_str);
const std::uint64_t bytes_expected = 100ULL * 1000 * 1000;
ASSERT_EQ(bytes, bytes_expected);
}
// Milibytes
{
const std::string bytes_str = "82GiB";
const std::uint64_t bytes = to_bytes(bytes_str);
const std::uint64_t bytes_expected = 82ULL * 1024 * 1024 * 1024;
ASSERT_EQ(bytes, bytes_expected);
}
// Large
{
const std::string bytes_str = "742TB";
const std::uint64_t bytes = to_bytes(bytes_str);
const std::uint64_t bytes_expected = 742ULL * 1000 * 1000 * 1000 * 1000;
ASSERT_EQ(bytes, bytes_expected);
}
// Extra Large
{
const std::string bytes_str = "51pib";
const std::uint64_t bytes = to_bytes(bytes_str);
const std::uint64_t bytes_expected = 51ULL * 1024 * 1024 * 1024 * 1024 * 1024;
ASSERT_EQ(bytes, bytes_expected);
}

// INVALID

// Empty
{
const std::string bytes_str = "";
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
}
// No unit
{
const std::string bytes_str = "100";
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
}
// No number
{
const std::string bytes_str = "MB";
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
}
// Invalid unit
{
const std::string bytes_str = "100G";
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
}
// Invalid number
{
const std::string bytes_str = "100.5MB";
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
}
// Number too large
{
const std::string bytes_str = "18446744073709551616PiB";
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
}
}

/**
* Test \c tsnh call
*/
Expand Down

0 comments on commit 14d8de3

Please sign in to comment.