11.5 Formatting and Parsing Messages, Numbers, Percentages, and Localized Dates
Key Takeaways
MessageFormat replaces numbered placeholders such as {0} and {1,number,currency} by argument index, so a pattern can reorder arguments for each language.
In a MessageFormat pattern a single quote starts literal text, so It's {0} prints Its {0}; write two single quotes for an apostrophe.
NumberFormat.getPercentInstance(Locale.US) formats 0.256 as 26% because percent formats multiply by 100 and show no fraction digits by default.
NumberFormat.parse declares the checked ParseException, and a US currency parser rejects a number without the dollar sign.
DateTimeFormatter is immutable and thread-safe, while NumberFormat, DecimalFormat, and MessageFormat instances are not thread-safe.
11.5 Formatting and Parsing Messages, Numbers, Percentages, and Localized Dates
The localization objective asks you to "parse and format messages, dates, times, and numbers including currency and percentage values." Section 11.4 introduced Locale, resource bundles, and the basic NumberFormat and DateTimeFormatter factories. This section completes the objective with MessageFormat, percentage and compact number formats, DecimalFormat patterns, parsing rules, and locale-sensitive date output.
Formatting Messages with java.text.MessageFormat
Resource bundle values often contain numbered placeholders that are filled in at runtime. MessageFormat replaces {0}, {1}, and so on with arguments in index order, regardless of where they appear in the pattern:
String msg = MessageFormat.format("{0} has {1} items costing {2,number,currency}", "Ana", 3, 9.5);
System.out.println(msg); // Ana has 3 items costing $9.50 (en_US default)
System.out.println(MessageFormat.format("{1} before {0}", "A", "B")); // B before A
System.out.println(MessageFormat.format("{0}", 1234567)); // 1,234,567
System.out.println(MessageFormat.format("{0,number,percent}", 0.5)); // 50%
A placeholder can name a format type and style: {n,number}, {n,number,integer}, {n,number,currency}, {n,number,percent}, {n,date,short}, or {n,time,medium}. Numbers are localized even without a type, which is why 1234567 printed with grouping separators.
For a specific locale, create an instance instead of calling the static method:
MessageFormat mf = new MessageFormat("{0,number,currency}", Locale.GERMANY);
System.out.println(mf.format(new Object[] {9.5})); // 9,50 €
Warning
The apostrophe trap. In a MessageFormat pattern, a single quote starts a quoted (literal) section. MessageFormat.format("It's {0}", "x") prints Its {0}: the quote disappears and the placeholder is never replaced. Write two single quotes for a literal apostrophe: "It''s {0}" prints It's x. Translators who store It's {0} in a .properties file hit exactly this bug.
Combining a bundle with MessageFormat is the standard localization pattern:
ResourceBundle rb = ResourceBundle.getBundle("Messages", new Locale("en", "US"));
// Messages_en_US.properties contains: greeting=Hello, {0}! You have {1} new messages.
String text = MessageFormat.format(rb.getString("greeting"), "Ravi", 4);
// Hello, Ravi! You have 4 new messages.
String.format (with %s, %d, %.2f) is a different API that uses the default FORMAT locale. After Locale.setDefault(Locale.Category.FORMAT, Locale.GERMANY), String.format("%.2f", 1234.5) returns 1234,50, while String.format(Locale.US, "%.2f", 1234.5) returns 1234.50.
Number, Currency, Percentage, and Compact Formats
java.text.NumberFormat provides locale-aware factories. All of them are not thread-safe, unlike DateTimeFormatter.
| Factory | Example (Locale.US unless noted) | Output |
|---|---|---|
NumberFormat.getInstance() / getNumberInstance() | format(3.14159) | 3.142 (at most 3 fraction digits by default) |
NumberFormat.getIntegerInstance() | format(2.5) / format(3.5) | 2 / 4 (rounding mode HALF_EVEN) |
NumberFormat.getCurrencyInstance() | format(49.99) | $49.99; in Locale.GERMANY, 49,99 € |
NumberFormat.getPercentInstance() | format(0.256) / format(1.5) | 26% / 150%; in Locale.FRANCE, 26 % |
NumberFormat.getCompactNumberInstance(locale, Style.SHORT) (Java 12) | format(1_250_000) / format(1_550) | 1M / 2K |
NumberFormat.getCompactNumberInstance(locale, Style.LONG) | format(2_000) | 2 thousand |
Remember that a percent format multiplies by 100: the value 0.256 is 25.6%, which rounds to 26% because the percent instance shows no fraction digits by default.
Custom patterns with DecimalFormat
DecimalFormat (a NumberFormat subclass) uses pattern symbols: 0 always prints a digit (padding with zeros), # prints a digit only when needed, , marks grouping, and . marks the decimal separator.
System.out.println(new DecimalFormat("#,##0.00").format(1234.5)); // 1,234.50
System.out.println(new DecimalFormat("000.##").format(3.14159)); // 003.14
System.out.println(new DecimalFormat("0.0").format(2.25)); // 2.2 (HALF_EVEN tie goes to the even digit)
Parsing Numbers and Currency
NumberFormat.parse(String) returns a Number (Long when the value is integral, otherwise Double) and declares the checked java.text.ParseException:
NumberFormat cf = NumberFormat.getCurrencyInstance(Locale.US);
System.out.println(cf.parse("$1,234.50")); // 1234.5
System.out.println(NumberFormat.getPercentInstance(Locale.US).parse("26%")); // 0.26
cf.parse("1234.50"); // throws ParseException: Unparseable number: "1234.50" (no $ prefix)
A currency parser expects the locale's currency symbol, and a percent parser divides by 100. As section 11.4 showed, parsing stops at the first character that cannot belong to a number, so "45.67xyz" parses as 45.67, but text that cannot start a number throws ParseException.
Locale-Sensitive Dates and Times
DateTimeFormatter styles produce different text for each locale. Use withLocale (or the locale argument of ofPattern) to choose it:
LocalDate d = LocalDate.of(2026, 4, 15);
DateTimeFormatter medium = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
System.out.println(medium.withLocale(Locale.US).format(d)); // Apr 15, 2026
System.out.println(medium.withLocale(Locale.FRANCE).format(d)); // 15 avr. 2026
System.out.println(medium.withLocale(Locale.GERMANY).format(d)); // 15.04.2026
System.out.println(DateTimeFormatter.ofPattern("EEEE d MMMM", Locale.FRANCE).format(d)); // mercredi 15 avril
Month and day names (MMMM, EEEE) come from the formatter's locale, while numeric patterns such as dd/MM/yyyy look the same everywhere. Parsing uses the same formatter: LocalDate.parse("15/04/2026", DateTimeFormatter.ofPattern("dd/MM/yyyy")) succeeds, and a mismatch throws the unchecked DateTimeParseException.
| API | Thread-safe? | Parse failure |
|---|---|---|
DateTimeFormatter | Yes (immutable) | DateTimeParseException (unchecked) |
NumberFormat, DecimalFormat, MessageFormat | No | ParseException (checked) |
What does the following code print?
NumberFormat us = NumberFormat.getPercentInstance(Locale.US);
NumberFormat fr = NumberFormat.getPercentInstance(Locale.FRANCE);
System.out.println(us.format(0.256) + " | " + fr.format(0.256));
25.6% | 25,6 %
26% | 26%
26% | 26 %
0.256% | 0,256 %
A resource bundle stores the value welcome=It's {0}'s turn. What does this code print?
String pattern = "It's {0}'s turn";
System.out.println(MessageFormat.format(pattern, "Sam"));
It's Sam's turn
Its {0}s turn
A java.lang.IllegalArgumentException is thrown
Its Sam's turn
Assuming the enclosing method handles checked exceptions, what is the result?
NumberFormat cf = NumberFormat.getCurrencyInstance(Locale.US);
Number a = cf.parse("$1,234.50");
Number b = cf.parse("1234.50");
System.out.println(a + " " + b);
1234.5 0, because unparseable text is read as zero
$1,234.50 1234.50, because parse keeps the original text
A ParseException is thrown by the second parse call
1234.5 1234.5, because the $ prefix is optional
Sections you finish are checked off in the contents.