12.2 Formatting and Parsing Numbers, Currencies, Dates, Times, and Messages

Key Takeaways

  • NumberFormat.getPercentInstance multiplies its argument by 100 and defaults to zero fraction digits, so 0.25 renders as 25% while 25 renders as 2,500%.
  • NumberFormat.parse returns a Number and throws java.text.ParseException, which is checked, whereas LocalDate.parse throws DateTimeParseException, which is unchecked.
  • In a DecimalFormat pattern the 0 symbol pads an absent digit with a zero while the # symbol omits the position entirely, and the default rounding mode across NumberFormat is HALF_EVEN.
  • DateTimeFormatter is immutable and thread-safe, so withLocale returns a new formatter, and the LONG and FULL time styles require a zone or they throw DateTimeException on a LocalTime or LocalDateTime.
  • MessageFormat treats a single quote as the start of a literal region, so an apostrophe must be doubled or the remaining placeholders are emitted as literal text.
Last updated: September 2026

Formatting and Parsing Numbers, Currencies, Dates, Times, and Messages

The second half of Oracle's localization objective is "Parse and format messages, dates, times, and numbers, including currency and percentage values." Java splits this work across three unrelated APIs, and the exam tests the seams between them - especially which exceptions are checked and which are not.

APIPackageHandlesParse failure throws
NumberFormat / DecimalFormatjava.textNumbers, currency, percentagesParseException (checked)
DateTimeFormatterjava.time.formatDates, times, zonesDateTimeParseException (unchecked)
MessageFormatjava.textComposite messages with placeholdersParseException (checked)

1. NumberFormat: Numbers, Currency, and Percentages

NumberFormat is obtained from static factories, never constructed directly. Every factory has a no-argument form that uses the default FORMAT locale and an overload that takes an explicit Locale.

double amount = 1234.5;

NumberFormat.getInstance(Locale.US).format(amount);          // 1,234.5
NumberFormat.getInstance(Locale.GERMANY).format(amount);     // 1.234,5
NumberFormat.getCurrencyInstance(Locale.US).format(amount);  // $1,234.50
NumberFormat.getIntegerInstance(Locale.US).format(amount);   // 1,235   (rounds)
NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT)
            .format(1_200_000);                              // 1M

Germany swaps the roles of . and , and moves the currency symbol after the amount, which is exactly the sort of contrast the exam uses to check that you understand the locale is doing the work.

Percentages Multiply by 100

NumberFormat pct = NumberFormat.getPercentInstance(Locale.US);
pct.format(0.25);    // 25%
pct.format(0.256);   // 26%   <- percent instances default to 0 fraction digits
pct.setMaximumFractionDigits(1);
pct.format(0.256);   // 25.6%

Forgetting the implicit multiplication is the classic error: formatting 25 with a percent instance yields 2,500%, not 25%.

Parsing Returns a Number and Throws a Checked Exception

NumberFormat nf = NumberFormat.getInstance(Locale.US);
try {
    Number n = nf.parse("1,234.5");   // Double 1234.5
    Number i = nf.parse("1,234");     // Long 1234 - integral input yields a Long
} catch (ParseException e) {          // java.text.ParseException is CHECKED
    // must be caught or declared
}

parse also stops at the first unparseable character rather than failing: nf.parse("12abc") quietly returns 12.

DecimalFormat Patterns

DecimalFormat is the NumberFormat subclass that accepts an explicit pattern. Two symbols carry all the weight:

SymbolMeaning
0A digit; always shown, padded with a zero if absent
#A digit; omitted when the position has no significant digit
,Grouping separator (the actual character comes from the locale)
.Decimal separator (likewise locale-dependent)
%Multiply by 100 and append the locale's percent sign
new DecimalFormat("#,###.##").format(3456.789);   // 3,456.79   (HALF_EVEN rounding)
new DecimalFormat("000.000").format(3.5);         // 003.500
new DecimalFormat("#.##").format(0.5);            // 0.5   -> note the leading digit survives
new DecimalFormat("#.00").format(9);              // 9.00

The default rounding mode across NumberFormat is RoundingMode.HALF_EVEN, not HALF_UP.


2. DateTimeFormatter: Localized Dates and Times

DateTimeFormatter lives in java.time.format and is immutable and thread-safe - the opposite of the legacy SimpleDateFormat. Because it is immutable, withLocale returns a new formatter:

