Incorrect Double.ToString formatting up to an thrown Exceptions

Double.ToString using formatting throws an 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.

2 Likes

Nice explenation. I think there is already a bug reported for this in the TinyCLR repo:

https://github.com/ghi-electronics/TinyCLR-Libraries/issues/629

Hope they fix it in the next release.

1 Like

Another related thread:

https://forums.ghielectronics.com/t/exception-in-tostring-f3/24140