Monday, December 26, 2022

Curly Braces #7: Complex math, BigDecimal, and infinity

Fortran has built-in functions for complex math. How do you handle that in Java?

My love for math has varied through the years and encompassed everything up to advanced math learned for a college physics class. I excelled—as long as I was applying the concepts to solve problems. As a software developer, I’ve had to leverage some of this background when working on financial systems early in my career and, more recently, on autonomous vehicle software.

Math is still a hobby to me, and I enjoy reading books on the subject. For instance, in a previous Curly Braces article about null, I mentioned the book Zero: The Biography of a Dangerous Idea, by Charles Seife. I also recently read The Calculus Wars, by Jason Bardi, and Descartes’s Secret Notebook, by Amir Aczel. Those led me to wonder about how Java compares to other languages, such as Fortran, when it comes to building math-intensive applications.

Comparing Java to Fortran

I dislike comparing languages, but for complex math operations it’s fair to compare Java to Fortran, because Fortran is often the choice for math- and science-based applications.

Fortran—short for Formula Translating System—is considered a general-purpose programming language, as it was in the 1950s when it was developed. This is a characteristic Fortran shares with Java.

Fortran was developed as a high-level and source-code-portable alternative to assembly language programming, which made Fortran more accessible and popular with those who were using computers primarily as a tool for another discipline (such as scientists, mathematicians, physicists, and so on). Dedicated programmers were more comfortable with assembly language than were scientists developing algorithms, for example, to help predict weather patterns. This one reason was enough for scientists and mathematicians to use Fortran.

It’s important to note that, unlike Java, Fortran has robust built-in primitive support for different types of numbers—including integers, real numbers, and complex numbers—and their operations. It was precisely the complex number data type that propelled Fortran for use in the sciences decades ago and, as a result, Fortran compilers were further optimized for these numerical use cases.

Complex numbers

If you do a search for the term complex math programming, a lot of information comes up related to the term complex numbers, and the concepts for these two terms are not the same. A complex number is one that contains real and imaginary elements, where the imaginary part contains i, the square root of -2. That is, i2 = -1. A complex number would be of the form a + bi.

Complex numbers, which are essential to algebra and calculus, help solve polynomial equations, and they are directly applicable to problems in the natural world, as well as to the laws of electricity and the world of electronics. Table 1 shows some examples of how Fortran makes programming complex numbers approachable.

Table 1. Examples of programming complex numbers in Fortran

Oracle Java, Oracle Java Programming, Oralce Java Career, Java Skills, Oracle Java Preparation, Java Guides, Java Tutorial and Material

In Fortran, once a complex number is created, you can easily operate on the complex number as a single value, or you can work on the real and imaginary parts individually, as follows:

real:: theta, modulus

complex:: z

modulus= cabs(z)

theta= atan(imag(z)/real(z))

Unlike Fortran, Java does not have native support for complex numbers, that is, numbers or variables that contain both a real and an imaginary component. Fortunately, there are libraries available that provide this functionality. The most comprehensive implementation I’ve found is in the Apache Commons Mathematical Library, which includes a Complex class to represent complex numbers and operations on them. It also provides ComplexUtils for conversion functionality and ComplexFormat to display complex numbers and operations.

Noncomplex math

This is all well and good, but what about math that doesn’t involve complex numbers but uses simple integers or floating-point numbers?

Java provides primitive types and classes for floating-point math: float and Float, as well as double and Double. Since the classes mainly wrap the primitives, while adding some additional functionality, they are equivalent in the way they store and operate on floating-point numbers. This is good in terms of consistency, but it means that neither set is ideal for precise floating-point math, which is required for monetary calculations.

Java’s floating-point arithmetic for float and double floating-point types conforms to the IEEE 754 standard for floating-point arithmetic. An issue is how the numbers are stored in binary format, and this results in unexpected imprecision in certain cases. For example, the following code doesn’t yield the expected output, which is 0.40:

System.out.println("1.00 - 0.60 = " + (1.00 - 6 * 0.10) );

Unfortunately, the output is 0.3999999999999999. I first ran into this issue in a financial application in certain cases when I saw $-0.00 as the output when the output should have been $0.00 for currency display. In those cases, the debugger showed the actual value as -0.000000000053518078857450746.

Joshua Bloch writes extensively about this issue in his book, Effective Java, but in brief, the problem derives from how float and double values are stored internally by the JVM.

Unlike int and long (and other types) that can be stored as exact binary representations of the numbers they’re assigned to, shortcuts are taken with the float and double types. Internally, Java stores values for these types with an inexact representation, using only a portion of the 64 bits for the significant digits. As a result, Java doesn’t store, calculate, or return the exact representation of the actual floating-point value. This seemingly intermittent behavior can be somewhat annoying because it becomes apparent only with specific combinations of numbers and operations. For someone concerned about high accuracy, of course, this situation can be more than merely annoying.

