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