Skip to content

Commit de170c0

Browse files
authored
Utility to convert a number of bytes (e.g. 123456) to a formatted string (e.g. 123.46KB) (#103)
* Convert a number of bytes to a string (number and closest measure unit) Signed-off-by: tempate <danieldiaz@eprosima.com> * Tests Signed-off-by: tempate <danieldiaz@eprosima.com> * Replace std::find_if with std::upper_bound Signed-off-by: tempate <danieldiaz@eprosima.com> * Accept decimal numbers in to_bytes Signed-off-by: tempate <danieldiaz@eprosima.com> --------- Signed-off-by: tempate <danieldiaz@eprosima.com>
1 parent fd7406a commit de170c0

4 files changed

Lines changed: 142 additions & 13 deletions

File tree

cpp_utils/include/cpp_utils/utils.hpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ void to_uppercase(
9696
/**
9797
* @brief Convert a string to a number of bytes.
9898
*
99-
* The string must be a number followed by a magnitude (e.g. 10MB, 0.5GiB).
99+
* The string must be a number followed by a magnitude (e.g. 10MB, 5GiB).
100100
*
101101
* @param input string to convert
102102
* @return number of bytes
@@ -105,6 +105,21 @@ CPP_UTILS_DllAPI
105105
std::uint64_t to_bytes(
106106
const std::string& input);
107107

108+
/**
109+
* @brief Convert a number of bytes to a string.
110+
*
111+
* Examples:
112+
* - The number 0 will be converted to 0B.
113+
* - The number 1500 will be converted to 1.50KB.
114+
* - The number 555555555 will be converted to 555.56MB.
115+
*
116+
* @param bytes number to convert
117+
* @return \c bytes converted to a string and formatted to the closest magnitude
118+
*/
119+
CPP_UTILS_DllAPI
120+
std::string from_bytes(
121+
const std::uint64_t bytes);
122+
108123
template <typename T, bool Ptr = false>
109124
std::ostream& element_to_stream(
110125
std::ostream& os,

cpp_utils/src/cpp/utils.cpp

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include <algorithm>
2121
#include <assert.h>
22+
#include <cmath>
2223
#include <cstdint>
2324
#include <iomanip>
2425
#include <regex>
@@ -120,21 +121,21 @@ std::uint64_t to_bytes(
120121
};
121122

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

126-
if (!std::regex_match(input, matches, pattern) || matches.size() != 3)
127+
if (!std::regex_match(input, matches, pattern) || matches.size() != 4)
127128
{
128129
throw std::invalid_argument(
129-
"The quantity is not in the expected format. It should be a natural number followed by a unit (e.g. 10MB).");
130+
"The quantity is not in the expected format. It should be a rational number followed by a unit (e.g. 10MB).");
130131
}
131132

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

136137
// Extract the unit
137-
std::string unit_str = matches[2].str();
138+
std::string unit_str = matches[3].str();
138139
to_uppercase(unit_str);
139140

140141
if (units.find(unit_str) == units.end())
@@ -152,13 +153,60 @@ std::uint64_t to_bytes(
152153
throw std::invalid_argument("The number is too large to be converted to bytes.");
153154
}
154155

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

159159
return bytes;
160160
}
161161

162+
std::string from_bytes(
163+
const std::uint64_t bytes)
164+
{
165+
if (bytes == 0)
166+
{
167+
return "0B";
168+
}
169+
170+
static const std::map<std::uint64_t, std::string> units = {
171+
{1, "B"},
172+
{1000, "KB"},
173+
{1000 * 1000, "MB"},
174+
{1000 * 1000 * 1000, "GB"},
175+
{1000ULL * 1000 * 1000 * 1000, "TB"},
176+
{1000ULL * 1000 * 1000 * 1000 * 1000, "PB"}
177+
};
178+
179+
// Find the factor and unit
180+
const auto it = std::upper_bound(units.begin(), units.end(), bytes,
181+
[](const std::uint64_t bytes, const std::pair<std::uint64_t, std::string>& unit)
182+
{
183+
return bytes < unit.first;
184+
});
185+
186+
const auto factor = std::prev(it)->first;
187+
const auto unit = std::prev(it)->second;
188+
189+
// Calculate the number
190+
const auto number_double = static_cast<double>(bytes) / factor;
191+
const auto number_int = static_cast<std::uint64_t>(number_double);
192+
193+
// Format the number
194+
std::ostringstream oss;
195+
196+
if (std::fabs(number_double - number_int) < 0.01)
197+
{
198+
// The decimal part is negligible. Print the number as an integer.
199+
oss << number_int;
200+
}
201+
else
202+
{
203+
// The decimal part is significant. Print the number as a double with two decimal places.
204+
oss << std::fixed << std::setprecision(2) << number_double;
205+
}
206+
207+
return oss.str() + unit;
208+
}
209+
162210
void tsnh(
163211
const Formatter& formatter)
164212
{

cpp_utils/test/unittest/utils/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ set(TEST_LIST
3030
to_lowercase
3131
to_uppercase
3232
to_bytes
33+
from_bytes
3334
tsnh_call
3435
is_file_accessible
3536
combined_file_permissions

cpp_utils/test/unittest/utils/utilsTest.cpp

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,20 @@ TEST(utilsTest, to_bytes)
410410
const std::uint64_t bytes_expected = 51ULL * 1024 * 1024 * 1024 * 1024 * 1024;
411411
ASSERT_EQ(bytes, bytes_expected);
412412
}
413+
// Small decimal number
414+
{
415+
const std::string bytes_str = "1.50KB";
416+
const std::uint64_t bytes = to_bytes(bytes_str);
417+
const std::uint64_t bytes_expected = 1ULL * 1000 + 500;
418+
ASSERT_EQ(bytes, bytes_expected);
419+
}
420+
// Large decimal number
421+
{
422+
const std::string bytes_str = "23.9999GB";
423+
const std::uint64_t bytes = to_bytes(bytes_str);
424+
const std::uint64_t bytes_expected = ((23ULL * 1000 + 999) * 1000 + 900) * 1000;
425+
ASSERT_EQ(bytes, bytes_expected);
426+
}
413427

414428
// INVALID
415429

@@ -433,18 +447,69 @@ TEST(utilsTest, to_bytes)
433447
const std::string bytes_str = "100G";
434448
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
435449
}
436-
// Invalid number
437-
{
438-
const std::string bytes_str = "100.5MB";
439-
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
440-
}
441450
// Number too large
442451
{
443452
const std::string bytes_str = "18446744073709551616PiB";
444453
ASSERT_THROW(to_bytes(bytes_str), std::invalid_argument);
445454
}
446455
}
447456

