2

Consider the following code:

double d = 1478110092.9070129;
decimal dc = 1478110092.9070129M;

Console.WriteLine(d.ToString("R"));
Console.WriteLine(dc);

Console.WriteLine(Convert.ToDouble(dc).ToString("R"));
Console.WriteLine(double.Parse(dc.ToString()).ToString("R"));

This generates the following:

1478110092.9070129
1478110092.9070129
1478110092.9070127
1478110092.9070129

My question is what is going on in Convert.ToDouble? Clearly this number can be represented in a double?

Paul Cassidy
  • 141
  • 1
  • 5
  • 2
    Here we go I think this link should be instersting to read http://csharpindepth.com/Articles/General/FloatingPoint.aspx – mybirthname Nov 04 '16 at 13:51
  • some numbers can't be represented accurately in binary, which is why `decimal` is used when accuracy is required. – user1666620 Nov 04 '16 at 13:52
  • 1
    The double may be an approximation but the double type resolves to a number ending in 129 when we call ToString("R") so why does the ConvertToDouble return 127 from a decimal that ends in 129 ? – Paul Cassidy Nov 04 '16 at 14:02

3 Answers3

5

To see why this happens we need to look in to the internals of Decimal.

In the source for Convert.ToDouble(Decimal) we see it does

public static double ToDouble(decimal value) {
    return (double)value;
}

In the source for Decimal we can see its explicit conversion operator for double

    public static explicit operator double(Decimal value) {
        return ToDouble(value);
    }

And its double ToDouble(Decimal) call

[System.Security.SecuritySafeCritical]  // auto-generated
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public static extern double ToDouble(Decimal d);\

We then need to go to the native implementation of the ToDouble call

FCIMPL1(double, COMDecimal::ToDouble, FC_DECIMAL d)
{
    FCALL_CONTRACT;

    ENSURE_OLEAUT32_LOADED();

    double result = 0.0;
    // Note: this can fail if the input is an invalid decimal, but for compatibility we should return 0
    VarR8FromDec(&d, &result);
    return result;
}
FCIMPLEND

Which leads us to VarR8FromDec which is a windows function inside OleAut32.dll which we don't have the source to.

Likely the issue is OleAut32.dll is not doing the max percision conversion it could be doing.

If you want to convert with max percision you need to first go to string with the R format then parse the string.


If you are curious, I ran VarR8FromDec through a decompiler and here is what it is doing internally

HRESULT __stdcall VarR8FromDec(const DECIMAL *pdecIn, DOUBLE *pdblOut)
{
  BYTE v2; // dl@1
  BYTE v3; // bl@2
  double v4; // st7@4
  HRESULT result; // eax@8

  v2 = pdecIn->scale;
  if ( v2 > 0x1Cu || (v3 = pdecIn->sign, v3 & 0x7F) )
  {
    result = -2147024809;
  }
  else
  {
    if ( (pdecIn->Mid32 & 0x80000000u) == 0 )
      v4 = ((double)pdecIn->Hi32 * 1.844674407370955e19 + (double)*(signed __int64 *)&pdecIn->Lo32) / sub_1006AC0D(v2);
    else
      v4 = ((double)*(signed __int64 *)&pdecIn->Lo32 + 1.844674407370955e19 + (double)pdecIn->Hi32
                                                                            * 1.844674407370955e19)
         / sub_1006AC0D(v2);
    if ( v3 )
      v4 = -v4;
    *(_QWORD *)pdblOut = *(_QWORD *)&v4;
    result = 0;
  }
  return result;
}

double __fastcall sub_1006AC0D(unsigned int a1)
{
  double result; // st7@2

  if ( a1 > 0x50 )
    result = sub_1006ABD4((void *)a1, 10.0);
  else
    result = dbl_1002EF08[a1];
  return result;
}

double __thiscall sub_1006ABD4(void *this, double a2)
{
  unsigned int v2; // eax@1
  double v3; // st6@3
  double i; // st5@3
  double result; // st7@9

  v2 = (unsigned int)this;
  if ( (signed int)this < 0 )
    v2 = -(signed int)this;
  v3 = 1.0;
  for ( i = a2; ; i = i * i )
  {
    if ( v2 & 1 )
      v3 = v3 * i;
    v2 >>= 1;
    if ( !v2 )
      break;
  }
  if ( (signed int)this >= 0 )
    result = v3;
  else
    result = 1.0 / v3;
  return result;
}

