c42f / tinyformat

Minimal, type safe printf replacement library for C++
531 stars 75 forks source link

Formatting NaN or infinity with explicit precision injects spurious zeros #76

Closed bbannier closed 3 years ago

bbannier commented 3 years ago

When I format e.g., double NaN or infinity values and explicitly request precision (e.g., %.16d), spurious 0 characters are injected into the output. Since NaN or infinity values are probably never formatted as digits I would expect them to not be present at all.

Examples:

CHECK_EQUAL(tfm::format("%.4g", std::numeric_limits<double>::infinity()), "inf");
CHECK_EQUAL(tfm::format("%.0d", std::numeric_limits<double>::infinity()), "inf");
CHECK_EQUAL(tfm::format("%.1d", std::numeric_limits<double>::infinity()), "inf");
CHECK_EQUAL(tfm::format("%.2d", std::numeric_limits<double>::infinity()), "inf");
CHECK_EQUAL(tfm::format("%.3d", std::numeric_limits<double>::infinity()), "inf");
CHECK_EQUAL(tfm::format("%.4d", std::numeric_limits<double>::infinity()), "inf"); // returns `0inf`, fails for this and larger n.
CHECK_EQUAL(tfm::format("%.4g", std::numeric_limits<double>::quiet_NaN()), "nan");
CHECK_EQUAL(tfm::format("%.0d", std::numeric_limits<double>::quiet_NaN()), "nan");
CHECK_EQUAL(tfm::format("%.1d", std::numeric_limits<double>::quiet_NaN()), "nan");
CHECK_EQUAL(tfm::format("%.2d", std::numeric_limits<double>::quiet_NaN()), "nan");
CHECK_EQUAL(tfm::format("%.3d", std::numeric_limits<double>::quiet_NaN()), "nan");
CHECK_EQUAL(tfm::format("%.4d", std::numeric_limits<double>::quiet_NaN()), "nan"); // returns `0nan`, fails for this and larger n.
c42f commented 3 years ago

This is a little tricky because there's no equivalent in normal printf for us to decide what to do here. Normal printf("%.4d", i) requires i to be an integer, which doesn't have any invalid representations equivalent to nan or inf.

I guess the closest equivalent is something like the following comparison:

#include <stdio.h>
#include <limits>
#include <tinyformat.h>

int main()
{
    tfm::printf("'%010.0f'\n", 123.0);
    tfm::printf("'%010.0f'\n", std::numeric_limits<double>::infinity());
    printf("------\n");
    printf("'%010.0f'\n", 123.0);
    printf("'%010.0f'\n", std::numeric_limits<double>::infinity());

    return 0;
}

which produces

'0000000123'
'0000000inf'
------
'0000000123'
'       inf'

so indeed the 0 flag is ignored if the value happens to be inf in this case.

I guess the fix here is to change formatValue() for floating point types so that it checks isfinite() and resets the fill of the stream to in that case. It's a bit ugly, but check #77

bbannier commented 3 years ago

Great! Thank you for looking into this so quickly @c42f.