457+
/**
458+
* Test \c from_bytes call
459+
*/
460+
TEST(utilsTest, from_bytes)
461+
{
462+
// Zero
463+
{
464+
const std::uint64_t bytes = 0ULL;
465+
const std::string bytes_str = from_bytes(bytes);
466+
const std::string bytes_str_expected = "0B";
467+
ASSERT_EQ(bytes_str, bytes_str_expected);
468+
}
469+
// Bytes
470+
{
471+
const std::uint64_t bytes = 100ULL;
472+
const std::string bytes_str = from_bytes(bytes);
473+
const std::string bytes_str_expected = "100B";
474+
ASSERT_EQ(bytes_str, bytes_str_expected);
475+
}
476+
// Kilobytes
477+
{
478+
const std::uint64_t bytes = 555ULL * 1000 + 559;
479+
const std::string bytes_str = from_bytes(bytes);
480+
const std::string bytes_str_expected = "555.56KB";
481+
ASSERT_EQ(bytes_str, bytes_str_expected);
482+
}
483+
// Megabytes
484+
{
485+
const std::uint64_t bytes = (100ULL * 1000 + 104) * 1000;
486+
const std::string bytes_str = from_bytes(bytes);
487+
const std::string bytes_str_expected = "100.10MB";
488+
ASSERT_EQ(bytes_str, bytes_str_expected);
489+
}
490+
// Gigabytes
491+
{
492+
const std::uint64_t bytes = 82ULL * 1000 * 1000 * 1000;
493+
const std::string bytes_str = from_bytes(bytes);
494+
const std::string bytes_str_expected = "82GB";
495+
ASSERT_EQ(bytes_str, bytes_str_expected);
496+
}
497+
// Terabytes
498+
{
499+
const std::uint64_t bytes = 742ULL * 1000 * 1000 * 1000 * 1000;
500+
const std::string bytes_str = from_bytes(bytes);
501+
const std::string bytes_str_expected = "742TB";
502+
ASSERT_EQ(bytes_str, bytes_str_expected);
503+
}
504+
// Extra Large
505+
{
506+
const std::uint64_t bytes = 12345ULL * 1000 * 1000 * 1000 * 1000 * 1000;
507+
const std::string bytes_str = from_bytes(bytes);
508+
const std::string bytes_str_expected = "12345PB";
509+
ASSERT_EQ(bytes_str, bytes_str_expected);
510+
}
511+
}
512+
448513
/**
449514
* Test \c tsnh call
450515
*/

0 commit comments

Comments
 (0)