DateTimeFormatter base = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
base.withLocale(Locale.FRANCE);              // return value DISCARDED - a common bug
DateTimeFormatter fr = base.withLocale(Locale.FRANCE);   // correct

Localized styles let the locale choose the pattern for you:

LocalDate date = LocalDate.of(2026, 9, 2);

DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.US).format(date);
// 9/2/26
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.FRANCE).format(date);
// 02/09/2026
DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.FRANCE).format(date);
// 02 septembre 2026

[!CAUTION] The LONG and FULL time styles include a time-zone field. Applying ofLocalizedTime(FormatStyle.FULL) to a LocalTime or ofLocalizedDateTime(FormatStyle.FULL) to a LocalDateTime throws a DateTimeException at run time, because those temporals carry no zone. Use a ZonedDateTime, or drop to SHORT/MEDIUM.

Formatting reads equally well from either side, and parsing is a static method on the target type:

String text = fmt.format(date);      // formatter-first
String same = date.format(fmt);      // temporal-first - identical result

LocalDate back = LocalDate.parse("02/09/2026",
        DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// A mismatch throws DateTimeParseException - a RuntimeException, so NO catch is required

That asymmetry is worth committing to memory: java.text parsing forces a try/catch, java.time parsing does not.


3. MessageFormat: Assembling Localized Sentences

Concatenating translated fragments produces broken word order in other languages. MessageFormat instead uses numbered placeholders, so a translation can reorder them freely.

String pattern = "{0}, you have {1} new messages on {2}.";
MessageFormat.format(pattern, "Duke", 3, LocalDate.of(2026, 9, 2));
// Duke, you have 3 new messages on 2026-09-02.

Placeholders may carry a type and a style, which is how currency, percentage, and date formatting reach into a message:

String pattern = "{0} spent {1,number,currency} ({2,number,percent} of budget).";
MessageFormat mf = new MessageFormat(pattern, Locale.US);
mf.format(new Object[] { "Duke", 1234.5, 0.42 });
// Duke spent $1,234.50 (42% of budget).

Supported forms include {n,number,integer|currency|percent}, {n,date,short|medium|long|full}, {n,time,...}, and {n,choice,...}.

[!IMPORTANT] Inside a MessageFormat pattern a single quote starts a quoted literal. To emit one apostrophe you must double it, and text wrapped in single quotes is passed through untouched:

"It's {0}"    ->  It s {0} placeholder is consumed as literal text
"It''s {0}"   ->  It's Duke                 (correct)
"'{0}' is {1}" ->  {0} is Duke              (the first placeholder is literal)

The lightweight alternative for simple cases is String.format, which also accepts a locale:

String.format(Locale.US,      "%,.2f", 1234.5);   // 1,234.50
String.format(Locale.GERMANY, "%,.2f", 1234.5);   // 1.234,50

4. Exam Checklist

  1. Percent instances multiply by 100 and default to zero fraction digits.
  2. NumberFormat.parse returns a Number (often Long for integral text) and throws the checked ParseException.
  3. LocalDate.parse throws the unchecked DateTimeParseException.
  4. DateTimeFormatter is immutable: assign the result of withLocale.
  5. FULL/LONG time styles need a zone; a LocalTime or LocalDateTime throws DateTimeException.
  6. In DecimalFormat, 0 pads and # disappears.
  7. Default rounding is HALF_EVEN.
  8. In MessageFormat, write '' for a literal apostrophe.
Loading diagram...
Three Localized Formatting APIs and Their Exam Traps
Test Your Knowledge

What is printed by the following code?

NumberFormat pct = NumberFormat.getPercentInstance(Locale.US);
System.out.println(pct.format(0.25) + " " + pct.format(25));

A
B
C
D
Test Your Knowledge

Which statement correctly contrasts parsing failures in java.text with those in java.time?

A
B
C
D
Test Your Knowledge

What does the following code print?

System.out.println(new DecimalFormat("#.##").format(9.0));
System.out.println(new DecimalFormat("0.00").format(9.0));

A
B
C
D
Test Your Knowledge

A localized pattern is written as MessageFormat.format("It's {0}", "Duke"). Why does the output not contain the substituted name?

A
B
C
D
Congratulations!

You've completed this section

Continue exploring other exams