2

I came to know about the accuracy issues when I executed the following following program:

public static void main(String args[])
{
    double table[][] = new double[5][4];
    int i, j;
    for(i = 0, j = 0; i <= 90; i+= 15)
    {
        if(i == 15 || i == 75)
            continue;
        table[j][0] = i;
        double theta = StrictMath.toRadians((double)i);
        table[j][1] = StrictMath.sin(theta);
        table[j][2] = StrictMath.cos(theta);
        table[j++][3] = StrictMath.tan(theta);
    }
    System.out.println("angle#sin#cos#tan");
    for(i = 0; i < table.length; i++){
        for(j = 0; j < table[i].length; j++)
            System.out.print(table[i][j] + "\t");
        System.out.println();
    }
}

And the output is:

angle#sin#cos#tan
0.0 0.0 1.0 0.0 
30.0    0.49999999999999994 0.8660254037844387  0.5773502691896257  
45.0    0.7071067811865475  0.7071067811865476  0.9999999999999999  
60.0    0.8660254037844386  0.5000000000000001  1.7320508075688767  
90.0    1.0 6.123233995736766E-17   1.633123935319537E16    

(Please forgive the unorganised output). I've noted several things:

  • sin 30 i.e. 0.5 is stored as 0.49999999999999994.
  • tan 45 i.e. 1.0 is stored as 0.9999999999999999.
  • tan 90 i.e. infinity or undefined is stored as 1.633123935319537E16 (which is a very big number).

Naturally, I was quite confused to see the output (even after deciphering the output).

So I've read this post, and the best answer tells me:

These accuracy problems are due to the internal representation of floating > point numbers and there's not much you can do to avoid it.

By the way, printing these values at run-time often still leads to the correct results, at >least using modern C++ compilers. For most operations, this isn't much of an issue.

answered Oct 7 '08 at 7:42

Konrad Rudolph

So, my question is:

Is there any way to prevent such inaccurate results (in Java)?

Should I round-off the results? In that case, how would I store infinity i.e. Double.POSITIVE_INFINITY?

5
  • 1
    theta isn't exactly half-pi, which is why there is no Infinity in the output - NaN/InF is not "stored as" 1.633123935319537E16, which is still a finite number. Commented Mar 5, 2014 at 5:18
  • 1
    See tan(PI/2) for why you don't get Infinity. However, I don't know of a way to get tan to "accept values close to PI/2" as PI/2, and all the environments seem to produce a value. Commented Mar 5, 2014 at 5:22
  • @user2864740 Thanks, that tells me why... But can u tell me how to fix it? Commented Mar 5, 2014 at 5:24
  • 1
    The only immediate solution I can think of is to special-case the input before calling tan. If it's "close enough to PI/2" for your liking, simply return Inf/-Inf (or whatever you are expecting) instead of calling tan. If you need to do "trigonometrical correct" math, there may be a library that can can deal with the exact mathematical concepts. (I think "symbolic math" may be a useful search term). Commented Mar 5, 2014 at 5:29
  • @user2864740 thanks, i'll see to it. Commented Mar 5, 2014 at 5:29

3 Answers 3

4

You have to take a bit of a zen* approach to floating-point numbers: rather than eliminating the error, learn to live with it.

