diff --git a/test/source/scwx/util/strings.test.cpp b/test/source/scwx/util/strings.test.cpp index e91c95d9..825b3999 100644 --- a/test/source/scwx/util/strings.test.cpp +++ b/test/source/scwx/util/strings.test.cpp @@ -7,6 +7,35 @@ namespace scwx namespace util { +class BytesToStringTest : + public testing::TestWithParam> +{ +}; + +TEST_P(BytesToStringTest, BytesToString) +{ + auto& [bytes, expected] = GetParam(); + + std::string s = BytesToString(bytes); + + EXPECT_EQ(s, expected); +} + +INSTANTIATE_TEST_SUITE_P(StringsTest, + BytesToStringTest, + testing::Values(std::make_pair(123, "123 bytes"), + std::make_pair(1000, "0.98 KB"), + std::make_pair(1018, "0.99 KB"), + std::make_pair(1024, "1.0 KB"), + std::make_pair(1127, "1.1 KB"), + std::make_pair(1260, "1.23 KB"), + std::make_pair(24012, "23.4 KB"), + std::make_pair(353974, "346 KB"), + std::make_pair(1024000, "0.98 MB"), + std::make_pair(1048576000, "0.98 GB"), + std::make_pair(1073741824000, + "0.98 TB"))); + TEST(StringsTest, ParseTokensColor) { static const std::string line {"Color: red green blue alpha discarded"}; diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index ad0d1f9c..e2821331 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -9,6 +9,16 @@ namespace scwx namespace util { +/** + * @brief Print the number of bytes using a dynamic suffix and limited number of + * decimal points. + * + * @param [in] bytes Number of bytes + * + * @return Human readable size string + */ +std::string BytesToString(std::ptrdiff_t bytes); + /** * @brief Parse a list of tokens from a string * diff --git a/wxdata/source/scwx/util/strings.cpp b/wxdata/source/scwx/util/strings.cpp index 7067d821..d5ac8cfc 100644 --- a/wxdata/source/scwx/util/strings.cpp +++ b/wxdata/source/scwx/util/strings.cpp @@ -4,12 +4,76 @@ #include #include +#include namespace scwx { namespace util { +std::string BytesToString(std::ptrdiff_t bytes) +{ + auto FormatNumber = [](double number) -> std::string + { + int precision; + + // Determine precision + if (number >= 100.0) + { + precision = 0; + } + else if (number >= 10.0) + { + precision = 1; + } + else + { + precision = 2; + } + + // Format the number + std::string formattedNum = fmt::format("{:.{}f}", number, precision); + + // Remove trailing zeroes + std::size_t found = formattedNum.find_last_not_of('0'); + if (found != std::string::npos && formattedNum[found] == '.') + { + // Keep one trailing zero if it's a decimal point + found++; + } + formattedNum.erase(found + 1, std::string::npos); + + return formattedNum; + }; + + // Print with appropriate suffix + if (bytes < 1000) + { + return fmt::format("{} bytes", bytes); + } + + double kilobytes = bytes / 1024.0; + if (kilobytes < 1000.0) + { + return fmt::format("{} KB", FormatNumber(kilobytes)); + } + + double megabytes = kilobytes / 1024.0; + if (megabytes < 1000.0) + { + return fmt::format("{} MB", FormatNumber(megabytes)); + } + + double gigabytes = megabytes / 1024.0; + if (gigabytes < 1000.0) + { + return fmt::format("{} GB", FormatNumber(gigabytes)); + } + + double terabytes = gigabytes / 1024.0; + return fmt::format("{} TB", FormatNumber(terabytes)); +} + std::vector ParseTokens(const std::string& s, std::vector delimiters, std::size_t pos)