views:

86

answers:

3

Hi,

So I'm just going to dive into this issue... I've got a heavily used web application that, for the first time in 2 years, failed doing an equality check on two doubles using the equality function a colleague said he'd also been using for years.

The goal of the function I'm about to paste in here is to compare two double values to 4 digits of precision and return the comparison results. For the sake of illustration, my values are:

Dim double1 As Double = 0.14625000000000002 ' The result of a calculation
Dim double2 As Double = 0.14625 ' A value that was looked up in a DB

If I pass them into this function:

Public Shared Function AreEqual(ByVal double1 As Double, ByVal double2 As Double) As Boolean

    Return (CType(double1 * 10000, Long) = CType(double2 * 10000, Long))

End Function

the comparison fails. After the multiplication and cast to Long, the comparison ends up being:

Return 1463 = 1462

I'm kind of answering my own question here, but I can see that double1 is within the precision of a double (17 digits) and the cast is working correctly.

My first real question is: If I change the line above to the following, why does it work correctly (returns True)?

Return (CType(CType(double1, Decimal) * 10000, Long) = _
    CType(CType(double2, Decimal) * 10000, Long))

Doesn't Decimal have even more precision, thus the cast to Long should still be 1463, and the comparison return False? I think I'm having a brain fart on this stuff...

Secondly, if one were to change this function to make the comparison I'm looking for more accurate or less error prone, would you recommend changing it to something much simpler? For example:

Return (Math.Abs(double1 - double2) < 0.0001)

Would I be crazy to try something like:

Return (double1.ToString("N5").Equals(double2.ToString("N5")))

(I would never do the above, I'm just curious about your reactions. It would be horribly inefficient in my application.)

Anyway, if someone could shed some light on the difference I'm seeing between casting Doubles and Decimals to Long, that would be great.

Thanks!

+4  A: 

What Every Computer Scientist Should Know About Floating-Point Arithmetic

Giorgi
I'm not certain the problem is a misunderstanding of floating point arithmetic. The problem is using a cast to try and fix floating point arithmetic.
Jason Berkan
@Jason Berkan - the usage of the cast demonstrates the misunderstanding.
Joel Etherton
Well like I said, the code is a colleague's and he believed it to be adequate -- I was tasked with doing the debugging. The `Math.Abs()` solution I threw in the OP was my suggestion as a fix, I just wanted to understand more about the type casting issue so I can avoid it in the future. The examples in my post were simply the result of my curiosity of casting behavior, something for which I apparently need to do more research.
Cory Larson
I didn't really ask for the inner-workings of floating point arithmetic; I get the gist of it. What I didn't know, and what Jason Berken found is that when casting a `double` to a `Decimal`, the `double` is rounded off to 15 significant digits.
Cory Larson
+2  A: 

When you use CType, you're telling your program "I don't care how you round the numbers; just make sure the result is this other type". That's not exactly what you want to say to your program when comparing numbers.

Comparing floating-point numbers is a pain and I wouldn't ever trust a Round function in any language unless you know exactly how it behaves (e.g. sometimes it rounds .5 up and sometimes down, depending on the previous number...it's a mess).

In .NET, I might actually use Math.Truncate() after multiplying out my double value. So, Math.Truncate(.14625 * 10000) (which is Math.Truncate(1462.5)) is going to equal 1462 because it gets rid of all decimal values. Using Truncate() with the data from your example, both values would end up being equal because 1) they remain doubles and 2) you made sure the decimal was removed from each.

I actually don't think String comparison is very bad in this situation since floating point comparison is pretty nasty in itself. Granted, if you're comparing numbers, it's probably better to stick with numeric types, but using string comparison is another option.

Ben McCormack
I disagree, String comparison is overly complex. The specification for the function is that `AreEqual` should be `True` if the difference between the numbers is less than `0.0001`. In other words, `AreEqual = (Math.Abs(double1 - double2) < 0.0001)`
MarkJ
@Mark I'm not suggesting you *should* use String comparison, but I don't think String comparison is as bad as using numeric conversions to do the rounding for you. However, the method you specify seems to be much better than either string comparison or implicit-rounding-by-conversion, so I would probably go with what you specified.
Ben McCormack
+3  A: 

Relying on a cast in this situation is error prone, as you have discovered - depending upon the rules used when casting, you may not get the number you expect.

I would strongly advise you to write the comparison code without a cast. Your Math.Abs line is perfectly fine.

Regarding your first question:

My first real question is: If I change the line above to the following, why does it work correctly (returns True)?

The reason is that the cast from Double to Decimal is losing precision, resulting in a comparison of 0.1425 to 0.1425.

Jason Berkan
"...the cast from Double to Decimal is losing precision..." That makes my brain hurt. Why does the precision get lost? Is it because a Decimal is a different base than Double; i.e. is it because the numbers are stored differently?
Cory Larson
Never mind. I'm typing without thinking. I meant the cast was narrowing (i.e. losing information), which it is, but I'm at a loss as to why. According to the Decimal documentation, casting from Double to Decimal does not lose anything, but the debugger is showing the tiny portion disappearing: i.e. Dim test As New Decimal(0.14625000000000002) results in test containing 0.14625. However, Dim test As Decimal = 0.14625000000000002D works. I'll go read the linked article about floating point arithmetic and stop answering at this point...
Jason Berkan
And some more searching through the documentation shows: "This constructor rounds value to 15 significant digits using rounding to nearest. This is done even if the number has more than 15 digits and the less significant digits are zero."
Jason Berkan
@Jason: that's exactly what I needed to know, thanks for doing the research :)
Cory Larson
+1 And the string comparison idea is crazy. The specification for the function is that `AreEqual` should be `True` if the difference between the numbers is less than `0.0001`. In other words, `AreEqual = (Math.Abs(double1 - double2) < 0.0001)` There's no need to add complexity.
MarkJ