NumberBox: Double values specified in XAML are corrupted and incorrect values are passed to code
Describe the bug
When using double values in XAML, the values are corrupted and close, but incorrect values, are used instead.
Steps to reproduce the bug
Take the following code to specify a NumberBox with a DecimalFormatter containing a IncrementNumberRounder;
I have used a IncrementNumberRounder as the values supplied to the Increment property must be one of a very specific set of double values otherwise an ArgumentException will be thrown. Details here: https://learn.microsoft.com/en-us/uwp/api/windows.globalization.numberformatting.incrementnumberrounder.increment?view=winrt-22621 The current corruption of double values means that it is impossible to specify an IncrementNumberRounder.Increment value of "0.01" in XAML.
<NumberBox>
<NumberBox.NumberFormatter>
<numberFormatting:DecimalFormatter>
<numberFormatting:DecimalFormatter.NumberRounder>
<numberFormatting:IncrementNumberRounder Increment="0.01" />
</numberFormatting:DecimalFormatter.NumberRounder>
</numberFormatting:DecimalFormatter>
</NumberBox.NumberFormatter>
</NumberBox>
The code above, will cause the following Exception to be thrown at runtime:
From the image you can see that the value "0.0099999997764825821" is used, not the value "0.01" as specified in the XAML. This causes the assignment to the Increment property to correctly throw an InvalidArgument exception.
To prove that this is not caused by a rounding error, or a limitation with double precision and is an issue with the XAML specified value being corrupted; the following work-around can be used;
<NumberBox>
<NumberBox.NumberFormatter>
<numberFormatting:DecimalFormatter>
<numberFormatting:DecimalFormatter.NumberRounder>
<numberFormatting:IncrementNumberRounder Increment="{x:Bind Increment}" />
</numberFormatting:DecimalFormatter.NumberRounder>
</numberFormatting:DecimalFormatter>
</NumberBox.NumberFormatter>
</NumberBox>
where in the code-behind, the property Increment is defined as:
public double Increment => 0.01;
This code moves the "0.01" double value out of the XAML and into the code-behind. This bypasses the issue and the correct "0.01" value is passed to the Increment property setter, which then works as expected.
In the debugger, it can then be seen that the correct "0.01" value is passed to the Increment property:
Expected behavior
Double values specified in XAML should be used as-is, not a different, but close, value.
Screenshots
No response
NuGet package version
WinUI 3 - Windows App SDK 1.4.5: 1.4.240411001
Windows version
Windows 11 (22H2): Build 22621
Additional context
No response
One important thing to point out as an interesting aside. Floating point numbers don't store exact values. This can be logically reasoned as how can you fit an infinite amount of numbers in a finite amount of bits. There is also the base 2 vs. base 10 representation going on. As an example, the value actually stored for 0.01 in a 32 bit IEEE 754 float is 0.00999999977648258209228515625. There tends to be a lot of rounding and other processing that takes place to make floating point numbers more usable. As you may notice, the value in the OP is a rounded version of this. The value stored for double is closer but actually over 0.01. This makes me think that a double to float conversion is going on here.
--Edit-- As a nice little example of showing this at work, consider the following C++ application.
#include <iostream>
#include <iomanip>
int wmain()
{
float f = 0.01f;
double d = 0.01;
std::cout << "Value stored as float: " << std::setprecision(27) << f << "\n";
std::cout << "Value stored as double: " << std::setprecision(58) << d << "\n";
double ftod = f;
std::cout << "Value converted from float: " << std::setprecision(58) << ftod << "\n";
double ftodm = round((f * 100.)) / 100.;
std::cout << "Value rounded from float: " << std::setprecision(58) << ftodm << "\n";
return 0;
}
This C++ application is compiled with strict floating point handling.
The output of this is as follows.
Value stored as float: 0.00999999977648258209228515625
Value stored as double: 0.01000000000000000020816681711721685132943093776702880859375
Value converted from float: 0.00999999977648258209228515625
Value rounded from float: 0.01000000000000000020816681711721685132943093776702880859375
These are showing the precise values stored in the variables. The two important things to note are the values for the floating point number and the value stored by directly assigning the float into a double. Once the double has lost its precision, you need to go out of your way to try to recover it. Also, depending in where the precision loss is, you may not even recover the real value. Anyway, as stated, this shows all of the signs of a double being converted to a float somewhere. Or at the very least, Xaml stores it internally as float.
This looks like it could be a xaml parser could be doing the conversion here.