diff --git a/CMakeLists.txt b/CMakeLists.txt index f872e3c9..260aa265 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,13 +68,13 @@ include(CheckCXXCompilerFlag) if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") 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 -Wcast-qual -Wformat=2 -Wmissing-include-dirs -Wcast-align -Wnon-virtual-dtor -Wctor-dtor-privacy -Wdisabled-optimization -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) if (NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.6) set(PEDANTIC_COMPILE_FLAGS ${PEDANTIC_COMPILE_FLAGS} -Wnoexcept) @@ -93,7 +93,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") endif () 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-c++98-compat -Wno-c++98-compat-pedantic -Wno-reserved-id-macro -Wno-global-constructors -Wno-disabled-macro-expansion diff --git a/include/fmt/format-inl.h b/include/fmt/format-inl.h index ee2f83de..524b229a 100644 --- a/include/fmt/format-inl.h +++ b/include/fmt/format-inl.h @@ -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]); } -FMT_FUNC void grisu2_round(char *buffer, size_t size, uint64_t delta, - uint64_t remainder, uint64_t exp, uint64_t diff) { +FMT_FUNC bool grisu2_round( + 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 && (remainder + exp < diff || diff - remainder > remainder + exp - diff)) { --buffer[size - 1]; remainder += exp; } + if (size > max_digits) { + --size; + ++exp10; + if (buffer[size] >= '5') + return false; + } + return true; } // Generates output using Grisu2 digit-gen algorithm. -FMT_FUNC void grisu2_gen_digits( - const fp &scaled_value, const fp &scaled_upper, uint64_t delta, - char *buffer, size_t &size, int &dec_exp) { - 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(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. +FMT_FUNC bool grisu2_gen_digits( + char *buffer, size_t &size, uint32_t hi, uint64_t lo, int &exp, + uint64_t delta, const fp &one, const fp &diff, size_t max_digits) { // Generate digits for the most significant part (hi). while (exp > 0) { uint32_t digit = 0; @@ -507,12 +505,11 @@ FMT_FUNC void grisu2_gen_digits( buffer[size++] = static_cast('0' + digit); --exp; uint64_t remainder = (static_cast(hi) << -one.e) + lo; - if (remainder <= delta) { - dec_exp += exp; - grisu2_round(buffer, size, delta, remainder, - static_cast(data::POWERS_OF_10_32[exp]) << -one.e, - diff.f); - return; + if (remainder <= delta || size > max_digits) { + return grisu2_round( + buffer, size, max_digits, delta, remainder, + static_cast(data::POWERS_OF_10_32[exp]) << -one.e, + diff.f, exp); } } // Generate digits for the least significant part (lo). @@ -524,49 +521,13 @@ FMT_FUNC void grisu2_gen_digits( buffer[size++] = static_cast('0' + digit); lo &= one.f - 1; --exp; - if (lo < delta) { - dec_exp += exp; - grisu2_round(buffer, size, delta, lo, one.f, - diff.f * data::POWERS_OF_10_32[-exp]); - return; + if (lo < delta || size > max_digits) { + return grisu2_round(buffer, size, max_digits, delta, lo, one.f, + diff.f * data::POWERS_OF_10_32[-exp], exp); } } } -template -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. FMT_FUNC char *write_exponent(char *buffer, int exp) { 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; } -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(size) - 1; - int num_digits = precision - static_cast(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(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 # define FMT_FALLTHROUGH [[clang::fallthrough]]; #elif FMT_GCC_VERSION >= 700 @@ -659,60 +559,159 @@ FMT_FUNC void grisu2_prettify(char *buffer, size_t &size, int exp, # define FMT_FALLTHROUGH #endif -// Formats a nonnegative value using Grisu2 algorithm. Grisu2 doesn't give any -// guarantees on the shortness of the result. -template -FMT_FUNC typename std::enable_if::type - grisu2_format(Double value, char *buffer, size_t &size, char type, - int precision, bool write_decimal_point) { - FMT_ASSERT(value >= 0, "value is negative"); - int dec_exp = 0; // K in Grisu. - if (value > 0) { - grisu2_format_positive(value, buffer, size, dec_exp); - } else { - *buffer = '0'; - size = 1; - } - const int default_precision = 6; - if (precision < 0) - precision = default_precision; - bool upper = false; - switch (type) { - case 'G': - upper = true; - FMT_FALLTHROUGH - case '\0': case 'g': { - int digits_to_remove = static_cast(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; +struct gen_digits_params { + unsigned min_digits; + unsigned max_digits; + bool fixed; + bool upper; + bool trailing_zeros; + + // Creates digit generation parameters from format specifiers for a number in + // the range [pow(10, exp - 1), pow(10, exp) or 0 if exp == 1. + gen_digits_params(const grisu2_specs &specs, int exp) + : min_digits(specs.precision >= 0 ? to_unsigned(specs.precision) : 6), + fixed(false), upper(false), trailing_zeros(false) { + switch (specs.type) { + case 'G': + upper = true; + FMT_FALLTHROUGH + case '\0': case 'g': + trailing_zeros = (specs.flags & HASH_FLAG) != 0; + if (-4 <= exp && exp < static_cast(min_digits) + 1) { + fixed = true; + if (!specs.type && trailing_zeros && exp >= 0) + min_digits = to_unsigned(exp) + 1; } + break; + case 'F': + upper = true; + FMT_FALLTHROUGH + case 'f': { + fixed = true; + trailing_zeros = true; + int adjusted_min_digits = static_cast(min_digits) + exp; + if (adjusted_min_digits > 0) + min_digits = to_unsigned(adjusted_min_digits); + break; } - precision = 0; - break; - } - case 'F': - upper = true; - FMT_FALLTHROUGH - case 'f': { - int digits_to_remove = -dec_exp - precision; - if (digits_to_remove > 0) { - if (digits_to_remove >= static_cast(size)) - digits_to_remove = static_cast(size) - 1; - round(buffer, size, dec_exp, digits_to_remove); + case 'E': + upper = true; + FMT_FALLTHROUGH + case 'e': + ++min_digits; + break; } - 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 ¶ms) { + 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(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; } - if (write_decimal_point && precision < 1) - precision = 1; - grisu2_prettify(buffer, size, dec_exp, precision, upper); + // pow(10, full_exp - 1) <= v <= pow(10, full_exp). + int int_size = static_cast(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; + int num_zeros = static_cast(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 +FMT_FUNC typename std::enable_if::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(upper.f >> -one.e); + int exp = static_cast(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 diff --git a/include/fmt/format.h b/include/fmt/format.h index 4ab5e675..dfe9aa8a 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -289,15 +289,20 @@ inline bool use_grisu() { return FMT_USE_GRISU && std::numeric_limits::is_iec559; } +struct grisu2_specs { + int precision; + char type; + uint_least8_t flags; +}; + // Formats value using Grisu2 algorithm: // https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf template -FMT_API typename std::enable_if::type - grisu2_format(Double value, char *buffer, size_t &size, char type, - int precision, bool write_decimal_point); +FMT_API typename std::enable_if::type + grisu2_format(Double value, char *buffer, size_t &size, grisu2_specs); template -inline typename std::enable_if::type - grisu2_format(Double, char *, size_t &, char, int, bool) {} +inline typename std::enable_if::type + grisu2_format(Double, char *, size_t &, grisu2_specs) { return false; } template typename Allocator::value_type *allocate(Allocator& alloc, std::size_t n) { @@ -1203,13 +1208,13 @@ struct align_spec : empty_spec { template class basic_format_specs : public align_spec { public: - unsigned flags_; int precision_; + uint_least8_t flags_; char type_; FMT_CONSTEXPR basic_format_specs( 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 int precision() const { return precision_; } @@ -2881,16 +2886,23 @@ void basic_writer::write_double(T value, const format_specs &spec) { memory_buffer buffer; char type = static_cast(spec.type()); - if (internal::const_check( - internal::use_grisu() && sizeof(T) <= sizeof(double)) && - type != 'a' && type != 'A') { + bool use_grisu = internal::use_grisu() && sizeof(T) <= sizeof(double) && + type != 'a' && type != 'A'; + if (use_grisu) { char buf[100]; // TODO: correct buffer size size_t size = 0; - internal::grisu2_format(static_cast(value), buf, size, type, - spec.precision(), spec.flag(HASH_FLAG)); - FMT_ASSERT(size <= 100, "buffer overflow"); - buffer.append(buf, buf + size); // TODO: avoid extra copy - } else { + auto gs = internal::grisu2_specs(); + gs.type = type; + gs.precision = spec.precision(); + gs.flags = spec.flags_; + use_grisu = internal::grisu2_format( + static_cast(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); normalized_spec.type_ = handler.type; write_double_sprintf(value, normalized_spec, buffer); diff --git a/test/format-impl-test.cc b/test/format-impl-test.cc index 4679436c..49132f60 100644 --- a/test/format-impl-test.cc +++ b/test/format-impl-test.cc @@ -104,7 +104,7 @@ TEST(FPTest, GetCachedPower) { TEST(FPTest, Grisu2FormatCompilesWithNonIEEEDouble) { 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 diff --git a/test/printf-test.cc b/test/printf-test.cc index bb656859..f9208c16 100644 --- a/test/printf-test.cc +++ b/test/printf-test.cc @@ -180,13 +180,8 @@ TEST(PrintfTest, HashFlag) { safe_sprintf(buffer, "%#E", -42.0); EXPECT_PRINTF(buffer, "%#E", -42.0); - if (fmt::internal::use_grisu()) { - EXPECT_PRINTF("-42.0", "%#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); - } + EXPECT_PRINTF("-42.0000", "%#g", -42.0); + EXPECT_PRINTF("-42.0000", "%#G", -42.0); safe_sprintf(buffer, "%#a", 16.0); EXPECT_PRINTF(buffer, "%#a", 16.0);