Sunday, August 12, 2007

ArithmeticException vs. NaN

Recently I worked on a small UI widget that renders bars that represent percentage. The percentage could be calculated by a simple formula such as the following:

percentage = value / total * 100%

Imagine a utility method that calculates the percentage and looks like the following:

public static int calcPercentage(int value, int total) {
return 100 * value / total;
}

You could say that this is a fairly simple method and would not be that hard to unit test it. Assuming that the given parameters will never be negative numbers one could whip up a sample test values very quickly.

public void testCalPercentage() {
assertEquals(0, calcPercentage(0, 2));
assertEquals(33, calcPercentage(1, 2));
assertEquals(50, calcPercentage(2, 2));
}

Take a bunch of positive numbers or zeros... Wait! Zeros?! But there could be a possible division by zero! That's right! There is our edge case. Let's test it! Aha! We now get an ArithmeticException.

public void testCalPercentageDivisionByZero() {
try {
assertEquals(0, calcPercentage(0, 0));
fail();
}
catch (ArithmeticException ex) {
// expected
}
}

That's understandable. So we need to work around this and the bar should not be rendered if not values (zeros) were entered.

And here comes the twist! Change the type of spent and remaining variables from primitive int to primitive float.

public static int calcPercentage(float value, float total) {
return (int) (100 * value / total);
}

No exception is thrown anymore. What happened? Why such a different behavior?

It all boils down to how Java and other programming languages handle float point arithmetic. I hope that the following quote sums it all.

Floating-point numbers in the JVM use a radix of two. Floating-point numbers in the JVM, therefore, have the following form:

sign * mantissa * 2 exponent

The mantissa of a floating-point number in the JVM is expressed as a binary number. A normalized mantissa has its binary point (the base-two equivalent of a decimal point) just to the left of the most significant non-zero digit. Because the binary number system has just two digits -- zero and one -- the most significant digit of a normalized mantissa is always a one.

The most significant bit of a float or double is its sign bit. The mantissa occupies the 23 least significant bits of a float and the 52 least significant bits of a double. The exponent, 8 bits in a float and 11 bits in a double, sits between the sign and mantissa. The format of a float is shown below. The sign bit is shown as an "s," the exponent bits are shown as "e," and the mantissa bits are shown as "m":

Bit layout of Java float
s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm

The exponent field is interpreted in one of three ways. An exponent of all ones indicates the floating-point number has one of the special values of plus or minus infinity, or "not a number" (NaN). NaN is the result of certain operations, such as the division of zero by zero. An exponent of all zeros indicates a denormalized floating-point number. Any other exponent indicates a normalized floating-point number.

The JVM throws no exceptions as a result of any floating-point operations. Special values, such as positive and negative infinity or NaN, are returned as the result of suspicious operations such as division by zero. An exponent of all ones indicates a special floating-point value. An exponent of all ones with a mantissa whose bits are all zero indicates an infinity. The sign of the infinity is indicated by the sign bit. An exponent of all ones with any other mantissa is interpreted to mean "not a number" (NaN). The JVM always produces the same mantissa for NaN, which is all zeros except for the most significant mantissa bit that appears in the number. These values are shown for a float below:

Special float values
ValueFloat bits (sign exponent mantissa)
+Infinity0 11111111 00000000000000000000000
-Infinity1 11111111 00000000000000000000000
NaN1 11111111 10000000000000000000000


Things like these we learn when we learn computer programming and tend to forget over time. And then... a simple percentage calculation will remind us.

Happy coding!

1 comment:

Anonymous said...

Thanks. It is very clear and I have the same issue in my application. But mine is with a double.


Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License.