It’s important to note that this isn’t a deficiency in Java and isn’t unique to the JVM; you’ll find it whenever you’re dealing with the IEEE 754 style of floating-point values.

BigDecimal to the rescue. Java provides the java.math.* class, which includes a class called BigDecimal that can be used to alleviate the rounding and loss of precision issues that are often seen with floating-point arithmetic. BigDecimal lets you specify precisely how the rounding behavior should work using the java.math.MathContext class. For instance, the number of digits to be returned can be specified with the object as well. (Frank Kiwy wrote a nice article, “Four common pitfalls of the BigDecimal class and how to avoid them.”)

The following are some examples of using BigDecimal:

// The following code returns: 

// 1.5500000000000000444089209850062616169452667236328125

BigDecimal bd = new BigDecimal(1.55);

// The following code returns: 1.550000

BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL32);

// The following code returns: 1.550000000000000

BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL64);

In the example above, notice how the constructor allows you to specify the precision used to store and work with the floating-point value. Below is how to specify rounding, which must occur when the exact value cannot be represented with the precision used. Note that the scale of the BigDecimal floating-point value indicates the number of digits to the right of the decimal point.

Both of the following code samples return 1.55:

BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL32);

bd = bd.setScale(2);

BigDecimal bd = new BigDecimal(1.55, MathContext.DECIMAL64);

bd = bd.setScale(2);

However, the following throws an exception indicating that rounding is necessary:

BigDecimal bd = new BigDecimal(1.55);

bd = bd.setScale(2);

There are multiple rounding types (to round up, round down, use ceiling or floor operators, and so on), and you can specify the rounding type as the second parameter when you set the scale as the first parameter.

BigDecimal bd = new BigDecimal(1.55);

bd = bd.setScale(2, BigDecimal.ROUND_DOWN);

In each of the examples above, notice that bd is reassigned after the call to setScale. This is done because BigDecimal is immutable; therefore, calling setScale on a BigDecimal object has no effect. This goes for arithmetic operations also; to illustrate, the following example shows floating-point operations that yield unexpected currency results:

double a = 106838.81;

double b = 263970.96;

double c = 879.35;

double d = 366790.80;

double total = 0;

total += a;

total += b;

total -= c;

total -= d;

At the end of this operation, the expected value is 3139.62, but instead the result is 3139.6200000000536.

BigDecimal can deliver the expected results.

BigDecimal total = new BigDecimal(0, MathContext.DECIMAL64);

total = total.setScale(2);

total = total.add(new BigDecimal(a, MathContext.DECIMAL64));

total = total.add(new BigDecimal(b, MathContext.DECIMAL64));

total = total.subtract(new BigDecimal(c, MathContext.DECIMAL64));

total = total.subtract(new BigDecimal(d, MathContext.DECIMAL64));

In the example above, the precision is set to 64 bits, and the scale is set to 2 to adequately represent currency values. The results of calls to add and subtract are reassigned to the original BigDecimal object since it’s immutable. You can avoid the verbose code (and the typing required) to set the precision and scale and instead use helper methods using this code instead:

private BigDecimal doubleToBD32(double val) {

  return new BigDecimal(val, MathContext.DECIMAL64).setScale(2);

}

private BigDecimal doubleToBD64(double val) {

  return new BigDecimal(val, MathContext.DECIMAL64).setScale(2);

}

private BigDecimal doubleToBD128(double val) {

  return new BigDecimal(val, MathContext.DECIMAL128).setScale(2);

}

private BigDecimal doubleToBD(double val) {

  return new BigDecimal(val, MathContext.UNLIMITED).setScale(2);

}

The final code looks like the following, which is more pleasing to me:

double a = 106838.81;

double b = 263970.96;

double c = 879.35;

double d = 366790.80;

BigDecimal total = doubleToBD64(0);

total = total.add( doubleToBD64(a) );

total = total.add( doubleToBD64(b) );

total = total.subtract( doubleToBD64(c) );

total = total.subtract( doubleToBD64(d) );

Although there are many, many more details about using BigDecimal and MathContext (and the entire java.math package, for that matter), this quick overview should help if you ever get surprised by Java’s binary representation of floating-point and double floating-point numbers and arithmetic operations.

The road to infinity

Fortran has native support for infinity, which is useful when you’re working with real numbers. For example, you can assign (and later check for) positive and negative infinity.

real :: a, b

a = -infinity

b = +infinity

Did you know that Java can work with infinities using the Float or Double classes?

double a = Double.POSITIVE_INFINITY;

double b = Double.NEGATIVE_INFINITY;

System.out.println("a="+a+", b="+b);

Source: oracle.com

Related Posts

0 comments:

Post a Comment