34

I have a double "138630.78380386264" and I want to convert it to a decimal, however when I do so I do it either by casting or by using Convert.ToDecimal() and I lose precision.

What's going on? Both decimal and double can hold this number:

enter image description here

double doub = double.Parse("138630.78380386264");
decimal dec = decimal.Parse("138630.78380386264");
string decs = dec.ToString("F17");
string doubse =DoubleConverter.ToExactString(doub);
string doubs = doub.ToString("F17");

decimal decC = (decimal) doub;
string doudeccs = decC.ToString("F17");
decimal decConv = Convert.ToDecimal(doub);
string doudecs = decConv.ToString("F17");

Also: how can I get the ToString() on double to print out the same result as the debugger shows? e.g. 138630.78380386264?

Grant Birchmeier
  • 17,809
  • 11
  • 63
  • 98
GreyCloud
  • 3,030
  • 5
  • 32
  • 47
  • possible duplicate of [Conversion of a decimal to double number in C# results in a difference](http://stackoverflow.com/questions/1584314/conversion-of-a-decimal-to-double-number-in-c-results-in-a-difference) – Miserable Variable Sep 17 '11 at 09:55
  • 1
    Well Convert.ToDecimal(somedouble) is exactly equal to (decimal)somedouble, so no surprises there. I'm not sure why that cast rounds the last digit down though. – harold Sep 17 '11 at 10:01
  • @harold You were tricked. It does round to nearest. The double value finishes 386264 and the 15 sig fig decimal ends 3863. – David Heffernan Sep 17 '11 at 10:08
  • @David sneaky stuff - so it's really the debug view that is wrong – harold Sep 17 '11 at 10:12
  • @harold I think you were tricked by the yellow highlighting which is in the wrong place. Just could up to 15 and you'll see what I mean. I was tricked too! – David Heffernan Sep 17 '11 at 10:16
  • haha! sorry about the yellow formatting! Thank you to all of the commenters on this question, i understand what is going on now. – GreyCloud Sep 17 '11 at 10:34

3 Answers3

26

138630.78380386264 is not exactly representable to double precision. The closest double precision number (as found here) is 138630.783803862635977566242218017578125, which agrees with your findings.

You ask why the conversion to decimal does not contain more precision. The documentation for Convert.ToDecimal() has the answer:

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. The following example illustrates how the Convert.ToDecimal(Double) method uses rounding to nearest to return a Decimal value with 15 significant digits.

The double value, rounded to nearest at 15 significant figures is 138630.783803863, exactly as you show above.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • so why doesnt the conversion to decimal pick up more of the precision? – GreyCloud Sep 17 '11 at 09:58
  • ok thanks for explaining that. Is there anyway to get the ToString to print out more than 15 sig fig? - I'd like to be able to get back to the original string - that way i could get Double to parse the string and do the conversion without loosing precision – GreyCloud Sep 17 '11 at 10:12
  • `doub.ToString("G17")` will give you the same string that the debugger produces. But I don't know for sure that's what the debugger is doing. Since `double` has limited precision you won't in general be able to parse the string and then get the original string back. If you need to know the original string, remember it. – David Heffernan Sep 17 '11 at 10:14
  • Thanks, I'll try "G17" - problem is the double is being produced via com interop so i cant get a string for it :( – GreyCloud Sep 17 '11 at 10:18
  • 1
    @GreyCloud because Double only *has* a bit more than 15 decimal digits of precision. Everything after that is artifacts of the binary fraction format. The method assumes that you're only interested in the "real" precision, not the accidental one. – Michael Borgwardt Sep 17 '11 at 10:24
  • @Michael - so the debugger should only show 15 decimal digits? i wonder why it doesn't... – GreyCloud Sep 17 '11 at 10:36
6

It is a unfortunate, I think. Near 139,000, a Decimal has far better precision than a Double. But still, because of this issue, we have different Doubles being projected onto the same Decimal. For example

double doub1 = 138630.7838038626;
double doub2 = 138630.7838038628;
Console.WriteLine(doub1 < doub2);                    // true, values differ as doubles
Console.WriteLine((decimal)doub1 < (decimal)doub2);  // false, values projected onto same decimal

In fact there are six different representable Double values between doub1 and doub2 above, so they are not the same.

Here is a somewhat silly work-aronud:

static decimal PreciseConvert(double doub)
{
  // Handle infinities and NaN-s first (throw exception)
  // Otherwise:
  return Decimal.Parse(doub.ToString("R"), NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint);
}

The "R" format string ensures that enough extra figures are included to make the mapping injective (in the domain where Decimal has superior precision).


Note that in some range, a long (Int64) has a precision that is superior to that of Double. So I checked if conversions here are made in the same way (first rounding to 15 significant decimal places). They are not! So:

double doub3 = 1.386307838038626e18;
double doub4 = 1.386307838038628e18;

Console.WriteLine(doub3 < doub4);              // true, values differ as doubles
Console.WriteLine((long)doub3 < (long)doub4);  // true, full precision of double used when converting to long

It seems inconsistent to use a different "rule" when the target is decimal.

Note that, near this value 1.4e18, because of this, (decimal)(long)doub3 produces a more accurate result than just (decimal)doub3.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • The problem is not with numbers near 139,000 is because the precision of the double is of 16 digitis, it does not matter if the digits are on the left or right side of the separator, if you decrease this number to 13.86307838038626 you will have the same problem. Decimal has a precision of 32 digits, that's why you don't have this problem. – Gabriel Vonlanten C. Lopes Sep 05 '13 at 17:52
  • @GabrielVonlantenC.Lopes You are right this problem is not specific to numbers near 139,000. I only considered that number as an example, chosen because that was the number in the original question. In fact all numbers between (very roughly) `1e-12` and `1e+29` have exactly the same issue. Outside this range a `decimal` is either not defined, or does no longer have a higher precision than a `double`. (However, `decimal` has a precision of 28-29 digits.) – Jeppe Stig Nielsen Sep 05 '13 at 18:53
  • `(decimal)(long)doub3` is only more precise if the exponent is large enough. There is a large loss of precision if the number would have, say `e01` instead of `e18`. – Abel Jan 26 '20 at 14:35
  • @Abel, Absolutely, that is the point (edited answer)! Generally, you would expect that going through an intermediate type (here `long`) in the conversion should be _worse_ than converting directly. – Jeppe Stig Nielsen Jan 26 '20 at 17:33
2

The answers here provided you insight in the why of the question, but (even though this is many years after you asked it), here's a method to convert a double to a decimal without rounding to 15 digits. It requires a bit of bit twiddling, though.

Other than casting, which throws an OverflowException if the value doesn't fit, or is NaN, INF or -INF, this method gives false in the success parameter if conversion is not possible. Since decimal does not support negative zero, but double does, I've ignored that and just return zero.

The following code is not entirely mine, but I couldn't find the original post to give credits to, sorry. Possibly it was taken from Jon Skeet's code of ToExactString, and adopted for binary exact conversion.

const decimal DecimalEpsilon = 0.0000000000000000000000000001M;
public static decimal TryToDecimalWithInsignificand(double dbl, out bool succeeded)
{
    if (double.IsPositiveInfinity(dbl))
    {
        succeeded = false;
        return decimal.MaxValue;
    }

    if (double.IsNegativeInfinity(dbl))
    {
        succeeded = false;
        return decimal.MinValue;
    }

    if (double.IsNaN(dbl))
    {
        succeeded = false;
        return 0M;
    }

    if (dbl > (double)decimal.MaxValue)
    {
        succeeded = false;
        return decimal.MaxValue;
    }

    if (dbl < (double)decimal.MinValue)
    {
        succeeded = false;
        return decimal.MinValue;
    }

    if (dbl > 0.0 && dbl <= (double)DecimalEpsilon)
    {
        succeeded = false;
        return 0M;
    }

    if (dbl < 0.0 && dbl >= (double)-DecimalEpsilon)
    {
        succeeded = false;
        return 0M;
    }

    // start conversion
    long bits = BitConverter.DoubleToInt64Bits(dbl);
    long mantissa = bits & 0xFFFFFFFFFFFFFL;        // 52 bits
    long exponent = ((bits >> 52) & 0x7FFL) - 1023L;    // next 11 bits
    bool negative = bits < 0;
    decimal fraction = mantissa / (decimal)0x10000000000000L;
    decimal result;

    if (exponent < 0)
    {
        long div = 1 << (int)-exponent;
        result = (fraction + 1) / div;
    }
    else
    {
        long mul = 1L << (int)exponent;
        result = (fraction + 1) * mul;
    }
    succeeded = true;
    if (negative)
    {
        return -1 * result;
    }
    return result;
}
Christian.K
  • 47,778
  • 10
  • 99
  • 143
Abel
  • 56,041
  • 24
  • 146
  • 247