Friday, August 22, 2014

Java Numeric Formatting

I can think of numerous times when I have seen others write unnecessary Java code and I have written unnecessary Java code because of lack of awareness of a JDK class that already provides the desired functionality. One example of this is the writing of time-related constants using hard-coded values such as 60, 24, 1440, and 86400 when TimeUnit provides a better, standardized approach. In this post, I look at another example of a class that provides the functionality I have seen developers often implement on their one: NumberFormat.

The NumberFormat class is part of the java.text package, which also includes the frequently used DateFormat and SimpleDateFormat classes. NumberFormat is an abstract class (no public constructor) and instances of its descendants are obtained via overloaded static methods with names such as getInstance(), getCurrencyInstance(), and getPercentInstance().

Currency

The next code listing demonstrates calling NumberFormat.getCurrencyInstance(Locale) to get an instance of NumberFormat that presents numbers in a currency-friendly format.

Demonstrating NumberFormat's Currency Support
/**
 * Demonstrate use of a Currency Instance of NumberFormat.
 */
public void demonstrateCurrency()
{
   writeHeaderToStandardOutput("Currency NumberFormat Examples");
   final NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.US);
   out.println("15.5      -> " + currencyFormat.format(15.5));
   out.println("15.54     -> " + currencyFormat.format(15.54));
   out.println("15.345    -> " + currencyFormat.format(15.345));  // rounds to two decimal places
   printCurrencyDetails(currencyFormat.getCurrency());
}

/**
 * Print out details of provided instance of Currency.
 *
 * @param currency Instance of Currency from which details
 *    will be written to standard output.
 */
public void printCurrencyDetails(final Currency currency)
{
   out.println("Concurrency: " + currency);
   out.println("\tISO 4217 Currency Code:           " + currency.getCurrencyCode());
   out.println("\tISO 4217 Numeric Code:            " + currency.getNumericCode());
   out.println("\tCurrency Display Name:            " + currency.getDisplayName(Locale.US));
   out.println("\tCurrency Symbol:                  " + currency.getSymbol(Locale.US));
   out.println("\tCurrency Default Fraction Digits: " + currency.getDefaultFractionDigits());
}

When the above code is executed, the results are as shown next:

==================================================================================
= Currency NumberFormat Examples
==================================================================================
15.5      -> $15.50
15.54     -> $15.54
15.345    -> $15.35
Concurrency: USD
 ISO 4217 Currency Code:           USD
 ISO 4217 Numeric Code:            840
 Currency Display Name:            US Dollar
 Currency Symbol:                  $
 Currency Default Fraction Digits: 2

The above code and associated output demonstrate that the NumberFormat instance used for currency (actually a DecimalFormat), automatically applies the appropriate number of digits and appropriate currency symbol based on the locale.

Percentages

The next code listings and associated output demonstrate use of NumberFormat to present numbers in percentage-friendly format.

Demonstrating NumberFormat's Percent Format
/**
 * Demonstrate use of a Percent Instance of NumberFormat.
 */
public void demonstratePercentage()
{
   writeHeaderToStandardOutput("Percentage NumberFormat Examples");
   final NumberFormat percentageFormat = NumberFormat.getPercentInstance(Locale.US);
   out.println("Instance of: " + percentageFormat.getClass().getCanonicalName());
   out.println("1        -> " + percentageFormat.format(1));
   // will be 0 because truncated to Integer by Integer division
   out.println("75/100   -> " + percentageFormat.format(75/100));
   out.println(".75      -> " + percentageFormat.format(.75));
   out.println("75.0/100 -> " + percentageFormat.format(75.0/100));
   // will be 0 because truncated to Integer by Integer division
   out.println("83/93    -> " + percentageFormat.format((83/93)));
   out.println("93/83    -> " + percentageFormat.format(93/83));
   out.println(".5       -> " + percentageFormat.format(.5));
   out.println(".912     -> " + percentageFormat.format(.912));
   out.println("---- Setting Minimum Fraction Digits to 1:");
   percentageFormat.setMinimumFractionDigits(1);
   out.println("1        -> " + percentageFormat.format(1));
   out.println(".75      -> " + percentageFormat.format(.75));
   out.println("75.0/100 -> " + percentageFormat.format(75.0/100));
   out.println(".912     -> " + percentageFormat.format(.912));
}
==================================================================================
= Percentage NumberFormat Examples
==================================================================================
1        -> 100%
75/100   -> 0%
.75      -> 75%
75.0/100 -> 75%
83/93    -> 0%
93/83    -> 100%
.5       -> 50%
.912     -> 91%
---- Setting Minimum Fraction Digits to 1:
1        -> 100.0%
.75      -> 75.0%
75.0/100 -> 75.0%
.912     -> 91.2%

The code and output of the percent NumberFormat usage demonstrate that by default the instance of NumberFormat (actually a DecimalFormat in this case) returned by NumberFormat.getPercentInstance(Locale) method has no fractional digits, multiplies the provided number by 100 (assumes that it is the decimal equivalent of a percentage when provided), and adds a percentage sign (%).

Integers

The small amount of code shown next and its associated output demonstrate use of NumberFormat to present numbers in integral format.

Demonstrating NumberFormat's Integer Format
/**
 * Demonstrate use of an Integer Instance of NumberFormat.
 */
