Number Format

Su Suriya Ravichandran Updated 14 Nov 2026
6 min read ·Lesson 112 of 133

Showing a number to a person involves more than converting it to text. The decimal separator, the grouping separator, the currency symbol and its position, and the number of decimal places all depend on the user's language and country. java.text.NumberFormat and its subclass DecimalFormat handle these conventions: they turn numbers into correctly formatted text for a given locale, and parse such text back into numbers.

Why Locale Matters

The same value is written differently around the world:

LocaleNumberCurrencyPercent
en-US1,234,567.891$1,234,567.8918%
en-IN1,234,567.891₹1,234,567.8918%
de-DE1.234.567,8911.234.567,89 €18 %
fr-FR1 234 567,8911 234 567,89 €18 %
ja-JP1,234,567.891¥1,234,56818%

A German reader sees 1.234 as one thousand two hundred thirty-four, not as one point two three four. Japanese yen has no minor unit, so the currency format shows no decimals. French uses a narrow non-breaking space to group digits. Hard-coding "," and "." produces output that is wrong, and sometimes dangerously misleading, for many users.

Getting a NumberFormat

NumberFormat provides factory methods for common styles:

import java.text.NumberFormat;
import java.util.Locale;

Locale india = Locale.forLanguageTag("en-IN");

NumberFormat number = NumberFormat.getNumberInstance(india);        // general numbers
NumberFormat currency = NumberFormat.getCurrencyInstance(india);    // money
NumberFormat percent = NumberFormat.getPercentInstance(india);      // percentages
NumberFormat integer = NumberFormat.getIntegerInstance(india);      // whole numbers, rounded

currency.format(1499.5);       // "₹1,499.50"
percent.format(0.175);         // "18%": the value is multiplied by 100

Without a locale argument, each method uses the JVM's default locale, which varies between machines. Pass an explicit Locale for predictable output.

Locales are created with Locale.forLanguageTag("en-IN"), Locale.of("en", "IN") (Java 19+), or constants such as Locale.US and Locale.GERMANY. In a web application, the locale usually comes from the user's profile or the browser's Accept-Language header.

Controlling decimals and rounding

NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
nf.setMinimumFractionDigits(2);
nf.setMaximumFractionDigits(2);
nf.setRoundingMode(RoundingMode.HALF_UP);
nf.format(1234.5);        // "1,234.50"
nf.setGroupingUsed(false);
nf.format(1234.5);        // "1234.50"

The default rounding mode is HALF_EVEN. When formatting double values, rounding applies to the exact binary value, so a double written as 2.345 (actually slightly less) may round differently from what you expect. Format BigDecimal values for exact results.

Currency Formatting

NumberFormat inr = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("en-IN"));
inr.getCurrency();                 // INR
inr.getMaximumFractionDigits();    // 2

NumberFormat usd = NumberFormat.getCurrencyInstance(Locale.US);
usd.format(new BigDecimal("12499.50"));    // "$12,499.50"

The locale determines both the formatting conventions and the default currency. To show a specific currency in a particular locale's style, set it explicitly: fmt.setCurrency(Currency.getInstance("EUR")).

NumberFormat accepts BigDecimal and BigInteger as well as primitives. For money, store and calculate with BigDecimal and format only at the point of display.

Indian lakh and crore grouping

Indian numbering groups the last three digits and then every two digits: 23,45,678.90 (twenty-three lakh). Java's DecimalFormat supports only a single, uniform grouping size, so even with the en-IN locale (and even with the pattern #,##,##0.00), the standard JDK prints 2,345,678.90. Applications that must display lakh/crore grouping use the ICU4J library or a small helper method, as in the practical example below.

Compact Number Formatting

Dashboards and social media counters often show 12K or 3M instead of full numbers. getCompactNumberInstance (Java 12+) provides this:

NumberFormat shortFmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
shortFmt.format(1250);             // "1K"
shortFmt.format(3_400_000);        // "3M"
shortFmt.format(7_200_000_000L);   // "7B"

NumberFormat longFmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
longFmt.format(3_400_000);         // "3 million"

By default, compact formats round to whole numbers.

DecimalFormat Patterns

DecimalFormat lets you describe the format with a pattern:

import java.text.DecimalFormat;

new DecimalFormat("#,##0.00").format(1234.5);          // "1,234.50"
new DecimalFormat("#,##0.00").format(0.5);             // "0.50"
new DecimalFormat("000000").format(42);                // "000042"
new DecimalFormat("0.##").format(2.5);                 // "2.5"
new DecimalFormat("0.##").format(2.0);                 // "2"
new DecimalFormat("0.0E0").format(123456);             // "1.2E5"
new DecimalFormat("+#,##0;-#,##0").format(-1500);      // "-1,500"
new DecimalFormat("#,##0.00 'kg'").format(12.5);       // "12.50 kg"
SymbolMeaning
0A digit, shown even if zero
#A digit, omitted if it is a leading or trailing zero
,Grouping separator position
.Decimal separator
%Multiply by 100 and show a percent sign
EScientific notation
;Separates the positive and negative patterns
'text'Literal text

