Format zero as 0x0p+0 in hexfloat (#4878)

fmt::format("{:a}", 0.0) produced 0x0p-1022 where printf's %a gives
0x0p+0, and likewise for -0.0, {:A}, {:#a} and any explicit precision.

basic_fp::assign maps a zero biased exponent to 1 ("subnormals use
biased exponent 1"), which is right for subnormals but not for zero, so
format_hexfloat inherited the minimum subnormal exponent for a value
whose significand carries no information. Reset the exponent there
rather than in assign, which the decimal path shares.

A zero significand can only come from +-0: normals get the implicit bit
added, and a subnormal has a nonzero significand by definition. So the
guard cannot affect anything else, and denorm_min keeps its existing
denormalized form (0x0.0000000000001p-1022, asserted in format_double).

The five new expectations were taken from printf rather than derived,
and all fail without this change.
This commit is contained in:
hexonal 2026-08-11 00:52:03 +08:00 committed by GitHub
parent 0cf9f5c99e
commit 6c285ba88a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 12 additions and 0 deletions

View File

@ -3110,6 +3110,11 @@ FMT_CONSTEXPR20 void format_hexfloat(Float value, format_specs specs,
f.e += num_float_significand_bits;
if (!has_implicit_bit<Float>()) --f.e;
// basic_fp maps a zero biased exponent to the minimum subnormal exponent,
// which is correct for subnormals but not for zero: printf prints zero as
// 0x0p+0. A zero significand can only come from +-0, so reset the exponent.
if (f.f == 0) f.e = 0;
const auto num_fraction_bits =
num_float_significand_bits + (has_implicit_bit<Float>() ? 1 : 0);
const auto num_xdigits = (num_fraction_bits + 3) / 4;

View File

@ -1539,6 +1539,13 @@ TEST(format_test, format_double) {
d = std::numeric_limits<double>::denorm_min();
EXPECT_EQ(fmt::format("{:a}", d), "0x0.0000000000001p-1022");
// Zero has no subnormal exponent: printf prints it as 0x0p+0.
EXPECT_EQ(fmt::format("{:a}", 0.0), "0x0p+0");
EXPECT_EQ(fmt::format("{:a}", -0.0), "-0x0p+0");
EXPECT_EQ(fmt::format("{:A}", 0.0), "0X0P+0");
EXPECT_EQ(fmt::format("{:#a}", 0.0), "0x0.p+0");
EXPECT_EQ(fmt::format("{:.3a}", 0.0), "0x0.000p+0");
}
if (std::numeric_limits<long double>::digits == 64) {