I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this article, we’re going to explore the DecimalFormat class along with its practical usages.

This is a subclass of NumberFormat, which allows formatting decimal numbers’ String representation using predefined patterns.

It can also be used inversely, to parse Strings into numbers.

2. How Does it Work?

In order to format a number, we have to define a pattern, which is a sequence of special characters potentially mixed with text.

There are 11 Special Pattern Characters, but the most important are:

  • 0 – prints a digit if provided, 0 otherwise
  • # – prints a digit if provided, nothing otherwise
  • . – indicate where to put the decimal separator
  • , – indicate where to put the grouping separator

When the pattern gets applied to a number, its formatting rules are executed, and the result is printed according to the DecimalFormatSymbol of our JVM’s Locale unless a specific Locale is specified.

The following examples’ outputs are from a JVM running on an English Locale.

3. Basic Formatting

Let’s now see which outputs are produced when formatting the same number with the following patterns.

3.1. Simple Decimals

double d = 1234567.89;    
assertThat(
  new DecimalFormat("#.##").format(d)).isEqualTo("1234567.89");
assertThat(
  new DecimalFormat("0.00").format(d)).isEqualTo("1234567.89");

As we can see, the integer part is never discarded, no matter if the pattern is smaller than the number.

assertThat(new DecimalFormat("#########.###").format(d))
  .isEqualTo("1234567.89");
assertThat(new DecimalFormat("000000000.000").format(d))
  .isEqualTo("001234567.890");

If the pattern instead is bigger than the number, zeros get added, while hashes get dropped, both in the integer and in the decimal parts.

3.2. Rounding

If the decimal part of the pattern can’t contain the whole precision of the input number, it gets rounded.

Here, the .89 part has been rounded to .90, then the 0 has been dropped:

assertThat(new DecimalFormat("#.#").format(d))
  .isEqualTo("1234567.9");

Here, the .89 part has been rounded to 1.00, then the .00 has been dropped and the 1 has been summed to the 7:

assertThat(new DecimalFormat("#").format(d))
  .isEqualTo("1234568");

The default rounding mode is HALF_EVEN, but it can be customized through the setRoundingMode method.

3.3. Grouping

The grouping separator is used to specify a sub-pattern which gets repeated automatically:

assertThat(new DecimalFormat("#,###.#").format(d))
  .isEqualTo("1,234,567.9");
assertThat(new DecimalFormat("#,###").format(d))
  .isEqualTo("1,234,568");

3.4. Multiple Grouping Patterns

Some countries have a variable number of grouping patterns in their numbering systems.

The Indian Numbering System uses the format #,##,###.##, in which only the first grouping separator holds three numbers, while all the others hold two numbers.

This isn’t possible to achieve using the DecimalFormat class, which keeps only the latest pattern encountered from left to right, and applies it to the whole number, ignoring previous grouping patterns.

An attempt to use the pattern #,##,##,##,### would result in a regroup to #######,### and end in a redistribution to #,###,###,###.

To achieve multiple grouping pattern matching, it’s necessary to write our own String manipulation code, or alternatively to try the Icu4J’s DecimalFormat, which allows that.

3.5. Mixing String Literals

It’s possible to mix String literals within the pattern:

assertThat(new DecimalFormat("The # number")
  .format(d))
  .isEqualTo("The 1234568 number");

It’s also possible to use special characters as String literals, through escaping:

assertThat(new DecimalFormat("The '#' # number")
  .format(d))
  .isEqualTo("The # 1234568 number");

4. Localized Formatting

Many countries don’t use English symbols and use the comma as decimal separator and the dot as grouping separator.

Running the #,###.## pattern on a JVM with an Italian Locale, for example, would output 1.234.567,89.

While this could be a useful i18n feature in some cases, in others we might want to enforce a specific, JVM-independent format.

Here’s how we can do that:

assertThat(new DecimalFormat("#,###.##", 
  new DecimalFormatSymbols(Locale.ENGLISH)).format(d))
  .isEqualTo("1,234,567.89");
assertThat(new DecimalFormat("#,###.##", 
  new DecimalFormatSymbols(Locale.ITALIAN)).format(d))
  .isEqualTo("1.234.567,89");

If the Locale we’re interested in is not among the ones covered by the DecimalFormatSymbols constructor, we can specify it with the getInstance method:

Locale customLocale = new Locale("it", "IT");
assertThat(new DecimalFormat(
  "#,###.##", 
   DecimalFormatSymbols.getInstance(customLocale)).format(d))
  .isEqualTo("1.234.567,89");

5. Scientific Notations

The Scientific Notation represents the product of a Mantissa and an exponent of ten. The number 1234567.89 can also be represented as 12.3456789 * 10^5 (the dot is shifted by 5 positions).

5.1. E-Notation

It’s possible to express a number in Scientific Notation using the E pattern character representing the exponent of ten:

assertThat(new DecimalFormat("00.#######E0").format(d))
  .isEqualTo("12.3456789E5");
assertThat(new DecimalFormat("000.000000E0").format(d))
  .isEqualTo("123.456789E4");

We should keep in mind that the number of characters after the exponent is relevant, so if we need to express 10^12, we need E00 and not E0.

5.2. Engineering Notation

It’s common to use a particular form of Scientific Notation called Engineering Notation, which adjusts results in order to be expressed as multiple of three, for example when using measuring units like Kilo (10^3), Mega (10^6), Giga (10^9), and so on.

We can enforce this kind of notation by adjusting the maximum number of integer digits (the characters expressed with the # and on the left of the decimal separator) so that it’s higher than the minimum number (the one expressed with the 0) and higher than 1.

This forces the exponent to be a multiple of the maximum number, so for this use-case we want the maximum number to be three:

assertThat(new DecimalFormat("##0.######E0")
  .format(d)).isEqualTo("1.23456789E6");		
assertThat(new DecimalFormat("###.000000E0")
  .format(d)).isEqualTo("1.23456789E6");

6. Parsing

Let’s see how is possible to parse a String into a Number with the parse method:

assertThat(new DecimalFormat("", new DecimalFormatSymbols(Locale.ENGLISH))
  .parse("1234567.89"))
  .isEqualTo(1234567.89);
assertThat(new DecimalFormat("", new DecimalFormatSymbols(Locale.ITALIAN))
  .parse("1.234.567,89"))
  .isEqualTo(1234567.89);

Since the returned value isn’t inferred by the presence of a decimal separator, we can use the methods like .doubleValue(), .longValue() of the returned Number object to enforce a specific primitive in output.

We can also obtain a BigDecimal as follows:

NumberFormat nf = new DecimalFormat(
  "", 
  new DecimalFormatSymbols(Locale.ENGLISH));
((DecimalFormat) nf).setParseBigDecimal(true);
 
assertThat(nf.parse("1234567.89"))
  .isEqualTo(BigDecimal.valueOf(1234567.89));

7. Thread-Safety

DecimalFormat isn’t thread-safe, thus we should pay special attention when sharing the same instance between threads.

8. Conclusion

We’ve seen the major usages of the DecimalFormat class, along with its strengths and weaknesses.

As always, the full source code is available over on Github.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS

Leave a Reply

Be the First to Comment!

avatar
  Subscribe  
Notify of