Integrate Grisu

This commit is contained in:
Victor Zverovich 2018-10-13 22:14:36 -07:00
parent 699297520a
commit 50b18a3c10
5 changed files with 201 additions and 195 deletions

View File

@ -68,13 +68,13 @@ include(CheckCXXCompilerFlag)
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
set(PEDANTIC_COMPILE_FLAGS -pedantic-errors -Wall -Wextra -pedantic set(PEDANTIC_COMPILE_FLAGS -pedantic-errors -Wall -Wextra -pedantic
-Wold-style-cast -Wfloat-equal -Wlogical-op -Wundef -Wold-style-cast -Wlogical-op -Wundef
-Wredundant-decls -Wshadow -Wwrite-strings -Wpointer-arith -Wredundant-decls -Wshadow -Wwrite-strings -Wpointer-arith
-Wcast-qual -Wformat=2 -Wmissing-include-dirs -Wcast-qual -Wformat=2 -Wmissing-include-dirs
-Wcast-align -Wnon-virtual-dtor -Wcast-align -Wnon-virtual-dtor
-Wctor-dtor-privacy -Wdisabled-optimization -Wctor-dtor-privacy -Wdisabled-optimization
-Winvalid-pch -Woverloaded-virtual -Winvalid-pch -Woverloaded-virtual
-Wno-ctor-dtor-privacy -Wno-dangling-else -Wno-float-equal -Wno-ctor-dtor-privacy -Wno-dangling-else
-Wno-format-nonliteral -Wno-sign-conversion -Wno-shadow) -Wno-format-nonliteral -Wno-sign-conversion -Wno-shadow)
if (NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.6) if (NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.6)
set(PEDANTIC_COMPILE_FLAGS ${PEDANTIC_COMPILE_FLAGS} -Wnoexcept) set(PEDANTIC_COMPILE_FLAGS ${PEDANTIC_COMPILE_FLAGS} -Wnoexcept)
@ -93,7 +93,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
endif () endif ()
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(PEDANTIC_COMPILE_FLAGS -Weverything -Wpedantic set(PEDANTIC_COMPILE_FLAGS -Wall -Wextra -Wpedantic
-Wno-weak-vtables -Wno-padded -Wno-gnu-statement-expression -Wno-weak-vtables -Wno-padded -Wno-gnu-statement-expression
-Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-reserved-id-macro -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-reserved-id-macro
-Wno-global-constructors -Wno-disabled-macro-expansion -Wno-global-constructors -Wno-disabled-macro-expansion

View File

@ -461,29 +461,27 @@ FMT_FUNC fp get_cached_power(int min_exponent, int &pow10_exponent) {
return fp(data::POW10_SIGNIFICANDS[index], data::POW10_EXPONENTS[index]); return fp(data::POW10_SIGNIFICANDS[index], data::POW10_EXPONENTS[index]);
} }
FMT_FUNC void grisu2_round(char *buffer, size_t size, uint64_t delta, FMT_FUNC bool grisu2_round(
uint64_t remainder, uint64_t exp, uint64_t diff) { char *buffer, size_t &size, size_t max_digits, uint64_t delta,
uint64_t remainder, uint64_t exp, uint64_t diff, int &exp10) {
while (remainder < diff && delta - remainder >= exp && while (remainder < diff && delta - remainder >= exp &&
(remainder + exp < diff || diff - remainder > remainder + exp - diff)) { (remainder + exp < diff || diff - remainder > remainder + exp - diff)) {
--buffer[size - 1]; --buffer[size - 1];
remainder += exp; remainder += exp;
} }
if (size > max_digits) {
--size;
++exp10;
if (buffer[size] >= '5')
return false;
}
return true;
} }
// Generates output using Grisu2 digit-gen algorithm. // Generates output using Grisu2 digit-gen algorithm.
FMT_FUNC void grisu2_gen_digits( FMT_FUNC bool grisu2_gen_digits(
const fp &scaled_value, const fp &scaled_upper, uint64_t delta, char *buffer, size_t &size, uint32_t hi, uint64_t lo, int &exp,
char *buffer, size_t &size, int &dec_exp) { uint64_t delta, const fp &one, const fp &diff, size_t max_digits) {
internal::fp one(1ull << -scaled_upper.e, scaled_upper.e);
internal::fp diff = scaled_upper - scaled_value; // wp_w in Grisu.
// hi (p1 in Grisu) contains the most significant digits of scaled_upper.
// hi = floor(scaled_upper / one).
uint32_t hi = static_cast<uint32_t>(scaled_upper.f >> -one.e);
// lo (p2 in Grisu) contains the least significants digits of scaled_upper.
// lo = scaled_upper % one.
uint64_t lo = scaled_upper.f & (one.f - 1);
size = 0;
auto exp = count_digits(hi); // kappa in Grisu.
// Generate digits for the most significant part (hi). // Generate digits for the most significant part (hi).
while (exp > 0) { while (exp > 0) {
uint32_t digit = 0; uint32_t digit = 0;
@ -507,12 +505,11 @@ FMT_FUNC void grisu2_gen_digits(
buffer[size++] = static_cast<char>('0' + digit); buffer[size++] = static_cast<char>('0' + digit);
--exp; --exp;
uint64_t remainder = (static_cast<uint64_t>(hi) << -one.e) + lo; uint64_t remainder = (static_cast<uint64_t>(hi) << -one.e) + lo;
if (remainder <= delta) { if (remainder <= delta || size > max_digits) {
dec_exp += exp; return grisu2_round(
grisu2_round(buffer, size, delta, remainder, buffer, size, max_digits, delta, remainder,
static_cast<uint64_t>(data::POWERS_OF_10_32[exp]) << -one.e, static_cast<uint64_t>(data::POWERS_OF_10_32[exp]) << -one.e,
diff.f); diff.f, exp);
return;
} }
} }
// Generate digits for the least significant part (lo). // Generate digits for the least significant part (lo).
@ -524,49 +521,13 @@ FMT_FUNC void grisu2_gen_digits(
buffer[size++] = static_cast<char>('0' + digit); buffer[size++] = static_cast<char>('0' + digit);
lo &= one.f - 1; lo &= one.f - 1;
--exp; --exp;
if (lo < delta) { if (lo < delta || size > max_digits) {
dec_exp += exp; return grisu2_round(buffer, size, max_digits, delta, lo, one.f,
grisu2_round(buffer, size, delta, lo, one.f, diff.f * data::POWERS_OF_10_32[-exp], exp);
diff.f * data::POWERS_OF_10_32[-exp]);
return;
} }
} }
} }
template <typename Double>
FMT_FUNC void grisu2_format_positive(Double value, char *buffer, size_t &size,
int &dec_exp) {
FMT_ASSERT(value > 0, "value is nonpositive");
fp fp_value(value);
fp lower, upper; // w^- and w^+ in the Grisu paper.
fp_value.compute_boundaries(lower, upper);
// Find a cached power of 10 close to 1 / upper.
const int min_exp = -60; // alpha in Grisu.
auto dec_pow = get_cached_power( // \tilde{c}_{-k} in Grisu.
min_exp - (upper.e + fp::significand_size), dec_exp);
dec_exp = -dec_exp;
fp_value.normalize();
fp scaled_value = fp_value * dec_pow;
fp scaled_lower = lower * dec_pow; // \tilde{M}^- in Grisu.
fp scaled_upper = upper * dec_pow; // \tilde{M}^+ in Grisu.
++scaled_lower.f; // \tilde{M}^- + 1 ulp -> M^-_{\uparrow}.
--scaled_upper.f; // \tilde{M}^+ - 1 ulp -> M^+_{\downarrow}.
uint64_t delta = scaled_upper.f - scaled_lower.f;
grisu2_gen_digits(scaled_value, scaled_upper, delta, buffer, size, dec_exp);
}
FMT_FUNC void round(char *buffer, size_t &size, int &exp,
int digits_to_remove) {
size -= to_unsigned(digits_to_remove);
exp += digits_to_remove;
int digit = buffer[size] - '0';
// TODO: proper rounding and carry
if (digit > 5 || (digit == 5 && (digits_to_remove > 1 ||
(buffer[size - 1] - '0') % 2) != 0)) {
++buffer[size - 1];
}
}
// Writes the exponent exp in the form "[+-]d{2,3}" to buffer. // Writes the exponent exp in the form "[+-]d{2,3}" to buffer.
FMT_FUNC char *write_exponent(char *buffer, int exp) { FMT_FUNC char *write_exponent(char *buffer, int exp) {
FMT_ASSERT(-1000 < exp && exp < 1000, "exponent out of range"); FMT_ASSERT(-1000 < exp && exp < 1000, "exponent out of range");
@ -590,67 +551,6 @@ FMT_FUNC char *write_exponent(char *buffer, int exp) {
return buffer; return buffer;
} }
FMT_FUNC void format_exp_notation(
char *buffer, size_t &size, int exp, int precision, bool upper) {
// Insert a decimal point after the first digit and add an exponent.
std::memmove(buffer + 2, buffer + 1, size - 1);
buffer[1] = '.';
exp += static_cast<int>(size) - 1;
int num_digits = precision - static_cast<int>(size) + 1;
if (num_digits > 0) {
std::uninitialized_fill_n(buffer + size + 1, num_digits, '0');
size += to_unsigned(num_digits);
} else if (num_digits < 0) {
round(buffer, size, exp, -num_digits);
}
char *p = buffer + size + 1;
*p++ = upper ? 'E' : 'e';
size = to_unsigned(write_exponent(p, exp) - buffer);
}
// Prettifies the output of the Grisu2 algorithm.
// The number is given as v = buffer * 10^exp.
FMT_FUNC void grisu2_prettify(char *buffer, size_t &size, int exp,
int precision, bool upper) {
// pow(10, full_exp - 1) <= v <= pow(10, full_exp).
int int_size = static_cast<int>(size);
int full_exp = int_size + exp;
const int exp_threshold = 21;
if (int_size <= full_exp && full_exp <= exp_threshold) {
// 1234e7 -> 12340000000[.0+]
std::uninitialized_fill_n(buffer + int_size, full_exp - int_size, '0');
char *p = buffer + full_exp;
if (precision > 0) {
*p++ = '.';
std::uninitialized_fill_n(p, precision, '0');
p += precision;
}
size = to_unsigned(p - buffer);
} else if (0 < full_exp && full_exp <= exp_threshold) {
// 1234e-2 -> 12.34[0+]
int fractional_size = -exp;
std::memmove(buffer + full_exp + 1, buffer + full_exp,
to_unsigned(fractional_size));
buffer[full_exp] = '.';
int num_zeros = precision - fractional_size;
if (num_zeros > 0) {
std::uninitialized_fill_n(buffer + size + 1, num_zeros, '0');
size += to_unsigned(num_zeros);
}
++size;
} else if (-6 < full_exp && full_exp <= 0) {
// 1234e-6 -> 0.001234
int offset = 2 - full_exp;
std::memmove(buffer + offset, buffer, size);
buffer[0] = '0';
buffer[1] = '.';
std::uninitialized_fill_n(buffer + 2, -full_exp, '0');
size = to_unsigned(int_size + offset);
} else {
format_exp_notation(buffer, size, exp, precision, upper);
}
}
#if FMT_CLANG_VERSION #if FMT_CLANG_VERSION
# define FMT_FALLTHROUGH [[clang::fallthrough]]; # define FMT_FALLTHROUGH [[clang::fallthrough]];
#elif FMT_GCC_VERSION >= 700 #elif FMT_GCC_VERSION >= 700
@ -659,60 +559,159 @@ FMT_FUNC void grisu2_prettify(char *buffer, size_t &size, int exp,
# define FMT_FALLTHROUGH # define FMT_FALLTHROUGH
#endif #endif
// Formats a nonnegative value using Grisu2 algorithm. Grisu2 doesn't give any struct gen_digits_params {
// guarantees on the shortness of the result. unsigned min_digits;
template <typename Double> unsigned max_digits;
FMT_FUNC typename std::enable_if<sizeof(Double) == sizeof(uint64_t)>::type bool fixed;
grisu2_format(Double value, char *buffer, size_t &size, char type, bool upper;
int precision, bool write_decimal_point) { bool trailing_zeros;
FMT_ASSERT(value >= 0, "value is negative");
int dec_exp = 0; // K in Grisu. // Creates digit generation parameters from format specifiers for a number in
if (value > 0) { // the range [pow(10, exp - 1), pow(10, exp) or 0 if exp == 1.
grisu2_format_positive(value, buffer, size, dec_exp); gen_digits_params(const grisu2_specs &specs, int exp)
} else { : min_digits(specs.precision >= 0 ? to_unsigned(specs.precision) : 6),
*buffer = '0'; fixed(false), upper(false), trailing_zeros(false) {
size = 1; switch (specs.type) {
} case 'G':
const int default_precision = 6; upper = true;
if (precision < 0) FMT_FALLTHROUGH
precision = default_precision; case '\0': case 'g':
bool upper = false; trailing_zeros = (specs.flags & HASH_FLAG) != 0;
switch (type) { if (-4 <= exp && exp < static_cast<int>(min_digits) + 1) {
case 'G': fixed = true;
upper = true; if (!specs.type && trailing_zeros && exp >= 0)
FMT_FALLTHROUGH min_digits = to_unsigned(exp) + 1;
case '\0': case 'g': {
int digits_to_remove = static_cast<int>(size) - precision;
if (digits_to_remove > 0) {
round(buffer, size, dec_exp, digits_to_remove);
// Remove trailing zeros.
while (size > 0 && buffer[size - 1] == '0') {
--size;
++dec_exp;
} }
break;
case 'F':
upper = true;
FMT_FALLTHROUGH
case 'f': {
fixed = true;
trailing_zeros = true;
int adjusted_min_digits = static_cast<int>(min_digits) + exp;
if (adjusted_min_digits > 0)
min_digits = to_unsigned(adjusted_min_digits);
break;
} }
precision = 0; case 'E':
break; upper = true;
} FMT_FALLTHROUGH
case 'F': case 'e':
upper = true; ++min_digits;
FMT_FALLTHROUGH break;
case 'f': {
int digits_to_remove = -dec_exp - precision;
if (digits_to_remove > 0) {
if (digits_to_remove >= static_cast<int>(size))
digits_to_remove = static_cast<int>(size) - 1;
round(buffer, size, dec_exp, digits_to_remove);
} }
break; max_digits = min_digits;
} }
case 'e': case 'E': };
format_exp_notation(buffer, size, dec_exp, precision, type == 'E');
// The number is given as v = buffer * pow(10, exp).
FMT_FUNC void format_float(char *buffer, size_t &size, int exp,
const gen_digits_params &params) {
if (!params.fixed) {
// Insert a decimal point after the first digit and add an exponent.
std::memmove(buffer + 2, buffer + 1, size - 1);
buffer[1] = '.';
exp += static_cast<int>(size) - 1;
if (size < params.min_digits) {
std::uninitialized_fill_n(buffer + size + 1,
params.min_digits - size, '0');
size = params.min_digits;
}
char *p = buffer + size + 1;
*p++ = params.upper ? 'E' : 'e';
size = to_unsigned(write_exponent(p, exp) - buffer);
return; return;
} }
if (write_decimal_point && precision < 1) // pow(10, full_exp - 1) <= v <= pow(10, full_exp).
precision = 1; int int_size = static_cast<int>(size);
grisu2_prettify(buffer, size, dec_exp, precision, upper); int full_exp = int_size + exp;
const int exp_threshold = 21;
if (int_size <= full_exp && full_exp <= exp_threshold) {
// 1234e7 -> 12340000000[.0+]
std::uninitialized_fill_n(buffer + int_size, full_exp - int_size, '0');
char *p = buffer + full_exp;
int num_zeros = static_cast<int>(params.min_digits) - full_exp;
if (num_zeros > 0 && params.trailing_zeros) {
*p++ = '.';
std::uninitialized_fill_n(p, num_zeros, '0');
p += num_zeros;
}
size = to_unsigned(p - buffer);
} else if (full_exp > 0) {
// 1234e-2 -> 12.34[0+]
int fractional_size = -exp;
std::memmove(buffer + full_exp + 1, buffer + full_exp,
to_unsigned(fractional_size));
buffer[full_exp] = '.';
++size;
if (!params.trailing_zeros) {
// Remove trailing zeros.
while (buffer[size - 1] == '0') --size;
} else if (params.min_digits >= size) {
// Add trailing zeros.
size_t num_zeros = params.min_digits - size + 1;
std::uninitialized_fill_n(buffer + size, num_zeros, '0');
size += to_unsigned(num_zeros);
}
} else {
// 1234e-6 -> 0.001234
int offset = 2 - full_exp;
std::memmove(buffer + offset, buffer, size);
buffer[0] = '0';
buffer[1] = '.';
std::uninitialized_fill_n(buffer + 2, -full_exp, '0');
size = to_unsigned(int_size + offset);
}
}
template <typename Double>
FMT_FUNC typename std::enable_if<sizeof(Double) == sizeof(uint64_t), bool>::type
grisu2_format(Double value, char *buffer, size_t &size,
grisu2_specs specs) {
FMT_ASSERT(value >= 0, "value is negative");
if (value == 0) {
gen_digits_params params(specs, 1);
*buffer = '0';
size = 1;
format_float(buffer, size, 0, params);
return true;
}
fp fp_value(value);
fp lower, upper; // w^- and w^+ in the Grisu paper.
fp_value.compute_boundaries(lower, upper);
// Find a cached power of 10 close to 1 / upper and use it to scale upper.
const int min_exp = -60; // alpha in Grisu.
int cached_exp = 0; // K in Grisu.
auto cached_pow = get_cached_power( // \tilde{c}_{-k} in Grisu.
min_exp - (upper.e + fp::significand_size), cached_exp);
cached_exp = -cached_exp;
upper = upper * cached_pow; // \tilde{M}^+ in Grisu.
--upper.f; // \tilde{M}^+ - 1 ulp -> M^+_{\downarrow}.
fp one(1ull << -upper.e, upper.e);
// hi (p1 in Grisu) contains the most significant digits of scaled_upper.
// hi = floor(upper / one).
uint32_t hi = static_cast<uint32_t>(upper.f >> -one.e);
int exp = static_cast<int>(count_digits(hi)); // kappa in Grisu.
gen_digits_params params(specs, cached_exp + exp);
fp_value.normalize();
fp scaled_value = fp_value * cached_pow;
lower = lower * cached_pow; // \tilde{M}^- in Grisu.
++lower.f; // \tilde{M}^- + 1 ulp -> M^-_{\uparrow}.
uint64_t delta = upper.f - lower.f;
fp diff = upper - scaled_value; // wp_w in Grisu.
// lo (p2 in Grisu) contains the least significants digits of scaled_upper.
// lo = supper % one.
uint64_t lo = upper.f & (one.f - 1);
size = 0;
if (!grisu2_gen_digits(buffer, size, hi, lo, exp, delta, one, diff,
params.max_digits)) {
return false;
}
format_float(buffer, size, cached_exp + exp, params);
return true;
} }
} // namespace internal } // namespace internal

View File

@ -289,15 +289,20 @@ inline bool use_grisu() {
return FMT_USE_GRISU && std::numeric_limits<double>::is_iec559; return FMT_USE_GRISU && std::numeric_limits<double>::is_iec559;
} }
struct grisu2_specs {
int precision;
char type;
uint_least8_t flags;
};
// Formats value using Grisu2 algorithm: // Formats value using Grisu2 algorithm:
// https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf // https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf
template <typename Double> template <typename Double>
FMT_API typename std::enable_if<sizeof(Double) == sizeof(uint64_t)>::type FMT_API typename std::enable_if<sizeof(Double) == sizeof(uint64_t), bool>::type
grisu2_format(Double value, char *buffer, size_t &size, char type, grisu2_format(Double value, char *buffer, size_t &size, grisu2_specs);
int precision, bool write_decimal_point);
template <typename Double> template <typename Double>
inline typename std::enable_if<sizeof(Double) != sizeof(uint64_t)>::type inline typename std::enable_if<sizeof(Double) != sizeof(uint64_t), bool>::type
grisu2_format(Double, char *, size_t &, char, int, bool) {} grisu2_format(Double, char *, size_t &, grisu2_specs) { return false; }
template <typename Allocator> template <typename Allocator>
typename Allocator::value_type *allocate(Allocator& alloc, std::size_t n) { typename Allocator::value_type *allocate(Allocator& alloc, std::size_t n) {
@ -1203,13 +1208,13 @@ struct align_spec : empty_spec {
template <typename Char> template <typename Char>
class basic_format_specs : public align_spec { class basic_format_specs : public align_spec {
public: public:
unsigned flags_;
int precision_; int precision_;
uint_least8_t flags_;
char type_; char type_;
FMT_CONSTEXPR basic_format_specs( FMT_CONSTEXPR basic_format_specs(
unsigned width = 0, char type = 0, wchar_t fill = ' ') unsigned width = 0, char type = 0, wchar_t fill = ' ')
: align_spec(width, fill), flags_(0), precision_(-1), type_(type) {} : align_spec(width, fill), precision_(-1), flags_(0), type_(type) {}
FMT_CONSTEXPR bool flag(unsigned f) const { return (flags_ & f) != 0; } FMT_CONSTEXPR bool flag(unsigned f) const { return (flags_ & f) != 0; }
FMT_CONSTEXPR int precision() const { return precision_; } FMT_CONSTEXPR int precision() const { return precision_; }
@ -2881,16 +2886,23 @@ void basic_writer<Range>::write_double(T value, const format_specs &spec) {
memory_buffer buffer; memory_buffer buffer;
char type = static_cast<char>(spec.type()); char type = static_cast<char>(spec.type());
if (internal::const_check( bool use_grisu = internal::use_grisu() && sizeof(T) <= sizeof(double) &&
internal::use_grisu() && sizeof(T) <= sizeof(double)) && type != 'a' && type != 'A';
type != 'a' && type != 'A') { if (use_grisu) {
char buf[100]; // TODO: correct buffer size char buf[100]; // TODO: correct buffer size
size_t size = 0; size_t size = 0;
internal::grisu2_format(static_cast<double>(value), buf, size, type, auto gs = internal::grisu2_specs();
spec.precision(), spec.flag(HASH_FLAG)); gs.type = type;
FMT_ASSERT(size <= 100, "buffer overflow"); gs.precision = spec.precision();
buffer.append(buf, buf + size); // TODO: avoid extra copy gs.flags = spec.flags_;
} else { use_grisu = internal::grisu2_format(
static_cast<double>(value), buf, size, gs);
if (use_grisu) {
FMT_ASSERT(size <= 100, "buffer overflow");
buffer.append(buf, buf + size); // TODO: avoid extra copy
}
}
if (!use_grisu) {
format_specs normalized_spec(spec); format_specs normalized_spec(spec);
normalized_spec.type_ = handler.type; normalized_spec.type_ = handler.type;
write_double_sprintf(value, normalized_spec, buffer); write_double_sprintf(value, normalized_spec, buffer);

View File

@ -104,7 +104,7 @@ TEST(FPTest, GetCachedPower) {
TEST(FPTest, Grisu2FormatCompilesWithNonIEEEDouble) { TEST(FPTest, Grisu2FormatCompilesWithNonIEEEDouble) {
size_t size = 0; size_t size = 0;
fmt::internal::grisu2_format(4.2f, FMT_NULL, size, 0, 0, false); grisu2_format(4.2f, FMT_NULL, size, fmt::internal::grisu2_specs());
} }
template <typename T> template <typename T>

View File

@ -180,13 +180,8 @@ TEST(PrintfTest, HashFlag) {
safe_sprintf(buffer, "%#E", -42.0); safe_sprintf(buffer, "%#E", -42.0);
EXPECT_PRINTF(buffer, "%#E", -42.0); EXPECT_PRINTF(buffer, "%#E", -42.0);
if (fmt::internal::use_grisu()) { EXPECT_PRINTF("-42.0000", "%#g", -42.0);
EXPECT_PRINTF("-42.0", "%#g", -42.0); EXPECT_PRINTF("-42.0000", "%#G", -42.0);
EXPECT_PRINTF("-42.0", "%#G", -42.0);
} else {
EXPECT_PRINTF("-42.0000", "%#g", -42.0);
EXPECT_PRINTF("-42.0000", "%#G", -42.0);
}
safe_sprintf(buffer, "%#a", 16.0); safe_sprintf(buffer, "%#a", 16.0);
EXPECT_PRINTF(buffer, "%#a", 16.0); EXPECT_PRINTF(buffer, "%#a", 16.0);