Skip to content

Conversation

@stefano-zanotti-88
Copy link
Contributor

Right now, NPF uses "round to nearest, ties to infinity".
printf should actually use the current rouding mode configured in the system.
See https://en.cppreference.com/w/c/numeric/fenv/FE_round
See also the standard, about printf:

f, F [...] The value is rounded to the appropriate number of digits.
[...]
Recommended practice [...]
For e, E, f, F, g, and G conversions, if the number of significant decimal digits is at most the maximum
value M of the T_DECIMAL_DIG macros (defined in <float.h>), then the result should be correctly
rounded. If the number of significant decimal digits is more than M but the source value is
exactly representable with M digits, then the result should be an exact representation with trailing
zeros. Otherwise, the source value is bounded by two adjacent decimal strings L < U, both having
M significant digits; the value of the resultant decimal string D should satisfy L ≤ D ≤ U, with the
extra stipulation that the error should have a correct sign for the current rounding direction.

Using the system rounding mode might be overkill for a "nano" library.
However, the most common rounding is "round to nearest, ties to even", slightly different from what NPF uses.
This PR changes NPF to use round-to-even.
This is the FE_TONEAREST of the standard (and roundTiesToEven of IEEE 754), whereas NPF's current policy corresponds to FE_TONEARESTFROMZERO (and roundTiesToAway of IEEE 754).

I'll let you decide whether the increase in code size is worth it.

Note that "even" refers to the least significant decimal digit that survives the rounding, ie 2.345000 rounded to 2 fractional digits is 2.34 because "4" is even, whereas 2.315000 is 2.32 because "1" is odd.
Also note that these examples are correct only with infinite precision. With float64, the numbers are actually:
2.345000 -> 2.345000000000000195399252334027551114559173583984375000000000
2.315000 -> 2.314999999999999946709294817992486059665679931640625000000000
So, the first one would round up because it is not actually a tie.
And the second one would round down, for the same reason.
That is, they would both round in the opposite direction as the one that would be used with infinite precision.
However, some numbers do indeed round in the infinite-precision way (those numbers whose full mantissa fits in a double).
For instance (rounding to the last-but-one digit):
0.5 -> rounds to 0
0.25 -> rounds to 0.2
0.75 -> rounds to 0.8
1.5 -> rounds to 2
1.25 -> rounds to 1.2
1.75 -> rounds to 1.8

Also note that NPF has inaccurate rounding. This is true especially if the integer used for the calculations has less bits than double, but it also happens in some other cases, if we don't have enough additional bits (see the discussion in #325).
As a result, perfect ties might be overestimated or understimated by NPF, resulting in a possibly wrong rounding. Also, non-ties might appear as perfect ties, again with possibly wrong results.
Anyway, this best effort does improve things, on average.

With 64-bit double, and uint64_t as NANOPRINTF_CONVERSION_FLOAT_TYPE, all the following cases do round appropriately.
The long expansions are not from NPF, they are just to see which ones are actually ties.
The part after "vs" is the current NPF rounding (only for the wrong ones)

printf("%6.0f       %.20f\n", 0.5      , 0.5      ); // "     0       0.50000000000000000000"  vs "1"
printf("%6.0f       %.20f\n", 1.5      , 1.5      ); // "     2       1.50000000000000000000"
printf("%6.0f       %.20f\n", 0.5000001, 0.5000001); // "     1       0.50000009999999994736"
printf("%6.0f       %.20f\n", 1.5000001, 1.5000001); // "     2       1.50000010000000005839"
printf("%6.1f       %.20f\n", 0.05     , 0.05     ); // "   0.1       0.05000000000000000278"
printf("%6.1f       %.20f\n", 0.15     , 0.15     ); // "   0.1       0.14999999999999999445"
printf("%6.1f       %.20f\n", 1.05     , 1.05     ); // "   1.1       1.05000000000000004441"
printf("%6.1f       %.20f\n", 1.15     , 1.15     ); // "   1.1       1.14999999999999991118"
printf("%6.1f       %.20f\n", 1.25     , 1.25     ); // "   1.2       1.25000000000000000000"  vs "1.3"
printf("%6.1f       %.20f\n", 1.35     , 1.35     ); // "   1.4       1.35000000000000008882"
printf("%6.0f       %.20f\n", 0.499    , 0.499    ); // "     0       0.49899999999999999911"

printf("%6.0f       %.20f\n", 0.5      , 0.5      ); // "     0       0.50000000000000000000"  vs "1"
printf("%6.1f       %.20f\n", 0.25     , 0.25     ); // "   0.2       0.25000000000000000000"  vs "0.3"
printf("%6.2f       %.20f\n", 0.125    , 0.125    ); // "  0.12       0.12500000000000000000"  vs "0.13"
printf("%6.1f       %.20f\n", 0.75     , 0.75     ); // "   0.8       0.75000000000000000000"
printf("%6.0f       %.20f\n", 1.5      , 1.5      ); // "     2       1.50000000000000000000"
printf("%6.1f       %.20f\n", 1.25     , 1.25     ); // "   1.2       1.25000000000000000000"  vs "1.3"
printf("%6.2f       %.20f\n", 1.125    , 1.125    ); // "  1.12       1.12500000000000000000"  vs "1.13"
printf("%6.1f       %.20f\n", 1.75     , 1.75     ); // "   1.8       1.75000000000000000000"
printf("%6.0f       %.20f\n", 2.5      , 2.5      ); // "     2       2.50000000000000000000"  vs "3"
printf("%6.1f       %.20f\n", 2.25     , 2.25     ); // "   2.2       2.25000000000000000000"  vs "2.3"
printf("%6.2f       %.20f\n", 2.125    , 2.125    ); // "  2.12       2.12500000000000000000"  vs "2.13"
printf("%6.1f       %.20f\n", 2.75     , 2.75     ); // "   2.8       2.75000000000000000000"

This is just for %f.
If you find it worthwhile, something similar will be needed for %e and %g, working from #325, especially considering that the rounding, in those cases, can also happen at an arbitrary integral digit, not just a fractional one. I'll have to think about that, it seems we might need something quite convoluted.

@charlesnicholson
Copy link
Owner

This PR has no changes to the codebase; is that intentional?

@stefano-zanotti-88
Copy link
Contributor Author

Sorry, I created the PR from the wrong branch.
Please see here:
stefano-zanotti-88@3fa4f05
which contains all the relevant changes.
If you like them, I'll fix the PR or open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants