Which C# double literal is not exactly representable as double?

后端 未结 3 1387
长发绾君心
长发绾君心 2021-01-14 10:18

Wikipedia says:

For example, the decimal number 0.1 is not representable in binary floating-point of any finite precision

Howe

相关标签:
3条回答
  • 2021-01-14 10:45

    I have a small source file which print the exact value stored in a double. Code at the end of the answer, just in case the link goes away. Basically, it fetches the exact bits of the double, and goes from there. It's not pretty or efficent, but it works :)

    string s = DoubleConverter.ToExactString(0.1);
    Console.WriteLine(s);
    

    Output:

    0.1000000000000000055511151231257827021181583404541015625
    

    When you just use 0.1.ToString() the BCL truncates the textual representation for you.

    As for which double values are exactly representable - basically, you'd need to work out what the closest binary representation is, and see whether that is the exact value. Basically it needs to be composed of powers of two (including negative powers of two) within the right range and precision.

    For example, 4.75 can be represented exactly, as it's 22 + 2-1 + 2-2

    Source code:

    using System;
    using System.Globalization;
    
    /// <summary>
    /// A class to allow the conversion of doubles to string representations of
    /// their exact decimal values. The implementation aims for readability over
    /// efficiency.
    /// </summary>
    public class DoubleConverter
    {    
        /// <summary>
        /// Converts the given double to a string representation of its
        /// exact decimal value.
        /// </summary>
        /// <param name="d">The double to convert.</param>
        /// <returns>A string representation of the double's exact decimal value.</return>
        public static string ToExactString (double d)
        {
            if (double.IsPositiveInfinity(d))
                return "+Infinity";
            if (double.IsNegativeInfinity(d))
                return "-Infinity";
            if (double.IsNaN(d))
                return "NaN";
    
            // Translate the double into sign, exponent and mantissa.
            long bits = BitConverter.DoubleToInt64Bits(d);
            // Note that the shift is sign-extended, hence the test against -1 not 1
            bool negative = (bits < 0);
            int exponent = (int) ((bits >> 52) & 0x7ffL);
            long mantissa = bits & 0xfffffffffffffL;
    
            // Subnormal numbers; exponent is effectively one higher,
            // but there's no extra normalisation bit in the mantissa
            if (exponent==0)
            {
                exponent++;
            }
            // Normal numbers; leave exponent as it is but add extra
            // bit to the front of the mantissa
            else
            {
                mantissa = mantissa | (1L<<52);
            }
    
            // Bias the exponent. It's actually biased by 1023, but we're
            // treating the mantissa as m.0 rather than 0.m, so we need
            // to subtract another 52 from it.
            exponent -= 1075;
    
            if (mantissa == 0) 
            {
                return "0";
            }
    
            /* Normalize */
            while((mantissa & 1) == 0) 
            {    /*  i.e., Mantissa is even */
                mantissa >>= 1;
                exponent++;
            }
    
            /// Construct a new decimal expansion with the mantissa
            ArbitraryDecimal ad = new ArbitraryDecimal (mantissa);
    
            // If the exponent is less than 0, we need to repeatedly
            // divide by 2 - which is the equivalent of multiplying
            // by 5 and dividing by 10.
            if (exponent < 0) 
            {
                for (int i=0; i < -exponent; i++)
                    ad.MultiplyBy(5);
                ad.Shift(-exponent);
            } 
            // Otherwise, we need to repeatedly multiply by 2
            else
            {
                for (int i=0; i < exponent; i++)
                    ad.MultiplyBy(2);
            }
    
            // Finally, return the string with an appropriate sign
            if (negative)
                return "-"+ad.ToString();
            else
                return ad.ToString();
        }
    
        /// <summary>Private class used for manipulating
        class ArbitraryDecimal
        {
            /// <summary>Digits in the decimal expansion, one byte per digit
            byte[] digits;
            /// <summary> 
            /// How many digits are *after* the decimal point
            /// </summary>
            int decimalPoint=0;
    
            /// <summary> 
            /// Constructs an arbitrary decimal expansion from the given long.
            /// The long must not be negative.
            /// </summary>
            internal ArbitraryDecimal (long x)
            {
                string tmp = x.ToString(CultureInfo.InvariantCulture);
                digits = new byte[tmp.Length];
                for (int i=0; i < tmp.Length; i++)
                    digits[i] = (byte) (tmp[i]-'0');
                Normalize();
            }
    
            /// <summary>
            /// Multiplies the current expansion by the given amount, which should
            /// only be 2 or 5.
            /// </summary>
            internal void MultiplyBy(int amount)
            {
                byte[] result = new byte[digits.Length+1];
                for (int i=digits.Length-1; i >= 0; i--)
                {
                    int resultDigit = digits[i]*amount+result[i+1];
                    result[i]=(byte)(resultDigit/10);
                    result[i+1]=(byte)(resultDigit%10);
                }
                if (result[0] != 0)
                {
                    digits=result;
                }
                else
                {
                    Array.Copy (result, 1, digits, 0, digits.Length);
                }
                Normalize();
            }
    
            /// <summary>
            /// Shifts the decimal point; a negative value makes
            /// the decimal expansion bigger (as fewer digits come after the
            /// decimal place) and a positive value makes the decimal
            /// expansion smaller.
            /// </summary>
            internal void Shift (int amount)
            {
                decimalPoint += amount;
            }
    
            /// <summary>
            /// Removes leading/trailing zeroes from the expansion.
            /// </summary>
            internal void Normalize()
            {
                int first;
                for (first=0; first < digits.Length; first++)
                    if (digits[first]!=0)
                        break;
                int last;
                for (last=digits.Length-1; last >= 0; last--)
                    if (digits[last]!=0)
                        break;
    
                if (first==0 && last==digits.Length-1)
                    return;
    
                byte[] tmp = new byte[last-first+1];
                for (int i=0; i < tmp.Length; i++)
                    tmp[i]=digits[i+first];
    
                decimalPoint -= digits.Length-(last+1);
                digits=tmp;
            }
    
            /// <summary>
            /// Converts the value to a proper decimal string representation.
            /// </summary>
            public override String ToString()
            {
                char[] digitString = new char[digits.Length];            
                for (int i=0; i < digits.Length; i++)
                    digitString[i] = (char)(digits[i]+'0');
    
                // Simplest case - nothing after the decimal point,
                // and last real digit is non-zero, eg value=35
                if (decimalPoint==0)
                {
                    return new string (digitString);
                }
    
                // Fairly simple case - nothing after the decimal
                // point, but some 0s to add, eg value=350
                if (decimalPoint < 0)
                {
                    return new string (digitString)+
                           new string ('0', -decimalPoint);
                }
    
                // Nothing before the decimal point, eg 0.035
                if (decimalPoint >= digitString.Length)
                {
                    return "0."+
                        new string ('0',(decimalPoint-digitString.Length))+
                        new string (digitString);
                }
    
                // Most complicated case - part of the string comes
                // before the decimal point, part comes after it,
                // eg 3.5
                return new string (digitString, 0, 
                                   digitString.Length-decimalPoint)+
                    "."+
                    new string (digitString,
                                digitString.Length-decimalPoint, 
                                decimalPoint);
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-14 10:52

    The BCL cheats here by giving you a rounded value when you print it. There should be no literal that prints a different representation or accuracy.

    Which is nice, in that it matches intuition most of the time. But so far I haven't found a nice way of getting the exact value to print.

    0 讨论(0)
  • 2021-01-14 10:58

    0.1 is the example. But in C# it is a literal of type double. And Double.ToString() uses format with precision of 15 instead of 17 digits by default.

    Relevant quote from documentation:

    By default, the return value only contains 15 digits of precision although a maximum of 17 digits is maintained internally. [...] If you require more precision, specify format with the "G17" format specification, which always returns 17 digits of precision, or "R", which returns 15 digits if the number can be represented with that precision or 17 digits if the number can only be represented with maximum precision.

    So, 0.1.ToString("G17") equals to "0.10000000000000001" which is the number 0.1000000000000000055511151231257827021181583404541015625 from Jon Skeet answer correctly rounded towards infinity. Note that last 1 in first number is 17th significant digit, and first 5 in second number is 18th significant digit. 0.1.ToString() is basically same as 0.1.ToString("G") which equals to "0.1". Or "0.100000000000000" if you print 15 digits after decimal point. Which is "0.10000000000000001" correctly rounded towards zero.

    Interestingly, Convert.ToDecimal(double) uses only 15 significant digits too.

    Relevant quote from documentation:

    The Decimal value returned by this method contains a maximum of 15 significant digits. If the value parameter contains more than 15 significant digits, it is rounded using rounding to nearest.

    You can use same G17 format and decimal.Parse() to convert 0.1 to decimal: decimal.Parse(0.1.ToString("G17")). This code snippet produces number that is not equal to 0.1m.

    For more information check The General ("G") Format Specifier page on MSDN.

    0 讨论(0)
提交回复
热议问题