Printf fixes around handling of -0.0 (negative zero) - #18147
Conversation
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
|
OK so after reading some of the review comments here, I went and checked the output of the currently released sprintf against both .NET 9 and .NET framework. The code: printfn "-- floats --"
printfn "%%f +0.0f => %f" +0.0f
printfn "%%f -0.0f => %f" -0.0f
printfn "%%+f +0.0f => %+f" +0.0f
printfn "%%+f -0.0f => %+f" -0.0f
printfn "%%010.3f +0.0f => %010.3f" +0.0f
printfn "%%010.3f -0.0f => %010.3f" -0.0f
printfn "-- decimals --"
printfn "%%f +0.0m => %f" +0.0m
printfn "%%f -0.0m => %f" -0.0m
printfn "%%+f +0.0m => %+f" +0.0m
printfn "%%+f -0.0m => %+f" -0.0m
printfn "%%010.3f +0.0m => %010.3f" +0.0m
printfn "%%010.3f -0.0m => %010.3f" -0.0mOutput for .NET 9: Output for .NET Framework 4.8: As you can see, neither #15557 nor #15558 are present in the .NET framework run (which makes some sense given @tannergooding's comments). Furthermore, the .NET framework version always treats (decimal and float) negative zero as positive zero for formatting purposes (which I didn't realize before since I didn't think to also test against Framework). Therefore I will amend this PR to preserve the .NET framework behavior in both cases. I personally don't particularly care how Additionally, it looks like |
|
Actually there is still one scenario that I'd argue is a bug under .NET framework, too: sprintf "%+f" -0.0000001Under .NET framework this prints "0.000000" which is definitely a bug ( Thoughts? |
.NET Framework has many bugs that will never be fixed due to its much stronger back-compat bar. Other bugs around IEEE 754 floating-point include things like Typically this much stronger backwards compatibility bar means that the behavior should be preserved "as is" on .NET Framework and fixes should only be taken on .NET [Core] where relevant. F# could decide differently, but that itself comes with its own risks and potential for new problems due to it differing from what users may expect and differing from what they'd experience using the regular BCL APIs.
This assumption is already broken (on .NET Framework) by negative zero. Negative zero itself exists due to there being negative non-zero results which round towards zero due to the precision limitations of the underlying format, for scientific and other mathematical domains this information is often relevant and so is pertinent to display and preserve (which is different from If you're printing with a limited number of digits, then today this functions (in both F# and the BCL) by functionally rounding to that many digits; thus if you print to 2 fractional digits then Edit: Noting that "rounding to that many digits" is meant to account for the exact underlying represented value, not strictly the literal the user visualizes, thus |
I should correct myself; I meant it's that
Again, let me refine that statement:
EDIT: That being said, given this comment:
If we were to decide in FSharp.Core to fix the bug by making let determineSign n = if n >= 0.0 then "+" else "-"to something like: let determineSign n nDecimalPlaces = if (roundTo n nDecimalPlaces) >= 0.0 then "+" else "-"My feeling was that there would still be room for some floating-point weirdness, but perhaps I was wrong; would that actually be a reasonable approach to take? |
|
I am reviewing PRs that could in theory still make it for NET10 if pursued. If yes, do you want any guidance? |
|
@T-Gro Sure I'd be interested in finishing this. I had unresolved questions about certain fixes (see #18147 (comment) and #18147 (comment) - though honestly we should probably all just re-read through this thing to refresh ourselves; there's a lot of corner cases) |
|
Here is a proposal. Instead of stripping the sign everywhere, make // GenericNumber
#if NETSTANDARD2_1_OR_GREATER // ns2.1 + net10 (Core family)
let inline singleIsPositive (n: single) = not (Single.IsNegative n)
let inline doubleIsPositive (n: double) = not (Double.IsNegative n)
#else // ns2.0 (.NET Framework contract)
let inline singleIsPositive (n: single) = n >= 0.0f
let inline doubleIsPositive (n: double) = n >= 0.0
#endif
let inline decimalIsPositive (n: decimal) = n >= 0.0M // unchanged: no signed zero
WDYT? |
|
Sounds reasonable. Though it does make the assumption that netstandard 2.0 = .net framework behavior, which is 99% of the time fine but I think there technically are situations where that could be violated (i.e. somehow paket (and nuget?) edge cases causing the ns 2.0 FSharp.Core to be used for .net core family; alternate runtimes like Unity / mono). However those could probably be considered collateral damage, I think at worst you'd just have the original "bugged" behavior persisting (the status quo). Actually a few more options occurred to me which could sidestep that and fix it everywhere (at the cost of like 1 iota more complexity):
|
… behavior on .NET Framework
…ork on net framework, net core on net core), and reimplement the logic All tests passing now (locally).
…ly running due to partial application It seems these tests have not been running for quite a while, if ever
…ypes, not just single/double/decimal
|
@T-Gro should we perhaps enable all printf tests? Maybe it's running faster now / we all have better hardware? At least on my machine it runs the full thing in less than a minute (14 seconds for both net11.0 and net472, not including build time). (or perhaps the reason is that it's still too slow in the CI VMs?) |
|
@jwosty : Yeah lets run them all, nice catch (I did not know about this randomized setting) |
|
@charlesroddie : I am going to merge this as the behavior this fixes is not good no matter the angle you take ( I have recently merged support for extension members and operators in SRTP - this could open up the door to using a dedicated |
No work is needed to create Rational types and that's not this issue. The solution to floating point tostring rendering with "-0" is not to avoid floating point numbers. We need to fix this. I'm going to try to do this starting with non-reflection methods. |
|
@charlesroddie I think I understand what you're saying now. I can see how it's reasonable to argue that However such a thing would very much be a breaking change, since Probably the best remedy at this point would be to have a new format flag that lets you opt-out of signed zero formatting behavior. Like |
Description
Fixes #15557 and #15558
Checklist
Test cases added
Release notes entry updated: