Skip to content

Fix the text output of to_chars, snprintf, operator<< and std::format - #1536

Merged
mborland merged 7 commits into
boostorg:developfrom
ibmibmibm:ostream-fixes
Oct 27, 2026
Merged

mborland merged 7 commits into
boostorg:developfrom
ibmibmibm:ostream-fixes

Conversation

@ibmibmibm

Copy link
Copy Markdown
Contributor

This PR has 7 commits. Each commit after the first one fixes one issue, and each later fix uses the earlier ones.

Fixes #1530
Fixes #1531
Fixes #1532
Fixes #1533
Fixes #1534
Fixes #1535

Correctness
  • The new tests pass at each commit with GCC 16.2 and the warning flags of the Jamfile.
  • The changed tests pass with MSVC 19.51, x64 and x86, /W4 /WX.
  • The changed tests pass with AddressSanitizer and UndefinedBehaviorSanitizer.
  • The 128-bit print gives the same output as develop for 12M to_chars calls, and the exact value for 20M random integers.
Speed

The 128-bit print makes to_chars of decimal128_t and decimal_fast128_t at full precision 34% to 48% faster with GCC and Clang, in the general, scientific and fixed formats. For example, decimal_fast128_t scientific goes from 93 ns to 54 ns.

@cppalliance-bot

cppalliance-bot commented Oct 26, 2026 •

Copy link
Copy Markdown

An automated preview of the documentation is available at https://1536.decimal.prtest3.cppalliance.org/libs/decimal/doc/html/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-10-27 04:20:20 UTC

@ibmibmibm
ibmibmibm force-pushed the ostream-fixes branch 2 times, most recently from cc3f0d2 to f9404c0 Compare October 26, 2026 08:07
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.27907% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.4%. Comparing base (c7a8e48) to head (baaf14f).

Files with missing lines Patch % Lines
test/github_issue_1533.cpp 93.7% 8 Missing ⚠️
test/github_issue_1532.cpp 95.0% 7 Missing ⚠️
include/boost/decimal/charconv.hpp 96.3% 5 Missing ⚠️
include/boost/decimal/cstdio.hpp 97.7% 3 Missing ⚠️
include/boost/decimal/detail/io.hpp 97.6% 3 Missing ⚠️
...ude/boost/decimal/detail/to_chars_integer_impl.hpp 82.4% 3 Missing ⚠️
test/github_issue_1534.cpp 83.4% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #1536     +/-   ##
=========================================
- Coverage     98.5%   98.4%   -0.0%     
=========================================
  Files          326     331      +5     
  Lines        27695   28245    +550     
  Branches      2305    2326     +21     
=========================================
+ Hits         27255   27784    +529     
- Misses         440     461     +21     
Files with missing lines Coverage Δ
include/boost/decimal/detail/locale_conversion.hpp 97.7% <100.0%> (+3.0%) ⬆️
include/boost/decimal/detail/u256.hpp 87.6% <100.0%> (ø)
test/github_issue_1530.cpp 100.0% <100.0%> (ø)
test/github_issue_1531.cpp 100.0% <100.0%> (ø)
test/test_decimal32_fast_stream.cpp 100.0% <100.0%> (ø)
test/test_decimal32_stream.cpp 100.0% <100.0%> (ø)
test/test_snprintf.cpp 100.0% <100.0%> (ø)
include/boost/decimal/cstdio.hpp 96.5% <97.7%> (-0.6%) ⬇️
include/boost/decimal/detail/io.hpp 92.5% <97.6%> (+12.9%) ⬆️
...ude/boost/decimal/detail/to_chars_integer_impl.hpp 83.9% <82.4%> (-7.6%) ⬇️
... and 4 more

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c7a8e48...baaf14f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ibmibmibm
ibmibmibm marked this pull request as ready for review October 27, 2026 04:13
- to_chars_integer_impl divided a 128-bit integer by 10^9 for each group
  of 9 digits. This is up to 5 slow 128-bit divisions.
- It now divides once by 10^19 with divmod_pow10_uint128. The 64-bit
  remainder gives the last 19 digits. A quotient below 10^19 takes the
  64-bit path, and a larger quotient is one digit and 19 more digits.
- The new to_chars_19_digits writes 19 digits with the leading zeros.
- The results do not change.
- Remove the includes that to_chars_integer_impl.hpp does not use:
  <array>, <utility>, <cstring>, <cstdio>, <cerrno>, <cmath> and a
  second <limits>.
- to_chars_round divides by the dropped power of ten once, keeps the
  remainder as the sticky bit, and rounds with the sign of the value.
- The scientific format moves the exponent when the result gets one more
  digit.
- The scientific path compares the precision without an overflow, checks
  the room for the minus sign, and checks the room for the trailing
  zeros without a pointer past the buffer.
- The fixed format with a precision has a new path,
  to_chars_fixed_precision. It rounds |x| * 10^precision to an integer
  and puts the point in front of its last precision digits.
- to_chars_fixed_precision computes its lengths in 64 bits, so a
  precision near INT_MAX gives value_too_large and not a signed
  overflow.
- to_chars_fixed_impl now handles only the general format, so its
  branches for the fixed format are removed.
- charconv.hpp includes <limits>, <system_error> and <type_traits>,
  which it uses.

Fixes boostorg#1530
- The new to_chars_general_precision rounds the significand once to P
  digits, where P is the precision and 0 counts as 1. It uses the fixed
  format when the exponent X after the rounding is in [-4, P), and the
  scientific format for other values. It removes the trailing zeros.
