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.
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.
| API | Package | Handles | Parse failure throws |
|---|---|---|---|
NumberFormat / DecimalFormat | java.text | Numbers, currency, percentages | ParseException (checked) |
DateTimeFormatter | java.time.format | Dates, times, zones | DateTimeParseException (unchecked) |
MessageFormat | java.text | Composite messages with placeholders | ParseException (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:
| Symbol | Meaning |
|---|---|
0 | A 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
LONGandFULLtime styles include a time-zone field. ApplyingofLocalizedTime(FormatStyle.FULL)to aLocalTimeorofLocalizedDateTime(FormatStyle.FULL)to aLocalDateTimethrows aDateTimeExceptionat run time, because those temporals carry no zone. Use aZonedDateTime, or drop toSHORT/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
MessageFormatpattern 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
- Percent instances multiply by 100 and default to zero fraction digits.
NumberFormat.parsereturns aNumber(oftenLongfor integral text) and throws the checkedParseException.LocalDate.parsethrows the uncheckedDateTimeParseException.DateTimeFormatteris immutable: assign the result ofwithLocale.FULL/LONGtime styles need a zone; aLocalTimeorLocalDateTimethrowsDateTimeException.- In
DecimalFormat,0pads and#disappears. - Default rounding is HALF_EVEN.
- In
MessageFormat, write''for a literal apostrophe.
What is printed by the following code?
NumberFormat pct = NumberFormat.getPercentInstance(Locale.US);
System.out.println(pct.format(0.25) + " " + pct.format(25));
Which statement correctly contrasts parsing failures in java.text with those in java.time?
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 localized pattern is written as MessageFormat.format("It's {0}", "Duke"). Why does the output not contain the substituted name?
You've completed this section
Continue exploring other exams