The symbols in a pattern are placeholders. The characters actually printed come from the locale's DecimalFormatSymbols, so #,##0.00 produces 1.234,50 with a German locale. Use new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(locale)) to choose them explicitly.

Parsing Formatted Text

parse converts locale-formatted text back into a Number:

NumberFormat.getNumberInstance(Locale.GERMANY).parse("1.234,56");   // 1234.56
NumberFormat.getNumberInstance(Locale.US).parse("abc");             // ParseException: Unparseable number: "abc"
NumberFormat.getNumberInstance(Locale.US).parse("1,234.56abc");     // 1234.56: trailing text ignored!

parse stops at the first character it does not understand and ignores the rest. To reject input with trailing garbage, use the parse(String, ParsePosition) overload and check that the position reached the end of the string. For exact decimal results, call ((DecimalFormat) fmt).setParseBigDecimal(true).

Thread Safety

NumberFormat and DecimalFormat instances are not thread-safe: they hold internal state while formatting. Do not share one instance between threads, such as in a static field of a web controller. Create one per use, keep one per thread (ThreadLocal), or synchronize access. (String.format creates its own formatter each time and is safe to call from any thread.)

NumberFormat Versus String.format

String.format("%,.2f", x)NumberFormat
Locale-aware separatorsYes (with a Locale argument)Yes
Currency symbols and placementNoYes
Compact formats (12K)NoYes
Custom patternsLimitedDecimalFormat patterns
ParsingNoYes
Thread-safeYesNo

Use String.format for simple, fixed layouts, such as logs, reports, and machine-readable files with Locale.ROOT. Use NumberFormat for anything a user reads as money, percentages, or localized numbers.

Practical Example: Price Tags for Several Audiences

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.*;

public class PriceTags {
    static String indianGrouping(BigDecimal amount) {
        BigDecimal rounded = amount.setScale(2, RoundingMode.HALF_UP);
        String plain = rounded.abs().toPlainString();
        String whole = plain.substring(0, plain.indexOf('.'));
        String fraction = plain.substring(plain.indexOf('.'));
        StringBuilder sb = new StringBuilder();
        int len = whole.length();
        if (len <= 3) {
            sb.append(whole);
        } else {
            String head = whole.substring(0, len - 3);
            for (int i = 0; i < head.length(); i++) {
                if (i > 0 && (head.length() - i) % 2 == 0) sb.append(',');
                sb.append(head.charAt(i));
            }
            sb.append(',').append(whole.substring(len - 3));
        }
        return (rounded.signum() < 0 ? "-₹" : "₹") + sb + fraction;
    }

    public static void main(String[] args) {
        List<BigDecimal> prices = List.of(new BigDecimal("499"), new BigDecimal("12499.5"),
                new BigDecimal("150000"), new BigDecimal("2345678.904"), new BigDecimal("-75000"));

        NumberFormat us = NumberFormat.getCurrencyInstance(Locale.US);
        NumberFormat inr = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("en-IN"));
        NumberFormat compact = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);

        System.out.printf("%-16s %-16s %-16s %s%n", "Indian grouping", "en-IN locale", "US dollars", "Compact");
        for (BigDecimal p : prices) {
            System.out.printf("%-16s %-16s %-16s %s%n", indianGrouping(p), inr.format(p), us.format(p), compact.format(p));
        }
    }
}

Output:

Indian grouping  en-IN locale     US dollars       Compact
₹499.00          ₹499.00          $499.00          499
₹12,499.50       ₹12,499.50       $12,499.50       12K
₹1,50,000.00     ₹150,000.00      $150,000.00      150K
₹23,45,678.90    ₹2,345,678.90    $2,345,678.90    2M
-₹75,000.00      -₹75,000.00      -$75,000.00      -75K

The second and third columns come straight from NumberFormat.getCurrencyInstance, with the symbol, grouping, and two decimal places chosen by the locale. The compact column suits small UI spaces, such as a sales badge. The first column comes from indianGrouping, which fills the gap described above:

  • It rounds with BigDecimal to exactly two decimal places.
  • It groups the last three digits of the whole part, then every two digits before them, producing 1,50,000.00 (one lakh fifty thousand) and 23,45,678.90.
  • It handles the sign separately so that negative amounts read -₹75,000.00.

For production systems that need lakh/crore formatting along with full localization, the ICU4J library supports Indian grouping directly.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Number Format and get answers drawn from it.

Signed-in readers only.