PR #2082: make RoundTripDoubleToBuffer locale-independent Imported from GitHub PR https://github.com/abseil/abseil-cpp/pull/2082 RoundTripDoubleToBuffer formats with snprintf %g, whose radix character follows the global C locale's LC_NUMERIC category, so in a process that has called setlocale() to something like de_DE or fr_FR absl::HighPrecision(d) comes out as e.g. "0,1". SimpleAtod only accepts '.', so the value no longer parses back to itself even though round-tripping through SimpleAtod is exactly what HighPrecision promises, and the rest of the float formatting here (RoundTripFloatToBuffer, SixDigitsToBuffer) is already locale-independent. This rewrites the radix character back to '.' in the produced buffer, and adds a regression test that exercises HighPrecision under a comma-radix locale (skipped when no such locale is installed). Merge 7ee6235813f3de485ab15e57cbd94217f758d123 into ce2e0bc69560bf3287928864f780faa4daed4dd0 Merging this change closes #2082 COPYBARA_INTEGRATE_REVIEW=https://github.com/abseil/abseil-cpp/pull/2082 from dxbjavid:highprecision-locale-radix 7ee6235813f3de485ab15e57cbd94217f758d123 PiperOrigin-RevId: 936184553 Change-Id: I003289412ce32167c0d8f52d6ad2ff1426cf4def
diff --git a/absl/strings/BUILD.bazel b/absl/strings/BUILD.bazel index 20149f1..6b96d0c 100644 --- a/absl/strings/BUILD.bazel +++ b/absl/strings/BUILD.bazel
@@ -1259,6 +1259,7 @@ ":pow10_helper", ":strings", "//absl/base:config", + "//absl/cleanup", "//absl/log", "//absl/numeric:int128", "//absl/random",
diff --git a/absl/strings/CMakeLists.txt b/absl/strings/CMakeLists.txt index 41fe3c0..ca07c3e 100644 --- a/absl/strings/CMakeLists.txt +++ b/absl/strings/CMakeLists.txt
@@ -470,6 +470,7 @@ COPTS ${ABSL_TEST_COPTS} DEPS + absl::cleanup absl::config absl::core_headers absl::int128
diff --git a/absl/strings/numbers.cc b/absl/strings/numbers.cc index f0a8f00..479c07e 100644 --- a/absl/strings/numbers.cc +++ b/absl/strings/numbers.cc
@@ -21,6 +21,7 @@ #include <array> #include <cassert> #include <cfloat> // for DBL_DIG and FLT_DIG +#include <clocale> // for localeconv #include <cmath> // for HUGE_VAL #include <cstdint> #include <cstdio> @@ -448,6 +449,30 @@ ABSL_ASSERT(snprintf_result > 0 && snprintf_result < numbers_internal::kFastToBufferSize); } + + // snprintf() writes the radix character chosen by the global C locale's + // LC_NUMERIC category, so a process that has called setlocale() can end up + // with a separator other than '.' here. The rest of Abseil's float formatting + // (RoundTripFloatToBuffer, SixDigitsToBuffer) is locale- independent and + // SimpleAtod() only accepts '.', so rewrite the radix back to '.' to keep + // absl::HighPrecision(double) locale-independent and round-trippable through + // SimpleAtod(). + // TODO: b/526633099 - Once all supported compilers ship std::to_chars with + // floating-point support, use it here for inherent locale independence. + const char* radix = localeconv()->decimal_point; + // Skip an empty decimal_point (some minimal environments leave it ""), which + // would otherwise match the beginning of the buffer and corrupt it. + if (radix[0] != '\0' && std::strcmp(radix, ".") != 0) { + if (char* p = std::strstr(buffer, radix)) { + const size_t radix_len = std::strlen(radix); + *p = '.'; + // A multibyte radix (rare, but possible in some locales) leaves trailing + // bytes behind; collapse them so the output is a single '.'. + if (radix_len > 1) { + std::memmove(p + 1, p + radix_len, std::strlen(p + radix_len) + 1); + } + } + } return buffer; }
diff --git a/absl/strings/numbers_test.cc b/absl/strings/numbers_test.cc index dd57a88..6f0cde8 100644 --- a/absl/strings/numbers_test.cc +++ b/absl/strings/numbers_test.cc
@@ -22,6 +22,7 @@ #include <cfloat> #include <cinttypes> #include <climits> +#include <clocale> #include <cmath> #include <cstddef> #include <cstdint> @@ -39,6 +40,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/cleanup/cleanup.h" #include "absl/log/log.h" #include "absl/numeric/int128.h" #include "absl/random/random.h" @@ -1717,6 +1719,37 @@ fenv_t fp_env_; }; +TEST(SimpleDtoa, HighPrecisionIsLocaleIndependent) { + // absl::HighPrecision(double) routes through RoundTripDoubleToBuffer(), which + // used to leak the global C locale's radix character (e.g. ',' under de_DE) + // into its output. HighPrecision() promises a value that SimpleAtod() reads + // back exactly, and SimpleAtod() only accepts '.', so the radix must stay '.' + // regardless of the active locale. + std::string old_locale = setlocale(LC_NUMERIC, nullptr); + auto restore_locale = + absl::MakeCleanup([&] { setlocale(LC_NUMERIC, old_locale.c_str()); }); + const char* comma_locales[] = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", + "nl_NL.UTF-8"}; + bool changed = false; + for (const char* loc : comma_locales) { + if (setlocale(LC_NUMERIC, loc) != nullptr) { + changed = true; + break; + } + } + if (!changed) { + GTEST_SKIP() << "No comma-radix locale available on this system."; + } + EXPECT_EQ(absl::StrCat(absl::HighPrecision(0.5)), "0.5"); + EXPECT_EQ(absl::StrCat(absl::HighPrecision(-1.25)), "-1.25"); + EXPECT_EQ(absl::StrCat(absl::HighPrecision(3.14159265358979)), + "3.14159265358979"); + double parsed = 0; + EXPECT_TRUE( + absl::SimpleAtod(absl::StrCat(absl::HighPrecision(0.1)), &parsed)); + EXPECT_EQ(parsed, 0.1); +} + // Run the given runnable functor for "cases" test cases, chosen over the // available range of float. pi and e and 1/e are seeded, and then all // available integer powers of 2 and 10 are multiplied against them. In