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

后端 未结 3 1386
长发绾君心
长发绾君心 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;
    
    /// 
    /// A class to allow the conversion of doubles to string representations of
    /// their exact decimal values. The implementation aims for readability over
    /// efficiency.
    /// 
    public class DoubleConverter
    {    
        /// 
        /// Converts the given double to a string representation of its
        /// exact decimal value.
        /// 
        /// The double to convert.
        /// A string representation of the double's exact decimal value.
        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();
        }
    
        /// Private class used for manipulating
        class ArbitraryDecimal
        {
            /// Digits in the decimal expansion, one byte per digit
            byte[] digits;
            ///  
            /// How many digits are *after* the decimal point
            /// 
            int decimalPoint=0;
    
            ///  
            /// Constructs an arbitrary decimal expansion from the given long.
            /// The long must not be negative.
            /// 
            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();
            }
    
            /// 
            /// Multiplies the current expansion by the given amount, which should
            /// only be 2 or 5.
            /// 
            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();
            }
    
            /// 
            /// 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.
            /// 
            internal void Shift (int amount)
            {
                decimalPoint += amount;
            }
    
            /// 
            /// Removes leading/trailing zeroes from the expansion.
            /// 
            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;
            }
    
            /// 
            /// Converts the value to a proper decimal string representation.
            /// 
            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);
            }
        }
    }
    

提交回复
热议问题