.text:1002EF08 ; double dbl_1002EF08[]
.text:1002EF08 dbl_1002EF08    dq 1.0                  ; DATA XREF: VarDecFromR4:loc_1002F23Ar
.text:1002EF08                                         ; VarDecFromR8:loc_1002F4BAr ...
.text:1002EF10                 dd 0
.text:1002EF14                 dd 40240000h, 0
.text:1002EF1C                 dd 40590000h, 0
.text:1002EF24                 dd 408F4000h, 0
.text:1002EF2C                 dd 40C38800h, 0
.text:1002EF34                 dd 40F86A00h, 0
.text:1002EF3C                 dd 412E8480h, 0
.text:1002EF44                 dd 416312D0h, 0
.text:1002EF4C                 dd 4197D784h, 0
.text:1002EF54                 dd 41CDCD65h, 20000000h, 4202A05Fh, 0E8000000h, 42374876h
.text:1002EF54                 dd 0A2000000h, 426D1A94h, 0E5400000h, 42A2309Ch, 1E900000h
.text:1002EF54                 dd 42D6BCC4h, 26340000h, 430C6BF5h, 37E08000h, 4341C379h
.text:1002EF54                 dd 85D8A000h, 43763457h, 674EC800h, 43ABC16Dh, 60913D00h
.text:1002EF54                 dd 43E158E4h, 78B58C40h, 4415AF1Dh, 0D6E2EF50h, 444B1AE4h
.text:1002EF54                 dd 64DD592h, 4480F0CFh, 0C7E14AF6h, 44B52D02h, 79D99DB4h
.text:1002EF54                 dd 44EA7843h, 2C280291h, 45208B2Ah, 0B7320335h, 4554ADF4h
.text:1002EF54                 dd 0E4FE8402h, 4589D971h, 2F1F1281h, 45C027E7h, 0FAE6D721h
.text:1002EF54                 dd 45F431E0h, 39A08CEAh, 46293E59h, 8808B024h, 465F8DEFh
.text:1002EF54                 dd 0B5056E17h, 4693B8B5h, 2246C99Ch, 46C8A6E3h, 0EAD87C03h
.text:1002EF54                 dd 46FED09Bh, 72C74D82h, 47334261h, 0CF7920E3h, 476812F9h
.text:1002EF54                 dd 4357691Bh, 479E17B8h, 2A16A1B1h, 47D2CED3h, 0F49C4A1Dh
.text:1002EF54                 dd 48078287h, 0F1C35CA5h, 483D6329h, 371A19E7h, 48725DFAh
.text:1002EF54                 dd 0C4E0A061h, 48A6F578h, 0F618C879h, 48DCB2D6h, 59CF7D4Ch
.text:1002EF54                 dd 4911EFC6h, 0F0435C9Eh, 49466BB7h, 0EC5433C6h, 497C06A5h
.text:1002EF54                 dd 0B3B4A05Ch, 49B18427h, 0A0A1C873h, 49E5E531h, 8CA3A8Fh
.text:1002EF54                 dd 4A1B5E7Eh, 0C57E649Ah, 4A511B0Eh, 76DDFDC0h, 4A8561D2h
.text:1002EF54                 dd 14957D30h, 4ABABA47h, 6CDD6E3Eh, 4AF0B46Ch, 8814C9CEh
.text:1002EF54                 dd 4B24E187h, 6A19FC41h, 4B5A19E9h, 0E2503DA9h, 4B905031h
.text:1002EF54                 dd 5AE44D13h, 4BC4643Eh, 0F19D6057h, 4BF97D4Dh, 6E04B86Dh
.text:1002EF54                 dd 4C2FDCA1h, 0E4C2F344h, 4C63E9E4h, 1DF3B015h, 4C98E45Eh
.text:1002EF54                 dd 0A5709C1Bh, 4CCF1D75h, 87666191h, 4D037269h, 0E93FF9F5h
.text:1002EF54                 dd 4D384F03h, 0E38FF872h, 4D6E62C4h, 0E39FB47h, 4DA2FDBBh
.text:1002EF54                 dd 0D1C87A19h, 4DD7BD29h, 463A989Fh, 4E0DAC74h, 0ABE49F64h
.text:1002EF54                 dd 4E428BC8h, 0D6DDC73Dh, 4E772EBAh, 8C95390Ch, 4EACFA69h
.text:1002EF54                 dd 0F7DD43A7h, 4EE21C81h, 75D49491h, 4F16A3A2h, 1349B9B5h
.text:1002EF54                 dd 4F4C4C8Bh, 0EC0E1411h, 4F81AFD6h
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
2

This I believe is a bug in Convert.ToDouble(decimal d). The C# spec says that conversion should give the closest double, but here it clearly doesn't. Looking at the bits, we can see that it is off by one double.

double d = 1478110092.9070129;
decimal dc = 1478110092.9070129M;
double dcd = Convert.ToDouble(dc);
long d_bits = BitConverter.DoubleToInt64Bits(d);     // 4743986451068882048
long dcd_bits = BitConverter.DoubleToInt64Bits(dcd); // 4743986451068882047

See also this possible duplicate:

Conversion of a decimal to double number in C# results in a difference

Community
  • 1
  • 1
Anders Forsgren
  • 10,827
  • 4
  • 40
  • 77
0

Double is only accurate to 16 decimal places and so repeated calculations quickly accumulate rounding errors. Decimal is something like 28 decimal places of accuracy. Double can handle larger numbers but unless you're dealing with large scientific calculations and understand what any loss of precision might mean for you, always use decimal, especially for monetary calculations.

There is some good discussion on this SO question.

EDIT

Please see the comments below - I misread the question and another user (user1666620) has supplied a legit answer in the comments which relates to how numbers are actually stored. In the case of Double that is a binary representation which cannot represent fractions correctly.

Community
  • 1
  • 1
Steve Pettifer
  • 1,975
  • 1
  • 19
  • 34
  • This doesn't really answer the question since in this case the real problem is that doubles are stored in binary, and some numbers do not have an accurate representation in binary. – user1666620 Nov 04 '16 at 13:53
  • 1
    Fair point, I didn't read the question thoroughly enough. I blame the pub lunch. – Steve Pettifer Nov 04 '16 at 13:58