microsoft / STL

MSVC's implementation of the C++ Standard Library.
Other
10.19k stars 1.5k forks source link

`<format>`: The Standard should clarify that `"{:#.6}"` keeps trailing zeros #2192

Open StephanTLavavej opened 3 years ago

StephanTLavavej commented 3 years ago

Test case and output

Thanks to @statementreply! :heart_eyes_cat:

#include <cstdio>
#include <format>
using namespace std;

int main() {
    puts(format("format(\"{{:#}}\", 1.0)    = {:#}", 1.0).c_str());
    puts(format("format(\"{{:#.6}}\", 1.0)  = {:#.6}", 1.0).c_str());
    puts(format("format(\"{{:#g}}\", 1.0)   = {:#g}", 1.0).c_str());
    puts(format("format(\"{{:#.6g}}\", 1.0) = {:#.6g}", 1.0).c_str());
    return 0;
}

After PR #2157

format("{:#}", 1.0)    = 1.
format("{:#.6}", 1.0)  = 1.00000
format("{:#g}", 1.0)   = 1.00000
format("{:#.6g}", 1.0) = 1.00000

libfmt behavior - https://godbolt.org/z/qns4carK7

format("{:#}", 1.0)    = 1.0
format("{:#.6}", 1.0)  = 1.00000
format("{:#g}", 1.0)   = 1.00000
format("{:#.6g}", 1.0) = 1.00000

My analysis

"{:#g}" and "{:#.6g}" - unambiguous

For "{:#g}" and "{:#.6g}", where After PR and libfmt agree that the output is "1.00000", I believe that the Standardese is clear. N4892 [format.string.std]/22 passes "6 if precision is not specified" so they are certainly equivalent. to_chars chars_format::general, 6 wants to trim the zeros and decimal point, but [format.string.std]/6 suppresses both trimmings from happening.

"{:#.6}" - wording issue

For "{:#.6}", where After PR and libfmt agree that the output is "1.00000", I believe that the Standardese is technically unclear, and should be patched with an LWG Issue. According to [format.string.std]/22, this has a type of "none", but because the precision is specified, we call to_chars with chars_format::general, 6. As before, to_chars wants to trim the zeros and decimal point. [format.string.std]/6 sentence 4 definitely keeps the decimal point. However, [format.string.std]/6 sentence 6 keeps trailing zeros "for g and G conversions".

:grey_question: The question is, because type-none-with-precision performs a chars_format::general, 6 conversion, should it be considered a g conversion?

:grey_exclamation: I believe the answer is "yes" - the user's mental model should be "if I pass a precision but not a letter, the letter defaults to g and I get all the behavior that I would have gotten if I explicitly said g". This is true at the to_chars layer (as the call is exactly chars_format::general, 6) and the same should be true for any format post-processing/modified behavior. The fact that libfmt displays this behavior is some additional evidence in favor of this being a reasonable interpretation.

"{:#}" - implementation divergence

For "{:#}", where After PR outputs "1.", these sentences are even more important. [format.string.std]/22 clearly begins by calling to_chars(first, last, value), i.e. this is plain shortest, not general precision. Thus, unlike "{:#.6}" which (in my opinion above) should be considered an "honorary" g conversion, "{:#}" should not be considered a g conversion. (Plain shortest switches between fixed and scientific with the fewest-characters, tiebreak-prefers-fixed criterion; it does not trim zeros because it doesn't emit them in the first place.) Again, [format.string.std]/6 sentence 4 definitely keeps the decimal point.

:question: The question here is, does [format.string.std]/6 sentence 6 apply?

:exclamation: I believe the answer is no: this is not a g conversion at the format specifier level, this is not a chars_format::general conversion at the to_chars level, no precision was passed to to_chars, and no trailing zeros were removed by to_chars. (There's definitely no "default of 6 precision" involved here, as plain shortest is information-preserving - it may need to emit up to 17 significant digits to round-trip a double.)

Proposed resolution: :smile_cat:

I believe that this could be clarified in the Standard by changing [format.string.std]/6 sentence 6 from:

In addition, for g and G conversions, trailing zeros are not removed from the result.

to:

In addition, for conversions that call to_chars with chars_format::general, trailing zeros are not removed from the result.

Notes

This captures the discussion in https://github.com/microsoft/STL/pull/2157#discussion_r702247363 and https://github.com/microsoft/STL/pull/2157#discussion_r703936220 .

StephanTLavavej commented 3 years ago

@vitaut https://github.com/microsoft/STL/pull/2157#discussion_r703949775

Great analysis, @StephanTLavavej. All makes sense and even though I'm not a big fan of 1. it is consistent with my interpretation of the wording and {fmt} should probably do the same in this case.

@miscco https://github.com/microsoft/STL/pull/2157#discussion_r704112114

We could also change the wording to add a trailing zero if the dot is added