-
Notifications
You must be signed in to change notification settings - Fork 790
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Printf fixes around handling of -0.0 (negative zero) #18147
base: main
Are you sure you want to change the base?
Changes from 9 commits
21a6776
541dad2
cad21b4
74a0c91
5a5e37e
f33ccc7
fcd573c
d8485cb
274544c
06fc2ab
68f5af1
9c035f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -658,8 +658,28 @@ module internal PrintfImpl = | |
|
||
/// Contains functions to handle left/right and no justification case for numbers | ||
module GenericNumber = | ||
|
||
let isPositive (n: obj) = | ||
|
||
let inline doubleIsPositive (n: double) = | ||
n >= 0.0 | ||
// Ensure -0.0 is treated as negative (see https://github.com/dotnet/fsharp/issues/15557) | ||
// and use bitwise comparison because floating point comparison treats +0.0 as equal to -0.0 | ||
&& (BitConverter.DoubleToInt64Bits n <> BitConverter.DoubleToInt64Bits -0.0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this going to do the right thing for |
||
|
||
let inline singleIsPositive (n: single) = | ||
doubleIsPositive (float n) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to the question of |
||
|
||
let decimalSignBit (n: decimal) = | ||
// Unfortunately it's impossible to avoid this array allocation without either targeting .NET 5+ or relying on knowledge about decimal's internal representation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could likely explicitly document this if really needed, but it's generally safe to depend on. There is a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The library is netstandard2.0/2.1, which I think rules out the IsPositive/IsNegative methods. And ._flags field itself is private, I am not sure there is a safe way to read it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I would vote against doing it that way |
||
let bits = Decimal.GetBits n | ||
bits[3] >>> 31 | ||
|
||
let inline decimalIsNegativeZero (n: decimal) = | ||
decimalSignBit n <> 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is notably checking |
||
|
||
let inline decimalIsPositive (n: decimal) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's much cheaper to just check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same as above, this has to build with netstandard2.0. |
||
n > 0.0M || (n = 0.0M && decimalSignBit n = 0) | ||
|
||
let isPositive (n: obj) = | ||
match n with | ||
| :? int8 as n -> n >= 0y | ||
| :? uint8 -> true | ||
|
@@ -671,9 +691,9 @@ module internal PrintfImpl = | |
| :? uint64 -> true | ||
| :? nativeint as n -> n >= 0n | ||
| :? unativeint -> true | ||
| :? single as n -> n >= 0.0f | ||
| :? double as n -> n >= 0.0 | ||
| :? decimal as n -> n >= 0.0M | ||
| :? single as n -> singleIsPositive n | ||
| :? double as n -> doubleIsPositive n | ||
| :? decimal as n -> decimalIsPositive n | ||
| _ -> failwith "isPositive: unreachable" | ||
|
||
/// handles right justification when pad char = '0' | ||
|
@@ -852,11 +872,19 @@ module internal PrintfImpl = | |
|
||
module FloatAndDecimal = | ||
|
||
let fixupDecimalSign (n: decimal) (nStr: string) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vzarytovskii we should probably just follow the same behavior as .NET here then, right? i.e. make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would ping @T-Gro for that, let the team decide what's desired and whether suggestion is needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a change we could take - but it should be documented on the .NET breaking changes and happen on a major version update. |
||
// Forward-compatible workaround for a .NET bug which causes -0.0m (negative zero) to be missing its sign (see: https://github.com/dotnet/runtime/issues/110712) | ||
// This also affects numbers which round/truncate to negative zero (i.e. very small negative numbers) | ||
if (n = 0.0m && not (nStr.StartsWith "-") && GenericNumber.decimalIsNegativeZero n) || (n < 0.0m && not (nStr.StartsWith "-")) then | ||
"-" + nStr | ||
else | ||
nStr | ||
|
||
let rec toFormattedString fmt (v: obj) = | ||
match v with | ||
| :? single as n -> n.ToString(fmt, CultureInfo.InvariantCulture) | ||
| :? double as n -> n.ToString(fmt, CultureInfo.InvariantCulture) | ||
| :? decimal as n -> n.ToString(fmt, CultureInfo.InvariantCulture) | ||
| :? decimal as n -> n.ToString(fmt, CultureInfo.InvariantCulture) |> fixupDecimalSign n | ||
| _ -> failwith "toFormattedString: unreachable" | ||
|
||
let isNumber (x: obj) = | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you just use
double.IsPositive(n)
, or does F# need to differ?.NET Framework notably has different behavior than .NET (Core), which is also intentional as .NET Framework has a much higher backwards compatibility bar