public void demonstrateInteger()
{
   writeHeaderToStandardOutput("Integer NumberFormat Examples");
   final NumberFormat integerFormat = NumberFormat.getIntegerInstance(Locale.US);
   out.println("7.65   -> " + integerFormat.format(7.65));
   out.println("7.5    -> " + integerFormat.format(7.5));
   out.println("7.49   -> " + integerFormat.format(7.49));
   out.println("-23.23 -> " + integerFormat.format(-23.23));
}
==================================================================================
= Integer NumberFormat Examples
==================================================================================
7.65   -> 8
7.5    -> 8
7.49   -> 7
-23.23 -> -23

As demonstrated in the above code and associated output, the NumberFormat method getIntegerInstance(Locale) returns an instance that presents provided numerals as integers.

Fixed Digits

The next code listing and associated output demonstrate using NumberFormat to print fixed-point representation of floating-point numbers. In other words, this use of NumberFormat allows one to represent a number with an exactly prescribed number of digits to the left of the decimal point ("integer" digits) and to the right of the decimal point ("fraction" digits).

Demonstrating NumberFormat for Fixed-Point Numbers
/**
 * Demonstrate generic NumberFormat instance with rounding mode,
 * maximum fraction digits, and minimum integer digits specified.
 */
public void demonstrateNumberFormat()
{
   writeHeaderToStandardOutput("NumberFormat Fixed-Point Examples");
   final NumberFormat numberFormat = NumberFormat.getNumberInstance();
   numberFormat.setRoundingMode(RoundingMode.HALF_UP);
   numberFormat.setMaximumFractionDigits(2);
   numberFormat.setMinimumIntegerDigits(1);
   out.println("234.234567 --> " + numberFormat.format(234.234567));
   out.println("1          --> " + numberFormat.format(1));
   out.println(".234567    --> " + numberFormat.format(.234567));
   out.println(".349       --> " + numberFormat.format(.349));
   out.println(".3499      --> " + numberFormat.format(.3499));
   out.println("0.9999     --> " + numberFormat.format(0.9999));
}
==================================================================================
= NumberFormat Fixed-Point Examples
==================================================================================
234.234567 --> 234.23
1          --> 1
.234567    --> 0.23
.349       --> 0.34
.3499      --> 0.35
0.9999     --> 1

The above code and associated output demonstrate the fine-grain control of the minimum number of "integer" digits to represent to the left of the decimal place (at least one, so zero shows up when applicable) and the maximum number of "fraction" digits to the right of the decimal point. Although not shown, the maximum number of integer digits and minimum number of fraction digits can also be specified.

Conclusion

I have used this post to look at how NumberFormat can be used to present numbers in different ways (currency, percentage, integer, fixed number of decimal points, etc.) and often means no or reduced code need be written to massage numbers into these formats. When I first began writing this post, I envisioned including examples and discussion on the direct descendants of NumberFormat (DecimalFormat and ChoiceFormat), but have decided this post is already sufficiently lengthy. I may write about these descendants of NumberFormat in future blog posts.

5 comments:

Marco Caboni said...

Hi Dusting,
recently I had some issues related to how numbers are rounded by the NumberFormat, and reading your examples I noticed that a couple of them do not behave in the way that I expected.
So I've run the code, and actually saw results different from yours.
These are the examples:

// Currency
out.println("15.345 -> " + currencyFormat.format(15.345)); // I expected "15.34" (because default rounding mode is HALF_EVEN), but got "15.35"
// Fixed digits
out.println(numberFormat.format(.349)); // I expected "0.35" (rounding mode is explicitly set to HALF_UP), but got "0.34"

Since I do not expect that the jvm version or a different locale can cause the difference, I suspect in just some typos composing the article.
Can this be the case?

Cheers,
marco

PS: great blog! I especially like the deep and honest book reviews

Marco Caboni said...

Whoops, sorry Dustin for the typo in your name
:)

@DustinMarx said...

Marco,

Thanks for the compliments. I figured the output shown in my post was accurate because I copied-and-pasted it from the IDE's output, but I re-ran the examples and verified that the output is correct.

Your attention to detail, however, caught a bigger issue. The fix for JDK Bug JDK-7131459 appears to have intentionally changed the way some rounding is done with DecimalFormat. This has led to non-bugs as people have seen discrepancies between Java 7 and Java 8, but it also appears to have introduced potential real bugs (one of which I think you identified in your comment): JDK-8029896 and JDK-8039915 (JDK-8041961 is a duplicate).

Some really good sources of additional details on these issues are Is inconsistency in rounding between Java 7 and Java 8 a bug?, NumberFormat rounding issue with Java 8 only, DigitList bug in recent patch, and Patch for JDK8 HALF_UP rounding bug.

Thanks for leaving the comment. It's good to be aware of these rounding differences.

OceanReBorn said...

overloaded static methods with names such as getInstance(), getCurrencyInstance(), and getPercentInstance().
????
1) to be overloaded methods should have single signature, but these even names are different
2) static methods are never overridden, because they are not virtual - they static

@DustinMarx said...

OceanReBorn,

Thanks for taking the time to leave feedback.

There are two overloaded versions of each of the listed methods; one takes no arguments and one takes a Locale. Each pair shares the same method name but has a different number of arguments (AKA different signature). For example, there is a getPercentInstance() method and a getPercentInstance(Locale) method.

Dustin