Format Currency in Java with NumberFormat, Locale and BigDecimal

Format currency in Java with NumberFormat.getCurrencyInstance(), Locale and BigDecimal. Switch currencies, parse amounts safely and control rounding.

Diagram of a BigDecimal amount and a Locale going into NumberFormat.getCurrencyInstance and producing dollar, euro, rupee and yen strings

To format currency in Java, we get a formatter with NumberFormat.getCurrencyInstance(locale) and call its format() method, which adds the currency symbol, the grouping and decimal separators and the number of decimal places that the locale uses. The locale decides the layout, and the region of the locale decides the currency.

We use currency formatting wherever money reaches a person, for example, prices in an online shop, totals on an invoice or a balance in a banking app.

The following example formats one product price for four locales.

BigDecimal price = new BigDecimal("1234.56");
String us = NumberFormat.getCurrencyInstance(Locale.US).format(price);                 // "$1,234.56"
String germany = NumberFormat.getCurrencyInstance(Locale.GERMANY).format(price);       // "1.234,56 €"
String india = NumberFormat.getCurrencyInstance(Locale.of("en", "IN")).format(price);  // "₹1,234.56"
String japan = NumberFormat.getCurrencyInstance(Locale.JAPAN).format(price);           // "¥1,235"

Notice that the yen amount has no decimals and is rounded, because the yen has no minor unit. A currency formatter never converts money between currencies; it only writes the number the way a country writes its own currency, so exchange rates stay our job. We start with the classes involved and move on to a currency other than the locale’s, parsing text back to a number and the rounding rules.

1. Classes used in Currency Formatting

Three classes work together. The Locale tells Java which country’s conventions to use, the Currency names the money by its ISO 4217 code, and the NumberFormat turns an amount into text.

Diagram of a BigDecimal amount and a Locale going into NumberFormat.getCurrencyInstance and producing dollar, euro, rupee and yen strings
The Locale picks the symbol, separators and decimals; the amount itself stays the same
  • The class java.util.Locale identifies a language and a region, such as de-DE. Only a locale with a region has a currency, which Java Locale explains.
  • The class java.util.Currency represents one ISO 4217 currency, such as EUR. It has no public constructor, so we get the single shared instance with Currency.getInstance().
  • The class java.text.NumberFormat formats and parses numbers. The factory getCurrencyInstance() returns a formatter set up for money in the given locale.
Currency euro = Currency.getInstance("EUR");
String code = euro.getCurrencyCode();                        // "EUR"
String name = euro.getDisplayName(Locale.US);                // "Euro"
String symbol = euro.getSymbol(Locale.FRANCE);               // "€"
int decimals = euro.getDefaultFractionDigits();              // 2
int yenDecimals = Currency.getInstance("JPY").getDefaultFractionDigits();     // 0
int dinarDecimals = Currency.getInstance("BHD").getDefaultFractionDigits();   // 3
Currency fromLocale = Currency.getInstance(Locale.JAPAN);    // JPY

A symbol depends on the reader. For example, the US dollar is $ for a US reader but US$ for a Canadian reader, because Canadians use $ for their own dollar.

String forUs = Currency.getInstance("USD").getSymbol(Locale.US);         // "$"
String forCanada = Currency.getInstance("USD").getSymbol(Locale.CANADA); // "US$"

2. Format Currency in Java

To format a value, we pass it to format(). The formatter accepts BigDecimal, long and double, and we use BigDecimal for money, because double cannot store most decimal fractions without a tiny error, as section 4 shows.

Locale1234.56-1234.50
Locale.US$1,234.56-$1,234.50
Locale.UK£1,234.56-£1,234.50
Locale.GERMANY1.234,56 €-1.234,50 €
Locale.FRANCE1 234,56 €-1 234,50 €
Locale.of(“de”, “CH”)CHF 1’234.56CHF-1’234.50
Locale.of(“pt”, “BR”)R$ 1.234,56-R$ 1.234,50
Locale.JAPAN¥1,235-¥1,234

Each row comes from Java 25 with its default CLDR locale data. The position of the symbol, the separators and even the place of the minus sign change by country, which is why we never build these strings with String.format().

A locale without a region has no currency. The formatter for Locale.ENGLISH or Locale.CHINESE prints the generic currency sign ¤ and uses the code XXX, and Currency.getInstance(Locale.ENGLISH) throws an exception.

String noRegion = NumberFormat.getCurrencyInstance(Locale.ENGLISH).format(9.99);   // "¤9.99"
Currency missing = Currency.getInstance(Locale.ENGLISH);    // IllegalArgumentException: The country of the input locale: "en" is not a valid ISO 3166 country code

2.1. Showing a Different Currency Than the Locale’s

Say a US online store sells holidays in Europe and charges in euros. The page must use US separators, but the euro sign. The method setCurrency() changes the currency of a formatter while it keeps the layout of the locale.

NumberFormat usInEuro = NumberFormat.getCurrencyInstance(Locale.US);
usInEuro.setCurrency(Currency.getInstance("EUR"));
String euroForUs = usInEuro.format(new BigDecimal("1234.56"));     // "€1,234.56"

