Wikipedia says:
For example, the decimal number 0.1 is not representable in binary floating-point of any finite precision
Howe
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);
}
}
}