6

Can some more experienced guys explain this strange error I found today? I was getting strange amounts when I was loading table data with my C# script.

As it turns out the problem is a different output from similar functions:

string amount_in_string = "1234567,15";
double amount_in_double = double.Parse(amount_in_string);
float amount_in_float = float.Parse(amount_in_string);

//amount_in_double = 1234567.15;
//amount_in_float = 1234567.13;

Why do I get such a different result when float and double are similar types (floating point). Can the precision make a difference with small amounts like these?

Boann
  • 48,794
  • 16
  • 117
  • 146
Ivan Bosmansky
  • 130
  • 2
  • 7
  • 6
    You should be using `decimal` for this, not `double` or `float`. – Ron Beyer Aug 23 '18 at 14:55
  • 2
    @Fildor @ron _"amount"_ is just a _quantity_ not necessarily a _currency_. OP hasn't indicated currencies. Also, be nice, no need to use `!` –  Aug 23 '18 at 14:56
  • @Fildor you should provide an explanation as to why he shouldn't floating point types – Bruno Aug 23 '18 at 15:02
  • @MickyD You are correct, "amount" could also be non-monetary. Comment retracted. Sidenote: I am not that sensitive about "!". Where I come from they (when used in singular "!", not "!!!!!!") do not cause offense. – Fildor Aug 23 '18 at 15:07
  • 2
    [JonSkeet has a very good explanation on this subject](https://stackoverflow.com/a/618596/9453080), basically `double` and `float` are binary point types (for example, a `double` or a `float` can never be equal to 0.1 as is) and **`decimal`** is a decimal (base 10) point type. If you're looking for point accuracy, you should be using `decimal` instead! – Ivan García Topete Aug 23 '18 at 15:16
  • Float has only 7 digit precision. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/float – skyoxZ Aug 23 '18 at 15:20
  • But how does the precision affect parsing single value to float or double? In the topic that @IvanGarcíaTopete shared there is an example - but it is showing the difference in division. I get the difference between the types in mathematical operations like division. But in my scenario there is no mathematical division. Why is there a difference even if you don't divide anything? – Ivan Bosmansky Aug 23 '18 at 17:36

3 Answers3

7

When “1234567.15” is converted to double, the result is the closest value representable in double, which is 1234567.1499999999068677425384521484375. Although you report in the question that the value is 1234567.15, the actual value is 1234567.1499999999068677425384521484375. “1234567.15” would be displayed when the value is displayed with a limited number of decimal digits.

When “1234567.15” is converted to float, the result is the closet value representable in float, which is 1234567.125. Although you report the value is 1234567.13, the actual value is 1234567.125. “1234567.13” may be displayed when the value is displayed with a limited number of decimal digits.


Observe that 1234567 exceeds 1,048,576, which is 220. The 32-bit floating-point format used for float uses 24 bits for the significand (fraction portion of the number). If the high bit of the significand represents 220, the low bit represents 220−23 = 2−3 = ⅛. This is why you see “1234567.15” converted to a value rounded to the nearest eighth.

ChickenFeet
  • 2,653
  • 22
  • 26
Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • This is interesting! Nice example, I think I finally get it, thanks. So when parsing float my "1234567.15" to float it gets "1234567" + nearest 1/8 so its "1234567.125". And why did it cut to 2 decimals? I didn't specify that..:) – Ivan Bosmansky Aug 23 '18 at 17:50
  • 1
    @IvanBosmansky: You did not show us how you got “1234567.13”; it merely appears in your question as a comment. So we do not know if you displayed it with some formatter, if you set some format/conversion specifications, or what. You would have to show complete code for that to be explained. – Eric Postpischil Aug 23 '18 at 17:52
  • 2
    @MickyD: For `float`, round to the nearest ⅛, as stated. For `double`, round to the nearest 2^(20-52) = 2^-32. The easiest way to do this rounding is to convert the number with `double.Parse` and then print it with 32 or more decimal digits after the decimal point. (I do not know whether C# will print so many digits accurately. Microsoft has not always provided good conversion routines. I used Apple’s developer tools for this, which do convert accurately. I used C: `printf(%.99g\n", 1234567.15);`.) If one wanted to look at the math in more detail, extended-precision software could help. – Eric Postpischil Aug 23 '18 at 17:56
  • Yeah I tried experimenting with the numbers as well as printing them out in different formats and this is definitely correct - it behaves like you describe. Many thanks to you for great explanation. Sorry guys for not getting it sooner, I just needed to see the numbers I guess – Ivan Bosmansky Aug 23 '18 at 18:02
  • Just a fancy way of saying "loss of precision" hahah. +1 – Piotr Kula Aug 23 '18 at 21:12
  • Thanks Eric! Brilliant answer. I had no idea this is how things work :) I learnt something new today. +1 –  Aug 24 '18 at 00:07
1

Floating point numbers are never exact, they are representations of numbers. An example commonly used is think of

1/3 + 1/3 = 2/3

...so the answer in floating point numbers, .33333 + .33333, is not 2/3rds exactly, it is .66666.

Long story short, the more precise fraction you take that can't be converted to an exact binary is going to always have a rounding number. The more precise the more likely it will have rounding errors.

Keep in mind if you do multiple different fractions, you can even have multiple different rounding errors that either make the number accidentally correct, or even further off.

obizues
  • 1,473
  • 5
  • 16
  • 30
  • A double is 64 bit, a floating is 32 bit. More precision means different rounding. – obizues Aug 23 '18 at 15:00
  • 2
    Not sure OP's question is about rational numbers though –  Aug 23 '18 at 15:04
  • I'm not sure why I'm being downvoted here. @MickyD that may be, but he's parsing a string to a double and a float based on the same string. The point is there is a least significant bit in any of these float-based number types that will cause rounding errors. I have a feeling if Op multiplied their result by a couple of factors of 10 then their answer would be equal. – obizues Aug 23 '18 at 15:44
  • Here's Microsoft's article explaining about how IEEE floating-point format works with rounding: https://learn.microsoft.com/en-us/cpp/build/reference/why-floating-point-numbers-may-lose-precision – obizues Aug 23 '18 at 15:45
  • 1
    @obizues Not the downvoter, but the problem is with the `,` (culture), not exactly with the types used. Besides, multiplication can't be an answer: what will happen in the case of `string ammount_in_string = 34028229999999999999999999999999999999.15`? Multiplying by 10 will become an `OverflowException` – Ivan García Topete Aug 23 '18 at 16:58
  • Ok I get that the difference between double and float and I will use decimal from now on to avoid such problems. However what I still don't get is - why is any rounding involved? it should just parse a single value - there is no division, multiplication or any other mathematical operation going on... – Ivan Bosmansky Aug 23 '18 at 17:21
-2

A fiddle you can see the results (there is a culture problem here too)

https://dotnetfiddle.net/Lnv1q7

    string amount_in_string = "1234567,15"; // NOTE THE COMMA in original
    string amount_in_string = "1234567.15"; //my correct culture
    double amount_in_double = double.Parse(amount_in_string);
    float amount_in_float = float.Parse(amount_in_string);

    Console.WriteLine(amount_in_string);
    Console.WriteLine(amount_in_double);
    Console.WriteLine(amount_in_float);

Results (parsing in the incorrect culture!)

1234567,15
123456715
1.234567E+08

Results (parsing in the correct culture!)

1234567.15
1234567.15
1234567

Another one that demonstrates the loss of precision with float

float flt = 1F/3;
double dbl = 1D/3;
decimal dcm = 1M/3;
Console.WriteLine("float: {0} double: {1} decimal: {2}", flt, dbl, dcm);

Result

float: 0.3333333  
double: 0.333333333333333  
decimal: 0.3333333333333333333333333333

floats should only be used in cases were some loss of precision is not extremely valuable. This is because floats are 32bit where decimal is 128 bit floats are mainly used in pixel coordinates, and loss of precision doesn't matter because a the consumer translates locations to more precise coordinates any way.

in .NET floats "should" be actively avoided unless you don't care about loss of (some) precision. Which is probably never now a days (unless you writting games)

This is where the banking problem came from when 100th's of a penny/cent across transactions where lost, seemingly invisible per transaction but amounting to large amounts of "missing" money.

Use decimal

Piotr Kula
  • 9,597
  • 8
  • 59
  • 85
  • Why the downvotes? I demonstrated loss of precision twice by using standard, simple c# code. Plus an example here used from another SO answer with 1k upvotes. I hate drive by downvotes.. nobody learns anything. code of conduct should force comments on down votes – Piotr Kula Aug 23 '18 at 15:08
  • "... should force comments on down votes" - I think there has been a discussion on meta about this. There are pros and cons. Seemingly the Cons are greater. – Fildor Aug 23 '18 at 15:16
  • dv might be because the problem is not the number of bits but the fact that `float` is binary representation of a decimal number and `decimal` is not. – Crowcoder Aug 23 '18 at 15:16
  • Yea I mentioned floats are used with pixel coordinates (most common) usage. Yea.. I thought I read he was loading a table of monetary values. – Piotr Kula Aug 23 '18 at 15:20
  • 2
    When you empathize **in .NET floats should never be used** (and leave "unless you don't care about loss of (some) precision." out) my mind only reads the bold part. Maybe you can change it to just **Using floats in .NET may (or will, depending on the accuracy required) cause loss of precision.** – Ivan García Topete Aug 23 '18 at 15:23
  • 3
    hm, mind that you never got "1234567.13" as OP is claiming to have. – Fildor Aug 23 '18 at 15:27
  • @IvanGarcíaTopete Perhaps. `float`s will definately cause loss of precesion whether you want to or not and it doesn't require anything complex either –  Aug 23 '18 at 15:27
  • Yea I also never go the `.13` but that can be a culture setting problem.. check your culture settings – Piotr Kula Aug 23 '18 at 15:28
  • Also if OP is getting the data with the `,` as decimal separator, maybe you can include a code snippet taking care of that (like `decimal.Parse(ammount_in_string.Replace(",", "."))` or something regarding [Decimal.Parse (String, NumberStyles, IFormatProvider)](https://learn.microsoft.com/en-us/dotnet/api/system.decimal.parse?view=netframework-4.7.2)?) – Ivan García Topete Aug 23 '18 at 15:30
  • 1
    How can 1234567.13 be caused by a culture problem? If the locale is mismatched, the parser ought to generate an exception or take the comma as a thousands separator. Neither of these would produce 1234567.13, nor would accepting and converting the input up to the invalid character. Accepting and converting the entire string to 32-bit floating-point, with the comma or period taken as the decimal point, would produce 1234567.125, which could be displayed as 1234567.13. – Eric Postpischil Aug 23 '18 at 17:22
  • Thanks for this, I found it informative. However I really was getting the .13, and I don't think that the culture explains why the .15 was changed to .13. It would explain any problems with comma and period separators etc, but changing the value like this? – Ivan Bosmansky Aug 23 '18 at 17:24
  • 2
    The code at the link you posted in the answer produces 1234567.125 for `float.Parse`. It is merely displayed as “1234567” because apparently the default formatting limits the digits shown. Printing more digits would show “1234567.13”. So OP’s result is **not** due to a locale/culture mismatch but is due to floating-point precision. – Eric Postpischil Aug 23 '18 at 17:25
  • OK makes sense. Yes. the culture bit relates to the formatting output rather than the value it self.. although.. on some processors the rounding may different due to Endianness, soft or hard float and their implementations. – Piotr Kula Aug 23 '18 at 21:10
  • @ppumkin: IEEE 754 rounding is defined mathematically. Endianness plays no part in it. And IEEE 754 rounding is the same regardless of whether it is implemented in hardware or software. – Eric Postpischil Aug 24 '18 at 09:32