PR #2085: Read float round-trip locale-independently in PrintPreciseFP

Imported from GitHub PR https://github.com/abseil/abseil-cpp/pull/2085

GenericPrint shortens floating-point output in TryShorten by
formatting the value with absl::StrFormat and then reading it back to
confirm the shorter form still round-trips. absl::StrFormat is
locale-independent and always writes a full stop as the radix, but the
read-back for float and double went through std::strtof and
std::strtod, which honour LC_NUMERIC. In a process that has called
setlocale to a comma-radix locale such as de_DE or fr_FR those readers
stop at the full stop, so the shortened candidate never compares equal
and the printer quietly falls back to the long form; for instance 1.1
prints as 1.10000000000000009.

Routing the float and double check through absl::SimpleAtof and
absl::SimpleAtod, which are locale-independent, lines the parse up
with the formatter and keeps the shortening working everywhere. The
long double overload still uses std::strtold as there is no
locale-independent long double parser to switch to.

Merge e67cf792630387b70b086724f01adb1970c9f2a2 into ce2e0bc69560bf3287928864f780faa4daed4dd0

Merging this change closes #2085

COPYBARA_INTEGRATE_REVIEW=https://github.com/abseil/abseil-cpp/pull/2085 from dxbjavid:generic-printer-fp-locale e67cf792630387b70b086724f01adb1970c9f2a2
PiperOrigin-RevId: 936136258
Change-Id: I8763cea760c1988388d639473e624813fe62f21c
diff --git a/absl/strings/BUILD.bazel b/absl/strings/BUILD.bazel
index def78d5..20149f1 100644
--- a/absl/strings/BUILD.bazel
+++ b/absl/strings/BUILD.bazel
@@ -1619,6 +1619,7 @@
         ":strings",
         "//absl/base:config",
         "//absl/base:core_headers",
+        "//absl/cleanup",
         "//absl/container:flat_hash_map",
         "//absl/log",
         "//absl/status",
diff --git a/absl/strings/CMakeLists.txt b/absl/strings/CMakeLists.txt
index e6b5da3..41fe3c0 100644
--- a/absl/strings/CMakeLists.txt
+++ b/absl/strings/CMakeLists.txt
@@ -1314,6 +1314,7 @@
     ${ABSL_TEST_COPTS}
   DEPS
     absl::base
+    absl::cleanup
     absl::config
     absl::flat_hash_map
     absl::generic_printer_internal
diff --git a/absl/strings/internal/generic_printer.cc b/absl/strings/internal/generic_printer.cc
index 16ca228..6535e1c 100644
--- a/absl/strings/internal/generic_printer.cc
+++ b/absl/strings/internal/generic_printer.cc
@@ -22,6 +22,7 @@
 #include "absl/base/config.h"
 #include "absl/strings/ascii.h"
 #include "absl/strings/escaping.h"
+#include "absl/strings/numbers.h"
 #include "absl/strings/str_format.h"
 
 namespace absl {
@@ -50,18 +51,28 @@
 // ensure that values are precise, but rather that they are wide enough to
 // represent distinct values. go/c++17std/numeric.limits.members.html
 std::ostream& PrintPreciseFP(std::ostream& os, float v) {
+  // TryShorten formats with absl::StrFormat(), which is locale-independent and
+  // always emits a '.' radix. Use absl::SimpleAtof() for locale-independent
+  // parsing.
   return os << TryShorten(v, [](const char* buf) {
-           char* unused;
-           return std::strtof(buf, &unused);
+           float out = 0;
+           static_cast<void>(absl::SimpleAtof(buf, &out));
+           return out;
          }) << "f";
 }
 std::ostream& PrintPreciseFP(std::ostream& os, double v) {
+  // TryShorten formats with absl::StrFormat(), which is locale-independent and
+  // always emits a '.' radix. Use absl::SimpleAtod() for locale-independent
+  // parsing.
   return os << TryShorten(v, [](const char* buf) {
-           char* unused;
-           return std::strtod(buf, &unused);
+           double out = 0;
+           static_cast<void>(absl::SimpleAtod(buf, &out));
+           return out;
          });
 }
 std::ostream& PrintPreciseFP(std::ostream& os, long double v) {
+  // No locale-independent long double parser is available, so this path keeps
+  // std::strtold and remains locale-sensitive.
   return os << TryShorten(v, [](const char* buf) {
            char* unused;
            return std::strtold(buf, &unused);
diff --git a/absl/strings/internal/generic_printer_test.cc b/absl/strings/internal/generic_printer_test.cc
index f5b737b..071bf76 100644
--- a/absl/strings/internal/generic_printer_test.cc
+++ b/absl/strings/internal/generic_printer_test.cc
@@ -15,6 +15,7 @@
 #include "absl/strings/internal/generic_printer.h"
 
 #include <array>
+#include <clocale>
 #include <cstdint>
 #include <limits>
 #include <map>
@@ -32,6 +33,7 @@
 #include "gtest/gtest.h"
 #include "absl/base/attributes.h"
 #include "absl/base/config.h"
+#include "absl/cleanup/cleanup.h"
 #include "absl/container/flat_hash_map.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
@@ -231,6 +233,32 @@
   EXPECT_THAT(GenericPrintToString(0.L), EndsWith("L"));
 }
 
+TEST(GenericPrinterTest, PreciseFPUnderCommaRadixLocale) {
+  // The values are formatted with locale-independent absl::StrFormat (a '.'
+  // radix), so the round-trip shortening must not depend on LC_NUMERIC. Under a
+  // comma-radix locale a locale-sensitive reader stops at the '.', which used
+  // to defeat the shortened output.
+  const char* saved = std::setlocale(LC_NUMERIC, nullptr);
+  std::string saved_locale = saved ? saved : "C";
+  absl::Cleanup restore = [&] {
+    std::setlocale(LC_NUMERIC, saved_locale.c_str());
+  };
+
+  bool set = false;
+  for (const char* name : {"de_DE.UTF-8", "fr_FR.UTF-8", "de_DE", "fr_FR"}) {
+    if (std::setlocale(LC_NUMERIC, name) != nullptr) {
+      set = true;
+      break;
+    }
+  }
+  if (!set) {
+    GTEST_SKIP() << "No comma-radix locale available on this system.";
+  }
+
+  EXPECT_EQ("1.1f", GenericPrintToString(1.1f));
+  EXPECT_EQ("1.1", GenericPrintToString(1.1));
+}
+
 TEST(GenericPrinterTest, StreamableLvalue) {
   generic_logging_test::Streamable x{234};
   EXPECT_EQ("Streamable{234}", GenericPrintToString(x));