- to_chars_fixed_impl now gets only the fixed format, so its general
  format code is removed.
- The fixed path and the nonfinite path check their own room, so the
  shared precision check is removed.
- The zero and the negative nan check the room for their exact length,
  without an overflow.
- The scientific format checks the room for the exponent before it
  writes. Its code for the general format is removed, because the
  general format reaches it only for zero, inf and nan.
- The decimal32 stream tests now expect the std::ostream output for a
  double.

Fixes boostorg#1531
- The parser reads the flags "-", "+", " " and "0", the field width, the
  precision, the type modifier and the conversion character, and stops
  after it.
- Each value is cast to the type of its modifier first. The cast value
  sizes the buffer, decides the 0 flag and gets printed, so a value that
  narrows to inf pads with spaces.
- Each value is formatted into a buffer of the to_chars bound plus the
  length of the integer part for the separators. The buffer is on the
  heap when the value is long. The value is then copied with its padding
  into the output.
- The new detail::to_chars_length gives the to_chars bound from the
  format, the precision and the exponent. It keeps the general format to
  the digits of the type, and the fixed format to its integer digits and
  precision. inf and nan do not use the precision, so a large precision
  does not size their buffer.
- The 0 flag pads every finite value with zeros, also a hex value that
  starts with a letter.
- A long padding goes into the output in one block, and the part that
  does not fit is only counted.
- When the output does not fit, snprintf writes what fits, adds the null
  terminator and returns the length of the full output, as std::snprintf
  does.
- A conversion with no value returns -1 with errno EINVAL.
- A width or a precision above INT_MAX, or a full count above INT_MAX,
  returns -1 with EOVERFLOW, as printf does.
- fprintf formats once into a local buffer, or into a heap buffer of the
  length that the first pass returned. If the second pass fails, fprintf
  writes nothing and returns -1.
- The grouping uses all the group sizes, and stops at the first
  character that is not a digit. detail::group_size and
  detail::count_separators hold the grouping rule for snprintf and
  operator<<.
- A hex value gets the decimal point of the locale but no grouping, as
  printf does.
- The separators go in by offsets from the start of the buffer, so no
  pointer goes before it.
- make_uppercase moves to charconv.hpp, so that operator<< and the
  formatters use the same ASCII rule.
- cstdio.hpp includes <initializer_list> and <type_traits>, which it
  uses. locale_conversion.hpp includes <string> for the grouping, and no
  longer includes <clocale>, which it does not use.

Fixes boostorg#1532
- Take the format from the float field, as for a double.
- Use the stream precision, with a negative precision as 6.
- Format into a local buffer. When the value does not fit, format on the
  heap with the length that the precision and the exponent give. A
  failed conversion sets badbit.
- Size the buffer for showpoint with the general format as for the
  scientific format, because showpoint keeps the trailing zeros.
- Apply uppercase, showpos and showpoint. The general format keeps the
  trailing zeros as printf %#g, and hexfloat keeps the point as %#a.
- Write the value with the fill as num_put does, with the runs of
  characters widened and written in blocks.
- Reset the width before the output, so that it also resets when the
  stream buffer throws.
- Take the grouping and the decimal point from the numpunct of the
  stream character type. Put the separators only between the digits
  before the point, with detail::count_separators.
- If an exception occurs during the output, set badbit. Send the
  exception to the caller only when badbit is in the exception mask.
- Remove the -Wformat-truncation pragma, which guards no snprintf call.
- io.hpp includes <locale> and <new>, which it uses, and no longer
  includes <cinttypes>, which it does not use.

Fixes boostorg#1533
- format.hpp includes <version> first, as the vendored int128 format.hpp
  does.
- The two formatters share detail::parse_format_spec and
  detail::format_decimal in the new detail/format_render.hpp, with the
  sign and alignment enums and a detail::format_spec.
- The buffer has the length that detail::to_chars_length gives. It is on
  the stack for a short value.
- A failed conversion throws format_error, or writes nothing without
  exceptions.
- With no alignment, the 0 flag pads with zeros after the sign, and with
  spaces for infinity and NaN.
- The locale grouping comes before the padding.
- L is accepted before and after the type.
- L writes the value as operator<< does, with the locale of the format
  context. Each formatter gives only the locale access of its context.
- The fill keeps the output character type.
- The uppercase changes only the ASCII letters.
- The parser rejects a and A of a fast type with format_error.
- A width or a precision above INT_MAX gives format_error.
- A bad specification throws the format_error of the library. Without
  exceptions, the parse stops at the bad character, so that the library
  rejects it.
- The padding goes straight to the output, and only L builds a string.
- Update format.adoc.
- format.hpp and fmt_format.hpp include <type_traits>, and
  fmt_format.hpp includes <locale>, which they use. They no longer
  include <iostream>, <iomanip> and <format>, which they do not use.

Fixes boostorg#1534
- Compare the separator as unsigned char with 0xE2, the first byte of
  the UTF-8 U+202F, in one detail::thousands_separator.
- Count the grouping in std::size_t, so the cast to int is gone.
- Copy into the bytes array of the u256.
- Remove the pragmas.
- u256.hpp includes <cstring>, <limits> and <type_traits>, and <ostream>
  for its operator<<. It no longer includes <cmath>, which it does not
  use.

Fixes boostorg#1535

@mborland mborland left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks as always

@mborland
mborland merged commit 752a56f into boostorg:develop Oct 27, 2026
75 checks passed
@ibmibmibm
ibmibmibm deleted the ostream-fixes branch October 28, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment