Double.ToString using formatting throws an exception.
- @LucaP in the “Debug writeline with a specific double throws WRONG_TYPE error”
- @Digilamer in the “Exception in .ToString(“F3”)”
- @SoftcontrolFrederik in the “ToString(“N2”) throws exception”
They encountered it. Maybe someone else. The only suggestion is to convert to Float before output. Not the best solution.
Here is the result of incorrect formatting:
(0.1).ToString("F3"); // Work correct
(0.6-0.5).ToString("F3"); // throw exception
(0.099999999999999978).ToString("F3"); // throw exception
because of
var result = FormatNative(value, formatCh, precision);
in System.Number.Format(object value, bool isInteger, string format, NumberFormatInfo info)
will return a string, firstly, that does not match the value, and secondly, with “non-text” characters.
value = 0.099999999999999978
formatCh = "F"
precision = 3
result = "0.011000000000000000000000000000000000000000000000000 \b$] 0p0�a�0�aD"
further, an exception will occur during formatting.
Different values reproduce exceptions with different specified formatting precision.
Example:
double val = 0.5 - 0.45; // val=0.049999999999999989
Debug.WriteLine(val.ToString()); // 0.5 - correct 0.5
Debug.WriteLine(val.ToString("F1")); // 0.0 - correct for 0.049999999999999989, but for 0.05 we expect 0.1
Debug.WriteLine(val.ToString("F2")); // 0.06 - incorrect
Debug.WriteLine(val.ToString("F3")); // 0.051 - incorrect
Is there a limitation on the use of precision? If so, why isn’t an exception thrown when it is reached? Now you can set the precision from 0 to 99 inclusive, and formatting actually returns no more than 50. Although even Double cannot return such precision, its limit is 17.
Example:
double val = 0.09999999999999998;
Debug.WriteLine(val.ToString()); // 0.1
Debug.WriteLine(val.ToString("N16")); // 0.1000000000000000
Debug.WriteLine(val.ToString("N17")); // 0.09999999999999998
Debug.WriteLine(val.ToString("F16")); // 0.1000000000000000
Debug.WriteLine(val.ToString("F17")); // 0.09999999999999998
Debug.WriteLine(val.ToString("D16")); // 0.1000000000000000
Debug.WriteLine(val.ToString("D17")); // 0.09999999999999998
Debug.WriteLine(val.ToString("G16")); // 0.09999999999999998
Debug.WriteLine(val.ToString("G17")); // 0.01 - WTF
The pre-rounding design in my test improves the situation somewhat, but not completely. Perhaps it could be used in FormatNative code.
In the current situation, I see the only way out is to create a method of preliminary preparation for Double formatting, but even this will not guarantee 100% correctness.
play around in test with usePreRound, precision and values.
Test code:
Program.cs
using System;
using System.Diagnostics;
using System.Globalization;
namespace TinyCLRApplication1
{
internal class Program
{
private static readonly double[] Powers =
{
1,
10, 100, 1_000,
10_000, 100_000, 1_000_000,
10_000_000, 100_000_000, 1_000_000_000,
10_000_000_000, 100_000_000_000, 1_000_000_000_000,
10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000,
10_000_000_000_000_000, 100_000_000_000_000_000, 1_000_000_000_000_000_000,
};
private static double GetPower(uint power) => power < Powers.Length ? Powers[power] : Math.Pow(10.0, power);
static void Main()
{
bool usePreRound = true;
uint precision = 17;
FloatTest("Double 0.1", 100, false, 0.11111111111111111111111111111111111111111111111111);
FloatTest("Double 0.1", precision, usePreRound, 0.1);
FloatTest("Double 0.5-0.45", precision, usePreRound, 0.5 - 0.45);
FloatTest("Double 0.06 - 0.05", precision, usePreRound, 0.06 - 0.05);
FloatTest("Double 0.6-0.5", precision, usePreRound, 0.6 - 0.5);
FloatTest("Double 61.0 - 51.0", precision, usePreRound, 61.0 - 51.0);
FloatTest("Double 0.099999999999999978", precision, usePreRound, 0.099999999999999978);
FloatTest("Double 0.999999999999999876", precision, usePreRound, 0.999999999999999876);
FloatTest("Floats array", precision, usePreRound, 0.0f, 0.1f, 0.6f - 0.5f, 0.100000024f, 0.099999999999999978f);
FloatTest("Floats to Doubles array", precision, usePreRound, (double)0.0f, (double)0.1f, (double)(0.6f - 0.5f), (double)0.100000024f, (double)0.099999999999999978f);
FloatTest("Doubles array", precision, usePreRound, 0.0, 0.1, 0.6 - 0.5, 0.100000024, 0.099999999999999978);
}
private static void FloatTest(string testName, uint precisionLimit, bool usePreRound, params IFormattable[] values)
{
string[] formats = { "G", "D", "F", "N" };
Debug.WriteLine("\"" + testName + "\"");
double value;
for (int valueId = 0; valueId < values.Length; valueId++)
{
Debug.WriteLine("\tValue: " + values[valueId]);
for (int formatId = 0; formatId < formats.Length; formatId++)
{
Debug.WriteLine("\t\tFormat: \"" + formats[formatId] + "\"");
for (uint precision = 0; precision <= precisionLimit; precision++)
{
string format = formats[formatId];
if (format != string.Empty) format += precision;
try
{
Debug.Write("\t\t\t" + format + ": ");
if (usePreRound && values[valueId] is double)
{
double pow = GetPower(precision);
value = (double)values[valueId];
value = Math.Round(value * pow) / pow;
Debug.WriteLine(value.ToString(format, NumberFormatInfo.CurrentInfo));
}
else
{
Debug.WriteLine(values[valueId].ToString(format, NumberFormatInfo.CurrentInfo));
}
}
catch
{
/*ignore*/
}
if (format == string.Empty) break;
}
}
}
Debug.WriteLine("");
}
}
}
I ask the GHI team to pay attention to this bug. It has been going on for a very, very long time. Throughout the release of TinyCLR 2.0. It is the native formatting library Number.FormatNative for Double that is persistently trying to ruin a happy life.
PS: damn it, initially I wanted to write only about exceptions, but it turned out like this… Cry of the soul.