The method setCurrency() does not update the number of decimal places, so a US formatter switched to yen still prints two decimals. We copy the decimals from the currency ourselves.

static NumberFormat currencyFormat(Locale locale, Currency currency) {
    NumberFormat format = NumberFormat.getCurrencyInstance(locale);
    format.setCurrency(currency);
    format.setMinimumFractionDigits(currency.getDefaultFractionDigits());
    format.setMaximumFractionDigits(currency.getDefaultFractionDigits());
    return format;
}
NumberFormat plain = NumberFormat.getCurrencyInstance(Locale.US);
plain.setCurrency(Currency.getInstance("JPY"));
String wrongYen = plain.format(new BigDecimal("1234.56"));                                         // "¥1,234.56"
String rightYen = currencyFormat(Locale.US, Currency.getInstance("JPY")).format(new BigDecimal("1234.56"));   // "¥1,235"

2.2. Accounting Format for Negative Amounts

Financial reports often show negative amounts in parentheses. Since Java 14, the Unicode extension -u-cf-account on the locale switches to the accounting format of that locale.

Locale accounting = Locale.forLanguageTag("en-US-u-cf-account");
String loss = NumberFormat.getCurrencyInstance(accounting).format(new BigDecimal("-1234.50"));     // "($1,234.50)"
String profit = NumberFormat.getCurrencyInstance(accounting).format(new BigDecimal("1234.50"));    // "$1,234.50"

2.3. Spaces in the Formatted Text

The space in French and German amounts is not a regular space. Java 25 puts a no-break space (U+00A0) between the number and the euro sign, and French uses a narrow no-break space (U+202F) as the grouping separator. The output looks right on a page, but a test that compares it with a hand-typed string fails.

String french = NumberFormat.getCurrencyInstance(Locale.FRANCE).format(new BigDecimal("1234.56"));   // "1 234,56 €"
boolean typed = french.equals("1 234,56 €");                          // false
boolean exact = french.equals("1\u202f234,56\u00a0€");                // true

In tests, we write the expected value with the \u202f and \u00a0 escapes, or we format the expected value with the same formatter. The exact characters can change when a new JDK updates its CLDR data.

3. Parsing Currency

The method parse() turns formatted text back into a Number. It needs the currency symbol of the locale and throws a ParseException without it. By default, it returns a Long or a Double, so we call setParseBigDecimal(true) on the DecimalFormat behind the formatter to keep the exact value.

NumberFormat usFormat = NumberFormat.getCurrencyInstance(Locale.US);
Number parsed = usFormat.parse("$1,234.57");          // 1234.57
Number noSymbol = usFormat.parse("1,234.57");         // ParseException: Unparseable number: "1,234.57"
Number partial = usFormat.parse("$12abc");            // 12

The last line is a common source of bugs. The method parse(String) stops at the first character it does not understand and returns what it read so far, so $12abc becomes 12 without an error. When the text comes from a user or a file, we parse with a ParsePosition and reject the input unless the whole text was consumed.

static Optional<BigDecimal> parsePrice(String text, Locale locale) {
    if (text == null || !(NumberFormat.getCurrencyInstance(locale) instanceof DecimalFormat format)) {
        return Optional.empty();
    }
    String input = text.strip();
    format.setParseBigDecimal(true);
    ParsePosition position = new ParsePosition(0);
    Number number = format.parse(input, position);
    if (number == null || position.getIndex() != input.length()) {
        return Optional.empty();
    }
    return Optional.of((BigDecimal) number);
}
Optional<BigDecimal> valid = parsePrice(" $1,234.57 ", Locale.US);   // Optional[1234.57]
Optional<BigDecimal> invalid = parsePrice("$12abc", Locale.US);      // Optional.empty
Optional<BigDecimal> german = parsePrice("1.234,57\u00a0€", Locale.GERMANY);   // Optional[1234.57]

4. Handling Currency Round Off

A currency formatter rounds to the decimals of the currency, and by default it uses RoundingMode.HALF_EVEN, which rounds a value that lies precisely halfway to the even neighbor. This mode is also called banker’s rounding, because it does not push totals upward over many transactions.

NumberFormat usd = NumberFormat.getCurrencyInstance(Locale.US);
RoundingMode mode = usd.getRoundingMode();                 // HALF_EVEN
String halfToEven = usd.format(new BigDecimal("0.125"));   // "$0.12"
String halfToEven2 = usd.format(new BigDecimal("0.135"));  // "$0.14"

Rounding works on the exact value only when we pass a BigDecimal. The double literal 2.675 is stored as 2.67499999…, so the formatter rounds it down, whereas the BigDecimal 2.675 lies precisely halfway and goes to the even digit 8.

NumberFormat dollars = NumberFormat.getCurrencyInstance(Locale.US);
String fromDouble = dollars.format(2.675);                      // "$2.67"
String fromBigDecimal = dollars.format(new BigDecimal("2.675"));  // "$2.68"

We change the mode with setRoundingMode(), for example, HALF_UP for receipts in countries that require it, or DOWN to cut off fractions of a cent.

