Skip to content

Commit 41253e4

Browse files
dxbjavidcopybara-github
authored andcommitted
PR #2082: make RoundTripDoubleToBuffer locale-independent
Imported from GitHub PR #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 7ee6235 into ce2e0bc Merging this change closes #2082 COPYBARA_INTEGRATE_REVIEW=#2082 from dxbjavid:highprecision-locale-radix 7ee6235 PiperOrigin-RevId: 936184553 Change-Id: I003289412ce32167c0d8f52d6ad2ff1426cf4def
1 parent 9a9215b commit 41253e4

4 files changed

Lines changed: 60 additions & 0 deletions

File tree

absl/strings/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,7 @@ cc_test(
12591259
":pow10_helper",
12601260
":strings",
12611261
"//absl/base:config",
1262+
"//absl/cleanup",
12621263
"//absl/log",
12631264
"//absl/numeric:int128",
12641265
"//absl/random",

absl/strings/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ absl_cc_test(
470470
COPTS
471471
${ABSL_TEST_COPTS}
472472
DEPS
473+
absl::cleanup
473474
absl::config
474475
absl::core_headers
475476
absl::int128

absl/strings/numbers.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <array>
2222
#include <cassert>
2323
#include <cfloat> // for DBL_DIG and FLT_DIG
24+
#include <clocale> // for localeconv
2425
#include <cmath> // for HUGE_VAL
2526
#include <cstdint>
2627
#include <cstdio>
@@ -448,6 +449,30 @@ char* absl_nonnull numbers_internal::RoundTripDoubleToBuffer(
448449
ABSL_ASSERT(snprintf_result > 0 &&
449450
snprintf_result < numbers_internal::kFastToBufferSize);
450451
}
452+
453+
// snprintf() writes the radix character chosen by the global C locale's
454+
// LC_NUMERIC category, so a process that has called setlocale() can end up
455+
// with a separator other than '.' here. The rest of Abseil's float formatting
456+
// (RoundTripFloatToBuffer, SixDigitsToBuffer) is locale- independent and
457+
// SimpleAtod() only accepts '.', so rewrite the radix back to '.' to keep
458+
// absl::HighPrecision(double) locale-independent and round-trippable through
459+
// SimpleAtod().
460+
// TODO: b/526633099 - Once all supported compilers ship std::to_chars with
461+
// floating-point support, use it here for inherent locale independence.
462+
const char* radix = localeconv()->decimal_point;
463+
// Skip an empty decimal_point (some minimal environments leave it ""), which
464+
// would otherwise match the beginning of the buffer and corrupt it.
465+
if (radix[0] != '\0' && std::strcmp(radix, ".") != 0) {
466+
if (char* p = std::strstr(buffer, radix)) {
467+
const size_t radix_len = std::strlen(radix);
468+
*p = '.';
469+
// A multibyte radix (rare, but possible in some locales) leaves trailing
470+
// bytes behind; collapse them so the output is a single '.'.
471+
if (radix_len > 1) {
472+
std::memmove(p + 1, p + radix_len, std::strlen(p + radix_len) + 1);
473+
}
474+
}
475+
}
451476
return buffer;
452477
}
453478

absl/strings/numbers_test.cc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <cfloat>
2323
#include <cinttypes>
2424
#include <climits>
25+
#include <clocale>
2526
#include <cmath>
2627
#include <cstddef>
2728
#include <cstdint>
@@ -39,6 +40,7 @@
3940

4041
#include "gmock/gmock.h"
4142
#include "gtest/gtest.h"
43+
#include "absl/cleanup/cleanup.h"
4244
#include "absl/log/log.h"
4345
#include "absl/numeric/int128.h"
4446
#include "absl/random/random.h"
@@ -1717,6 +1719,37 @@ class SimpleDtoaTest : public testing::Test {
17171719
fenv_t fp_env_;
17181720
};
17191721

1722+
TEST(SimpleDtoa, HighPrecisionIsLocaleIndependent) {
1723+
// absl::HighPrecision(double) routes through RoundTripDoubleToBuffer(), which
1724+
// used to leak the global C locale's radix character (e.g. ',' under de_DE)
1725+
// into its output. HighPrecision() promises a value that SimpleAtod() reads
1726+
// back exactly, and SimpleAtod() only accepts '.', so the radix must stay '.'
1727+
// regardless of the active locale.
1728+
std::string old_locale = setlocale(LC_NUMERIC, nullptr);
1729+
auto restore_locale =
1730+
absl::MakeCleanup([&] { setlocale(LC_NUMERIC, old_locale.c_str()); });
1731+
const char* comma_locales[] = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR",
1732+
"nl_NL.UTF-8"};
1733+
bool changed = false;
1734+
for (const char* loc : comma_locales) {
1735+
if (setlocale(LC_NUMERIC, loc) != nullptr) {
1736+
changed = true;
1737+
break;
1738+
}
1739+
}
1740+
if (!changed) {
1741+
GTEST_SKIP() << "No comma-radix locale available on this system.";
1742+
}
1743+
EXPECT_EQ(absl::StrCat(absl::HighPrecision(0.5)), "0.5");
1744+
EXPECT_EQ(absl::StrCat(absl::HighPrecision(-1.25)), "-1.25");
1745+
EXPECT_EQ(absl::StrCat(absl::HighPrecision(3.14159265358979)),
1746+
"3.14159265358979");
1747+
double parsed = 0;
1748+
EXPECT_TRUE(
1749+
absl::SimpleAtod(absl::StrCat(absl::HighPrecision(0.1)), &parsed));
1750+
EXPECT_EQ(parsed, 0.1);
1751+
}
1752+
17201753
// Run the given runnable functor for "cases" test cases, chosen over the
17211754
// available range of float. pi and e and 1/e are seeded, and then all
17221755
// available integer powers of 2 and 10 are multiplied against them. In

0 commit comments

Comments
 (0)