PHPNumber Formatting

Number Formatting

A raw PHP number and a number a human wants to read are two different things. 1234567.5 is correct as a value, but nobody wants to see it that way on an invoice or a dashboard — they want 1,234,567.50, or in Germany 1.234.567,50. PHP separates these concerns cleanly: number_format() handles the common "add separators and fix decimal places" case with zero setup, while the intl extension's NumberFormatter handles the harder problem of formatting correctly for a specific locale, including currency symbols and locale-specific separator conventions.

number_format() for the common case

number_format() takes a number and up to three extra arguments: how many decimal places to show, what character to use as the decimal point, and what character to use as the thousands separator. With no extra arguments it defaults to zero decimals and US-style comma separators.

number_format basics

PHP
<?php

echo number_format(1234567.891) . PHP_EOL;         // 1,234,568 (rounds, 0 decimals default)
echo number_format(1234567.891, 2) . PHP_EOL;       // 1,234,567.89
echo number_format(1234567.891, 2, ',', '.') . PHP_EOL; // 1.234.567,89 (German-style)
echo number_format(0.5) . PHP_EOL;                  // 1 (rounds half up)
1,234,568
1,234,567.89
1.234.567,89
1
number_format() rounds — it doesn't truncate
`number_format(1234567.891, 2)` produced `1,234,567.89`, rounding the third decimal, not chopping it off. If you need truncation instead of rounding, handle that separately (for example with `bcmath`'s fixed-scale operations) before formatting for display.
Manually swapping separators for one locale isn't localization

Passing custom separator characters to number_format(), as shown above, gets you the right punctuation for one specific locale, but it's a hardcoded guess — it doesn't know the user's actual locale, doesn't handle currency symbols or their placement, and doesn't adapt to right-to-left number systems or non-Latin digit sets used in some locales. For real internationalization, use the intl extension's NumberFormatter, which is aware of proper locale conventions.

Locale-aware formatting with NumberFormatter

PHP
<?php

$number = 1234567.891;

$us = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
echo $us->format($number) . PHP_EOL;   // 1,234,567.891

$de = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
echo $de->format($number) . PHP_EOL;   // 1.234.567,891

$in = new NumberFormatter('en_IN', NumberFormatter::DECIMAL);
echo $in->format($number) . PHP_EOL;   // 12,34,567.891 (Indian digit grouping)
1,234,567.891
1.234.567,891
12,34,567.891
NumberFormatter requires the intl extension
`NumberFormatter` lives in the `intl` extension, which ships with PHP but must be explicitly enabled (`extension=intl` in `php.ini`). Check `extension_loaded('intl')` if you're not sure it's available in your environment before relying on it.
Currency formatting

Currency is more than decimal places and separators — the symbol, its position (before or after the number), and even the number of decimal places vary by currency and locale (Japanese yen typically shows zero decimal places, for example). NumberFormatter's CURRENCY style handles all of that for you, given a locale and an ISO 4217 currency code.

Currency-aware formatting

PHP
<?php

$amount = 1234.5;

$usFormatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
echo $usFormatter->formatCurrency($amount, 'USD') . PHP_EOL; // $1,234.50

$deFormatter = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $deFormatter->formatCurrency($amount, 'EUR') . PHP_EOL; // 1.234,50 €

$jpFormatter = new NumberFormatter('ja_JP', NumberFormatter::CURRENCY);
echo $jpFormatter->formatCurrency($amount, 'JPY') . PHP_EOL; // ¥1,235 (yen has no decimals)
$1,234.50
1.234,50 €
¥1,235

Notice the yen example rounded 1234.5 up to 1,235 with no decimal places at all — that's the correct convention for that currency, and it's exactly the kind of detail that's easy to get wrong when you hand-roll formatting with string concatenation instead of delegating it to NumberFormatter.

Rounding modes

Both round() and NumberFormatter support alternative rounding strategies beyond "round half away from zero," which matters for financial or scientific contexts with specific rounding requirements. round() accepts a mode constant as its third argument.

Rounding modes with round()

PHP
<?php

echo round(2.5, 0, PHP_ROUND_HALF_UP) . PHP_EOL;   // 3 (default behavior)
echo round(2.5, 0, PHP_ROUND_HALF_DOWN) . PHP_EOL; // 2
echo round(2.5, 0, PHP_ROUND_HALF_EVEN) . PHP_EOL; // 2 ("banker's rounding")
echo round(3.5, 0, PHP_ROUND_HALF_EVEN) . PHP_EOL; // 4 (rounds to the nearest even)
3
2
2
4

PHP_ROUND_HALF_EVEN — banker's rounding — rounds a tied value (exactly halfway between two numbers) toward whichever neighbor is even. It's used in some accounting and statistical contexts because it avoids a consistent upward bias when rounding many values.

Tool

Best for

number_format()

Quick, fixed-format output when you already know the exact separators you want

NumberFormatter::DECIMAL

Locale-correct plain number formatting

NumberFormatter::CURRENCY

Locale- and currency-correct formatting with symbols

round() with a mode constant

Controlling tie-breaking behavior for calculations, not just display

  • number_format() rounds (it does not truncate) and always needs explicit separator arguments for non-US conventions.

  • NumberFormatter (from the intl extension) is locale-aware and handles grouping, decimals, and currency symbols correctly per locale.

  • Currencies differ in decimal places (yen has none) — let NumberFormatter::CURRENCY handle that rather than hardcoding two decimals everywhere.

  • round() supports rounding modes beyond the default "half away from zero," including banker's rounding via PHP_ROUND_HALF_EVEN.

Tip
Keep formatting at the very edge of your application — format numbers only when you're about to display them, and keep every calculation upstream working with raw `int`/`float`/`bcmath` values. Formatting a number early and then trying to parse it back out of a string like `"$1,234.50"` for further math is a common, avoidable source of bugs.