Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cast float to double without extra digits added?

I came across an interesting problem: when I convert the float value -99.9f to a double variable, that variable's value is -99.9000015258789 Therefore this unit test fails:

float f = -99.9f;
double d = (double)f;
Assert.AreEqual(-99.9, d);

I understand that an extra 32 bits are being added in various places. Yet, the value I'm after, -99.9000000000000, is represented just fine as a double if I assign it directly, as evidenced by this unit test:

double d2 = -99.9;
Assert.AreEqual(-99.9, d2);
Assert.AreEqual(-99.9000000000000, d2);

So my ultimate question is: is it possible to take -99.9f and convert it to a double such that it will truly equal -99.9000000000000?

EDIT

I found a workaround that, for the moment, seems to work well for my particular application (i.e. one where I don't know ahead of time how many digits of precision to round the float to):

float f = -99.9f;
double d = double.Parse(f.ToString("r"));
Assert.AreEqual(-99.9, d);

I'm not sure if this would work for all cases; comments are welcome

EDIT 2 As per Lasse V. Karlsen`s answer below, the solution was to use the MSTest equivalent to the AreEqual method:

Assert.AreEqual(-99.9, d, 0.00001);

Note that if I were to add one more zero to that delta value, the test would fail

like image 572
BCA Avatar asked Jan 08 '23 07:01

BCA


2 Answers

The problem is that 99.9 cannot be represented exactly in either Single or Double (corresponds to the C# keywords float and double).

The problem here is the .9 which has to be written as a sum of fractions of powers of two. Because the underlying representation is bits, we can only for each bit position say that it is on and off, and each bit position on the fraction side means 1/2^N, which means that 0.9 can be represented like this:

1   1   1    1    1      1      1      1       1       1
- + - + - + -- + --- + ---- + ---- + ----- + ----- + ------ + ....
2   4   8   64   128   1024   2048   16384   32768   262144

In the end you will either end up just slightly below 0.9 or slightly above 0.9, regardless of how many bits you have because 0.9 cannot be exactly represented with bits like this, but this is how floating point numbers are stored.

The on-screen representation you see, either by calling .ToString(), or just Console.WriteLine, or "" + f, or even inspecting in the debugger, are all subject to possibly being rounded, which means you will not see the exact number that is being stored, only how the rounding/formatting returned it.

This is why, when comparing floating point values, you should always do so using an "epsilon" comparison.

Your unit-test basically says "As long as the actual value is exactly the same as the expected value, we're OK", but when dealing with floating point values this will rarely happen. In fact, I would say that it would happen so seldom that you would discover it immediately except for one thing, when you use constants as both expected and actual and they're of the same type.

Instead you should write your code to test that the actual value is close enough to the expected value, where "close enough" is something very small compared to the range of possible values, and will likely differ for each case.

You should not expect that Single and Double can represent the exact same values either so casting back and forth may even end up returning different values.

Also be aware that with .NET, floating point calculations behave slightly different in regards to DEBUG and RELEASE builds since the built-in FPU part of the processor can usually operate with higher precision than the storage types, this means that if the compiler/optimizer/jitter ends up using the FPU for a sequence of calculations, the outcome can be slightly different from when the values are temporarily stored in variables generated by the compiler.

So this is how you would write a comparison:

if (Math.Abs(actual - expected) < 0.000001) { ... }

where 0.000001 is "close enough".

In terms of many unit-test frameworks, like NUnit, you can ask the comparison to take this into account like this:

Assert.AreEqual(-99.9, d2, 0.000001);

Also, if you want to learn more about this, this article has a lot of gory detail:

  • What Every Computer Scientist Should Know About Floating-Point Arithmetic
like image 99
Lasse V. Karlsen Avatar answered Jan 20 '23 13:01

Lasse V. Karlsen


The problem you are seeing is because of float resolution.

Floats have low precission (depending on what your needs are) and even if you assign -99.9f the float will not have that value, it will really have -99.9000015258789f as value, because that when you convert it to double you see that value, so the problem is not converting to double, is using a float.

If you know how many decimal places will have your number you can use Math.Round(floatNumber, decimalPlaces) and it will solve nearly every problem, but for some values it will fail also depending on how many decimal places you have.

Cheers.

like image 34
Gusman Avatar answered Jan 20 '23 12:01

Gusman