Popular programming languages do not natively support decimal numbers. This is because CPUs operate on binary numbers. Even though there is a new IEEE standard for decimal floating point types, CPUs still don’t support it fully. So every time we see a notation like
0.1 in the code, it’s not what it seems. Our calculations might be inaccurate.
Most modern languages have dedicated libraries to handle decimals. Internally, they use either a long integer type or a string to store the number. They implement their own arithmetic engines. In Java, there is a
The safest way to create a new number is to use a string as an input:
final BigDecimal number = new BigDecimal("123.45");
To save memory, special
BigDecimalinstances already exist:
BigDecimal.TEN. You should reuse them instead of creating your own.
It is not recommended to use the
double type when creating a
BigDecimal object. Even if we enter a value like
0.1, the actual representation equals to something around 0.10000000000000000555 which definitely does not look like a monetary amount or anything else that we would expect. This is because
double is a base-2 scientific notation type. Try running this code to see it for yourself:
System.out.println(0.20 + 0.10);
BigDecimal class offers several methods for basic operations like addition, subtraction, multiplication and division. Before we go into calculations, we need to talk more about the internals.
Precision and scale
BigDecimal uses two parameters to define the maximum number of digits it can hold and how many digits are behind the decimal point. The first one is called precision, and the other one is called scale.
It is very important that you understand what happens to these parameters because they affect rounding and the string representation.
If you use the simplest string constructor like in the examples above, precision is set to 0 (which means infinite length) and scale is set to the number of digits behind the decimal point. For
123.45, scale will be 2.
You can use the
setScale() method to increase scale if you want to show the exact number of digits in a fraction, even if these will be zeros. The number
123.45 with a scale of 4 would be represented as
Scale can change when you add, subtract, multiply or divide numbers with fractions. This matters especially when you try to calculate taxes. For example, multiplying
1.23 gives us
151.8435, but this is not a proper monetary amount. You have to perform rounding using the second argument for
final BigDecimal net = new BigDecimal("123.45"); final BigDecimal tax = new BigDecimal("1.23"); final BigDecimal gross = net.multiply(tax).setScale(2, RoundingMode.HALF_UP); System.out.println(gross); // output is 151.84
Some numbers do not have a finite decimal representation, like 1/3. They cannot be stored as
BigDecimal and rounding has to be applied. It’s your responsibility to specify target precision or scale, otherwise division might cause an exception:
final BigDecimal result = BigDecimal.ONE.divide( new BigDecimal("3"), 5, RoundingMode.HALF_EVEN); System.out.println(result); // output is 0.33333
Another operation that involves changing scale is removing trailing zeros. Sometimes, for example after several subtractions, you don’t want to leave zeros at the end. The
stripTrailingZeros() method will return the same number without trailing zeros.
In the previous example you’ve seen an example of rounding. The most popular option is called
HALF_UP and it is commonly taught at school. You round up when the discarded fraction is greater than or equal to
0.5; you round down when the fraction is below
0.5. So for example, assuming a target scale of 2, the number
1.234 will be rounded to
1.23, and the number
1.235 will be rounded to
However, different taxation laws might require different rounding modes, for example always rounding up. They are listed in an enum called
Below are some trivias which can help you distinguish between different modes:
UPnever decreases the magnitude of the calculated value.
DOWNnever increases the magnitute of the calculated value.
HALF_UPis commonly taught at school.
FLOORnever increases the calculated value.
CEILINGnever decreases the calculated value.
HALF_EVENis also known as “banker’s rounding” because it reduces error when performing multiple operations on rounded numbers. If the first digit outside scale is 5, we round to the nearest even number. Otherwise, standard rules apply.
UNNECESSARYis used to check if rounding was performed or not; if rounding would be necessary, an
Always consult the rounding mode and other assumptions with an accounting or taxation expert. It is their responsibility to make decisions according to the law, and your responsibility is only to write reliable software that implements these rules.
BigDecimal class uses rules defined by a
MathContext to perform numerical operations. In most cases you won’t need to worry about it. However, we should get back to the example of dividing 1 by 3.
BigDecimal numbers have “unlimited” precision. In fact, the maximum unscaled value is equal to 2^Integer.MAX_VALUE, according to the
BigInteger documentation. This looks like more than enough to represent any finite number you need.
Nevertheless, we don’t want to run out of memory when doing a simple division of 1 by 3. Earlier, we just specified a desired scale and a rounding mode, but you should be also aware that you can control precision of such operation.
There are three
MathContext objects that correspond to the IEEE 754R decimal formats.
DECIMAL128 allow a maximum number of 7, 16 and 34 digits, respectively. They all use the
HALF_EVEN rounding mode. You can use these contexts to control division:
final BigDecimal result = BigDecimal.ONE.divide( new BigDecimal("3"), MathContext.DECIMAL32); System.out.println(result); // output is 0.3333333
A very important concept of Java
BigDecimal type is immutability. It means that once an object is instantiated, its state cannot be changed. The only way to obtain a modified object is to create a new instance:
final BigDecimal number1 = new BigDecimal("99"); number1.add(BigDecimal.ONE); System.out.println(number1); // number1 is still 99
This behavior prevents many bugs that could occur if we passed an object to other methods and they unexpectedly altered the object’s state.
A standard way to output a
BigDecimal object on the screen is to just use the
toString() method. There are two other methods though, and it’s worth to know them.
The difference is visible when we operate on numbers written using scientific notation, like
1.23E+3, which is equal to
toString() method will create a string in that notation, while
toPlainString() will always return the full number.
toEngineeringString() is a variation where the exponent is always a multiple of three (if an exponent is needed at all).
Just to remind, you can use
stripTrailingZeros() to strip unnecessary zeros from fractions:
final BigDecimal numberWithZeros = new BigDecimal("1.000"); System.out.println(numberWithZeros); // output is 1.000 final BigDecimal strippedNumber = numberWithZeros.stripTrailingZeros(); System.out.println(strippedNumber); // output is 1
The only problem with all the examples above is that they don’t conform to language rules other than English. What if we want to make an international application?
Using locale for number formatting
Most programming languages assume English notation for numbers. They use a dot to separate decimal part from an integer part. When presenting a number to a user, we can optionally separate thousands with comma.
However, many languages and countries have different regulations. If our application is dedicated for international markets, localization is a very important matter we should take into account.
To make localization easier, a concept of a locale was introduced. A locale is a “set of parameters that defines the user’s language, region and any special variant preferences that the user wants to see in their user interface.” (Wikipedia)
A locale identifier combines language and country code. So for British English we have
en_GB, American English is
en_US, and Swiss German will be
Let’s analyze how a sample number would be formatted using some of the world’s locales. We’ll pick twelve thousand three hundred forty five point sixty seven, which can be written as
12345.67 in the code:
|Language||Country||Locale code||Formatted value|
Notice the difference for Spanish language. In Spain, people use a dot to separate thousands and a comma as a decimal separator. In Mexico, it’s the other way around, just like in the U.S. It means that it’s not enough to localize your application for a specific language; the region is important too.
Formatting and parsing numbers with NumberFormat
An abstract class called
NumberFormat has multiple
getInstance()-like methods that we can use to create a localized number format, depending on our needs. As the only argument, we should specify a desired locale. If we skip this, the default system locale will be used.
final BigDecimal result = new BigDecimal("12345.67"); final NumberFormat numberFormat = NumberFormat.getInstance(Locale.forLanguageTag("en_US")); System.out.println(numberFormat.format(result)); // output is 12,345.67
The number format can be further customized. For example, you can turn grouping off by calling
You can also use
NumberFormat.getPercentInstance() to create a percentage format. This way, a number like
0.51 will be presented as
51%. Such format is useful to print a tax rate.
Here’s an extended version of the code to calculate tax and gross values – typical data on every sales invoice:
final BigDecimal net = new BigDecimal("123.45"); final BigDecimal taxRate = new BigDecimal("0.23"); final BigDecimal tax = net.multiply(taxRate).setScale(2, RoundingMode.HALF_UP); final BigDecimal gross = net.add(tax); final NumberFormat numberFormat = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("en_US")); numberFormat.setCurrency(Currency.getInstance("USD")); final NumberFormat percentFormat = NumberFormat.getPercentInstance(Locale.forLanguageTag("en_US")); System.out.println("Net value: " + numberFormat.format(net)); System.out.println("Tax value: " + numberFormat.format(tax)); System.out.println("Tax rate: " + percentFormat.format(taxRate)); System.out.println("Gross value: " + numberFormat.format(gross)); /* output is: Net value: USD 123.45 Tax value: USD 28.39 Tax rate: 23% Gross value: USD 151.84 */
Decimal calculations need extra care. Computers do not support decimal numbers natively, so we have to use dedicated libraries like
Accuracy is especially important for monetary calculations. I recommend using the Java Money library as it also introduces handling currencies. However, knowing the
BigDecimal class can still be useful.
This is a part of the book I’m writing – “Money explained to Java developers”. If you’re interested, follow me on Twitter for more insights!