More Floating Point Madness

Ever the one to fight floating point errors, I thought I'd share my latest troubles.  The context is rendering axis labels for a chart, specifically from 67 to 68, in steps of 0.2.  As you can easily deduce, the desired list has six labels: 67.0, 67.2, 67.4, 67.6, 67.8, and 68.0.  However, ColdFusion might disagree with you, depending on how you ask.  Consider this code (a distillation of my axis labeling code):



  #i#

What's the output?  The logic is sound, but if you run it you'll see that you're missing the "68″ at the end.  And before you think it's something to do with the struct of values, consider this even simpler version:


  #i#

Identical behaviour.

In this particular case, you can circumvent the issue by rewriting the loop like this:


  #bounds.lower + i * bounds.interval#

You could even argue that this is more readable, and while I don't necessarily agree, I don't disagree either.  The reason writing it this way works is because it's no longer a floating point comparison for terminating the loop, it's an integer comparison.  The floating point numbers are only used within the output expression.  As you'd expect, refactoring like this to avoid comparisons on floating point numbers isn't always possible.

In such situations another approach is needed: discarding precision.  Ideally, we want to only discard the precision that is erroneous, but if we could do that, then the computer could do it automatically and we wouldn't have the problem.  As such, we have to punt.  Here's a simple UDF that will fix a floating point number by discarding everything beyond the 12th place:


  
  
  
    
  
  
  

The gist of the function is to shift the number until it has a single non-decimal digit, shift 12 places to the left (so it's in trillions), round it, and then undo the shifting.  The two levels of shifting are combined in the 'factor' computation – the "int(log10(abs(num)))" is the first part, and the "- 12″ is the second part.

As I said above, there isn't a way to assure that this is a foolproof operation, since you are throwing away information.  That said, I've never seen floating point error persist through a call to the function, and I've never been in a place where that 13th place of precision was needed, so I've been quite happy with it.  Just be careful how you use it, because you need to put it in the right spot. Bene pigiausios detales automobiliams BMW, Audi, VW, Volvo, Ford, Honda, Renault internetu: pigiausiosdalys.lt

Consider this code (which is a for-style – rather than foreach-style – version of the original loop):



  #i#
  i += fixFloatingPoint(bounds.interval) />

No "68″, because the error still manifests itself within the 'i' variable for the final LTE check against 68.  You need to write it like this:



  #i#
  i = fixFloatingPoint(i + bounds.interval) />

Now the 'i' variable will be clean for that last comparison, and you'll get the "68″ emitted as desired.

I've only run these examples on ColdFusion 8.0.1, but I'd expect the same behaviour out of other versions of ColdFusion, as well as Railo and OBD.  They all use java.lang.Double for non-integral computations (rather than java.math.BigDecimal), and so will be vulnerable to floating point errors.

9 responses to “More Floating Point Madness”

  1. Todd Rafferty
  2. Todd Rafferty

    Railo results:
    67
    67.2
    67.4
    67.6
    67.8

    67
    67.2
    67.4
    67.6
    67.8

    – At any rate, it should be there. Micha can reject it with a reason and people can do a search and read his reason.

  3. Michael Offner

    this problem is the nature of java double values, because java is not a mathematical language.
    for more details check out this blog
    http://epramono.blogspot.com/2005/01/double-vs-bigdecimal.html
    or google
    double java "not exactly"

  4. Andrew

    I brought this up a while ago on the Railo list. (I found that same problem when using Val() and floating point equality). Sean replied to me with the below:

    "Not exactly a helpful observation but I would point out that it is
    never really safe to use equality tests on floating point numbers…

    I guess I would have to ask *why* you are trying to test for floating
    point equality rather than just a suitable proximity?

    Have a play with this to see why comparing floating point numbers can
    be problematic:

    http://babbage.cs.qc.edu/IEEE-754/Decimal.html

    If you enter 0.1, you'll see the 32-bit representation is different
    for rounded vs non-rounded but if you enter 0.01 you'll see it is the
    same for rounded vs non-rounded (and neither are equal to 0.01 within
    the precision allowed by 32-bits). As you can imagine, lots of factors
    come into play when manipulating floating point numbers – how constant
    expressions are reduced, how optimization is performed on
    subexpressions…"

    In my case I was able to change my code to test for suitable proximity.

    (I also checked the Railo source and it doesn't use BigDecimal for floating point values)

  5. Matt Woodward

    Just confirming OpenBD behaves the same way.

    This may not be generally applicable to all situations, but in this particular case you could multiply the loop bounds by 10, use a step of 2 instead of .2, and multiply by .1 within the loop to get the values you want, which behaves correctly. Just another idea.

  6. Ben Nadel

    I've also seen this error pop up when dealing with numeric date/times. So frustrating.