NumberFormat halfUp = NumberFormat.getCurrencyInstance(Locale.US);
halfUp.setRoundingMode(RoundingMode.HALF_UP);
String receipt = halfUp.format(new BigDecimal("123456.785"));   // "$123,456.79"
NumberFormat exactOnly = NumberFormat.getCurrencyInstance(Locale.US);
exactOnly.setRoundingMode(RoundingMode.UNNECESSARY);
String notExact = exactOnly.format(new BigDecimal("1.005"));    // ArithmeticException: Rounding needed with the rounding mode being set to RoundingMode.UNNECESSARY

The table shows each RoundingMode with two decimals for a positive and a negative amount. The modes UP and DOWN are defined relative to zero, whereas CEILING and FLOOR are defined relative to positive and negative infinity.

Rounding modeRule1.005-1.0051.015
UPAway from zero1.01-1.011.02
DOWNToward zero1.00-1.001.01
CEILINGToward positive infinity1.01-1.001.02
FLOORToward negative infinity1.00-1.011.01
HALF_UPNearest, a tie goes away from zero1.01-1.011.02
HALF_DOWNNearest, a tie goes toward zero1.00-1.001.01
HALF_EVENNearest, a tie goes to the even digit1.00-1.001.02
UNNECESSARYNo rounding allowedArithmeticExceptionArithmeticExceptionArithmeticException

5. Currency Formatting FAQs

Money formatting raises these questions once the first price shows up wrong in production.

5.1. Should we store money as double in Java?

No. A double stores binary fractions, so 0.1 + 0.2 gives 0.30000000000000004 and halfway values round the wrong way. We store amounts as BigDecimal, or as a long number of cents, and format them only for display.

5.2. Why does Java format Indian rupees as 1,234,567.89 instead of 12,34,567.89?

Because DecimalFormat supports only one grouping size, so Java 25 prints the Western grouping even for en-IN. When an app needs the lakh and crore grouping, we use a library such as ICU4J, whose number formatter supports the Indian pattern.

String rupees = NumberFormat.getCurrencyInstance(Locale.of("en", "IN")).format(new BigDecimal("1234567.89"));   // "₹1,234,567.89"

5.3. How do we format an amount without the currency symbol?

We use NumberFormat.getNumberInstance(locale) and set the decimals of the currency, so we keep the separators of the locale but drop the symbol. More options are covered in formatting a float with fixed decimal places.

NumberFormat amountOnly = NumberFormat.getNumberInstance(Locale.GERMANY);
amountOnly.setMinimumFractionDigits(2);
amountOnly.setMaximumFractionDigits(2);
String plainAmount = amountOnly.format(new BigDecimal("1234.5"));   // "1.234,50"

5.4. Can Java convert between currencies?

Not with the JDK. NumberFormat and Currency only format, and conversions need exchange rates from a provider. The JSR 354 Money and Currency API with its reference implementation Moneta adds money types and conversion providers as a library.

5.5. Is NumberFormat thread-safe?

No. A NumberFormat keeps state while it formats, so we create one per thread or per call, or keep one in a ThreadLocal. The Currency instances are immutable and safe to share.

6. Conclusion

We format money with NumberFormat.getCurrencyInstance(locale) and a BigDecimal amount. The locale decides the symbol, the separators and the decimals, setCurrency() shows another currency in the same layout, and we copy the currency’s decimals when we switch.

For input, parse() with a ParsePosition rejects partial matches, and setParseBigDecimal(true) keeps the exact value. Rounding defaults to HALF_EVEN, and the formatter never converts between currencies. Related locale guides, such as Localized Date Time Formats, are linked from our Java date and time guides.

7. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi Lokesh,

    The Code which you provided is not working for Finland , India, Switzerland and lot many countries. Can i know is there any thing such that only some countries can will work

  2. Hey, Thanks for such a good article. I am looking something like Custom Formatter available in .NET, like so exist in Java ? I am looking for totally custom formatter. If you know anything like this then please tell me.

  3. Hi.
    I don’t know it is a good solution use currency in long life application. What if the currency of this state will change in future and you have no chance change the JDK of your application? (For example you app is deployed and customer can’t pay you for changes) Customers will expect actual currency and your application will depend on JDK version.

    • Thanks for sharing your thoughts. Further, I believe that no application is life long application, at least history teaches us so. An application without upgrade for so long time just seems too much hypothetical me. And changing the currency of any nation is also not frequent event. It happen perhaps once in one (or more) decade(s).

      According to Java docs, Users can supersede the Java runtime currency data by creating a properties file named /lib/currency.properties. The contents of the properties file are key/value pairs of the ISO 3166 country codes and the ISO 4217 currency data respectively. The value part consists of three ISO 4217 values of a currency, i.e., an alphabetic code, a numeric code, and a minor unit. Those three ISO 4217 values are separated by commas.

      Ref: https://docs.oracle.com/javase/7/docs/api/java/util/Currency.html

      So, even if currency is changed to another currency then also we have some easy control to handle the scenario by changing a property file, which seems good enough for me.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.