In practice this usually means doing things like:

  • when displaying the number, use String.format to specify the amount of precision to display (it'll do the appropriate rounding for you)
  • when comparing against an expected value, don't look for equality (==). Instead, look for a small-enough delta: Math.abs(myValue - expectedValue) <= someSmallError

EDIT: For infinity, the same principle applies, but with a tweak: you have to pick some number to be "large enough" to treat as infinity. This is again because you have to learn to live with, rather than solve, imprecise values. In the case of something like tan(90 degrees), a double can't store π/2 with infinite precision, so your input is something very close to, but not exactly, 90 degrees -- and thus, the result is something very big, but not quite infinity. You may ask "why don't they just return Double.POSITIVE_INFINITY when you pass in the closest double to π/2," but that could lead to ambiguity: what if you really wanted the tan of that number, and not 90 degrees? Or, what if (due to previous floating-point error) you had something that was slightly farther from π/2 than the closest possible value, but for your needs it's still π/2? Rather than make arbitrary decisions for you, the JDK treats your close-to-but-not-exactly π/2 number at face value, and thus gives you a big-but-not-infinity result.

For some operations, especially those relating to money, you can use BigDecimal to eliminate floating-point errors: you can really represent values like 0.1 (instead of a value really really close to 0.1, which is the best a float or double can do). But this is much slower, and doesn't help you for things like sin/cos (at least with the built-in libraries).

* this probably isn't actually zen, but in the colloquial sense

5
  • Well, that's solved the _0.5 stored as ` 0.49999999999999994` problem. What do I do about infinity? Commented Mar 5, 2014 at 5:20
  • @ambigram_maker Again, you have to pick some "large-enough" number at which you say "okay, close enough to infinity". Doubles actually can store infinity, but you don't get that result because StrictMath.toRadians(90) returns a number really close to, but not exactly pi/2 (which you can't represent in a finite number of bits).
    – yshavit
    Commented Mar 5, 2014 at 5:24
  • So, what if I change my representation system to Radians and not degrees? will it help? Commented Mar 5, 2014 at 5:25
  • 1
    No, because there's just no way to represent π/2 exactly with a float or double. If you had a library that took degrees (rather than radians), you could do that -- since it is possible to represent 90.0 exactly. But afaik, StrictMath doesn't do that.
    – yshavit
    Commented Mar 5, 2014 at 5:34
  • 1
    I guess you're right... I must live with it. :-) So, I guess in certain specific cases, we must return expected values, rather than the seemingly-wrong-but-very-close calculated values Commented Mar 5, 2014 at 5:41
3

You have to use BigDecimal instead of double. Unfortunately, StrictMath doesn't support BigDecimal, so you will have to use another library, or your own implementation of sin/cos/tan.

1
  • 3
    Perhaps further elaboration? Commented Mar 5, 2014 at 5:14
2

This is inherent in using floating-point numbers, in any language. Actually, it's inherent in using any representation with a fixed maximum precision.

There are several solutions. One is to use an extended-precision math package -- BigDecimal is often suggested for Java. BigDecimal can handle many more digits of precision, and also -- because it's a decimal representation rather than a 2's-complement representation -- tends to round off in ways that are less surprising to humans who are used to working in base 10. (That doesn't necessarily make them more correct, please note. Binary can't represent 1/3 exactly, but neither can decimal.)

There are also extended-precision 2's-complement floating-point representations. Java directly supports float and double (which are usually also supported by the hardware), but it's possible to write versions which support more digits of accuracy.

Of course any of the extended-precision packages will slow down your computations. So you shouldn't resort to them unless you actually need them.

Another may to use fixed-point binary rather than floating point. For example, the standard solution for most financial calculations is simply to compute in terms of the smallest unit of currency -- pennies, in the US -- in integers, converting to and from the display format (eg dollars and cents) only for I/O. That's also the approach used for time in Java -- the internal clock reports an integer number of milliseconds (or nanoseconds, if you use the nanotime call), which gives both more than sufficient precision and a more than sufficient range of values for most practical purposes. Again, this means that roundoff tends to happen in a way that matches human expectations... and again, that's less about accuracy than about not surprising the users. And these representations, because they process as integers or longs, allow fast computation -- faster than floating point, in fact.

There are yet other solutions which involve computing in rational numbers, or other variations, in an attempt to compromise between computational cost and precision.

But I also have to ask... Do you really NEED more precision than float is giving you? I know the roundoff is surprising, but in many cases it's perfectly acceptable to just let it happen, possibly rounding off to a less surprising number of fractional digts when you display the results to the user. In many cases, float or double are Just Fine for real-world use. That's why the hardware supports them, and that's why they're in the language.

2
  • That was enlightening... :-) Well, you're right: I don't need precision here, I just need to make it acceptable to humans. But There remains the problem of the infinity. Commented Mar 5, 2014 at 5:37
  • 1
    Floating point has a reserved value meaning infinity, along with a reserved value meaning 'not a meaningfully computable number` (NAN). Some of the others may; you'd have to look at their documentation. Integer and scaled integer and related don't, of course. But generally real-world problems don't actually need INF or NAN except as error cases... unless you're writing a symbolic math package, in which case you mostly aren't going to be computing with numbers at all.
    – keshlam
    Commented Mar 5, 2014 at 5:44

Not the answer you're looking for? Browse other questions tagged or ask your own question.