From fdbc474d135a9e2b1ccc2ede067449776de3f002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Karas?= Date: Sat, 23 Dec 2023 17:42:44 +0100 Subject: [PATCH] locale-aware byte size formatter Char-based number formatting (using std::stringstream) is broken for locales with non-ascii thousand separator (at least with gcc 8.3.0), for that reason, we should provide custom locale aware number-to-string conversion for float numbers and use it in ByteSizeToString utility. --- Tests/src/StringUtils.cpp | 13 +++++ .../src/osmscoutclient/MapManager.cpp | 2 +- libosmscout/include/osmscout/util/String.h | 20 ++++++-- libosmscout/src/osmscout/util/String.cpp | 50 ++++++++++++------- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/Tests/src/StringUtils.cpp b/Tests/src/StringUtils.cpp index dde2cb83c..561102b28 100644 --- a/Tests/src/StringUtils.cpp +++ b/Tests/src/StringUtils.cpp @@ -308,8 +308,21 @@ TEST_CASE("Local aware number to string") { osmscout::Locale locale; locale.SetThousandsSeparator(" "); + locale.SetDecimalSeparator("."); REQUIRE(osmscout::NumberToString(1002030, locale) == "1 002 030"); REQUIRE(osmscout::NumberToString(-1002030, locale) == "-1 002 030"); + + REQUIRE(osmscout::FloatToString(-1002030.123456, locale, 6) == "-1 002 030.123 456"); + REQUIRE(osmscout::FloatToString(-1002030.125, locale, 2) == "-1 002 030.13"); +} + +TEST_CASE("Byte size to string") +{ + osmscout::Locale locale; + locale.SetThousandsSeparator("'"); + locale.SetDecimalSeparator(","); + locale.SetUnitsSeparator(" "); + REQUIRE(osmscout::ByteSizeToString(1063256064.0, locale) == "1'014,0 MiB"); } TEST_CASE("Trim string") diff --git a/libosmscout-client/src/osmscoutclient/MapManager.cpp b/libosmscout-client/src/osmscoutclient/MapManager.cpp index a87a8e3fc..661615038 100644 --- a/libosmscout-client/src/osmscoutclient/MapManager.cpp +++ b/libosmscout-client/src/osmscoutclient/MapManager.cpp @@ -48,7 +48,7 @@ CancelableFuture MapManager::LookupDatabases() // https://en.cppreference.com/w/cpp/filesystem/is_directory // https://en.cppreference.com/w/cpp/filesystem/status if (!std::filesystem::exists(lookupDir) || !std::filesystem::is_directory(lookupDir)) { - osmscout::log.Warn() << "Lookup dir" << lookupDir.string() << "doesn't exist or isn't a directory"; + osmscout::log.Warn() << "Lookup dir " << lookupDir.string() << " doesn't exist or isn't a directory"; continue; } diff --git a/libosmscout/include/osmscout/util/String.h b/libosmscout/include/osmscout/util/String.h index 6e0938467..4bff5a2e0 100644 --- a/libosmscout/include/osmscout/util/String.h +++ b/libosmscout/include/osmscout/util/String.h @@ -74,10 +74,20 @@ namespace osmscout { * * @param value * @param locale - * @return + * @return UTF-8 string */ extern OSMSCOUT_API std::string NumberToString(long value, const Locale &locale); + /** + * Returns locale-aware string representation of number + * + * @param value + * @param locale + * @param precision + * @return UTF-8 string + */ + extern OSMSCOUT_API std::string FloatToString(double value, const Locale &locale, uint32_t precision = 3); + /** * \ingroup Util * Returns the numerical value of the given character, if the character @@ -403,9 +413,13 @@ namespace osmscout { /** * \ingroup Util * + * Prints byte size with short, human readable form by ISO/IEC 80000 standard. + * It means that KiB stands for 1024 bytes, MiB for 1024^2, GiB 1024^3... + * + * Returned string is locale aware, UTF-8 encoded */ - extern OSMSCOUT_API std::string ByteSizeToString(FileOffset size); - extern OSMSCOUT_API std::string ByteSizeToString(double size); + extern OSMSCOUT_API std::string ByteSizeToString(FileOffset size, const Locale &locale = Locale::ByEnvironment()); + extern OSMSCOUT_API std::string ByteSizeToString(double size, const Locale &locale = Locale::ByEnvironment()); /** * \ingroup Util diff --git a/libosmscout/src/osmscout/util/String.cpp b/libosmscout/src/osmscout/util/String.cpp index 42a34bdeb..b1c693cd7 100644 --- a/libosmscout/src/osmscout/util/String.cpp +++ b/libosmscout/src/osmscout/util/String.cpp @@ -196,7 +196,8 @@ namespace osmscout { std::string NumberToString(long value, const Locale &locale) { std::stringstream ss; - if (std::abs(value) < 1000 || locale.GetThousandsSeparator().empty()){ + ss.imbue(std::locale("C")); + if (locale.GetThousandsSeparator().empty()){ ss << value; }else{ long mag=1000; @@ -214,6 +215,25 @@ namespace osmscout { return ss.str(); } + extern OSMSCOUT_API std::string FloatToString(double value, const Locale &locale, uint_2 precision) + { + std::stringstream ss; + ss << NumberToString(static_cast(value), locale); + + if (precision > 0) { + ss << locale.GetDecimalSeparator(); + double fractionNum = std::abs(value - std::ceil(value)); + std::string fraction = NumberToString(std::round(fractionNum * std::pow(10, precision)), Locale()); + for (size_t i = 0; i < fraction.size(); i++) { + if (i > 0 && i % 3 == 0 && i < fraction.size() - 1) { + ss << locale.GetThousandsSeparator(); + } + ss << fraction[i]; + } + } + return ss.str(); + } + bool GetDigitValue(char digit, size_t& result) { switch (digit) { @@ -329,36 +349,32 @@ namespace osmscout { return wordCount; } - std::string ByteSizeToString(FileOffset value) + std::string ByteSizeToString(FileOffset value, const Locale &locale) { - return ByteSizeToString((double)value); + return ByteSizeToString(static_cast(value), locale); } - std::string ByteSizeToString(double value) + std::string ByteSizeToString(double value, const Locale &locale) { std::stringstream buffer; - buffer.setf(std::ios::fixed); - buffer << std::setprecision(1); - if (value<1.0 && value>-1) { - buffer << "0 B"; + buffer << "0" << locale.GetUnitsSeparator() << "B"; } - else if (ceil(value)>=1024.0*1024*1024*1024) { - buffer << value/(1024.0*1024*1024*1024) << " TiB"; + else if (ceil(value)>=std::pow(1024.0, 4.0)) { + buffer << FloatToString(value/std::pow(1024.0, 4.0), locale, 1) << locale.GetUnitsSeparator() << "TiB"; } - else if (ceil(value)>=1024.0*1024*1024) { - buffer << value/(1024.0*1024*1024) << " GiB"; + else if (ceil(value)>=std::pow(1024.0, 3.0)) { + buffer << FloatToString(value/std::pow(1024.0, 3.0), locale, 1) << locale.GetUnitsSeparator() << "GiB"; } - else if (ceil(value)>=1024.0*1024) { - buffer << value/(1024.0*1024) << " MiB"; + else if (ceil(value)>=std::pow(1024.0, 2.0)) { + buffer << FloatToString(value/std::pow(1024.0, 2.0), locale, 1) << locale.GetUnitsSeparator() << "MiB"; } else if (ceil(value)>=1024.0) { - buffer << value/1024.0 << " KiB"; + buffer << FloatToString(value/1024.0, locale, 1) << locale.GetUnitsSeparator() << "KiB"; } else { - buffer << std::setprecision(0); - buffer << value << " B"; + buffer << FloatToString(value, locale, 0) << locale.GetUnitsSeparator() << "B"; } return buffer.str();