PHP 8.0: Locale-independent float
to string
casting
PHP 8.0 changes the way PHP coerces float
values to string
.
Prior to PHP 8.0, float
to string
conversions depended on the current locale set with the setlocale
function. setlocale
function is not thread-safe, which means setting locale will affect all threads in a PHP process.
setlocale(LC_ALL, "fr_FR");
echo 1.618; // 1,618
// ^^^ comma
PHP does not inherit the system locale by default, but calling setlocale
without a specific locale makes it inherit the system locale. If setlocale()
is called without an explicit locale set (e.g. setlocale(LC_ALL, null)
, PHP will take the system locale from LC_ALL
environment variable. System locale can be obtained by running locale
in terminal.
Both comma (,
) and period (.
) symbols are accepted as a valid decimal separator throughout the world, and there is no international standard that prefers one over another. However, PHP can only accept the period notation for floats:
$val = 1,618;
// PHP Parse error: syntax error, unexpected ',' in ... on line ...
PDO
extension, and functions such as var_export
, json_encode
and serialize
try to minimize the locale-dependence with special handling for float values.
In PHP 8, all string
to float
conversions will be locale-independent. This means even if PHP is running under a different locale that prefers periods for the decimal separator, PHP will not use that locale when float values are presented.
setlocale(LC_ALL, "de_DE");
echo 1.618; // 1.618
// ^^^ period
printf
functions and specifiers
printf
-line of functions provide several modifiers to format and convert data formats.
*printf
specifiers will continue to use the %f
specifier in a locale-dependent way. It is already noted in the documentation that %f
output is locale-aware.
For locale-independent float formatting, use %F
specifier.
setlocale(LC_ALL, "fr_FR");
printf("%f", 1.618); // 1,618000
printf("%.3f", 1.618); // 1,618
printf("%F", 1.618); // 1.618000
printf("%.3F", 1.618); // 1.618
In PHP 8, using the string specifier (%s
) will have the locale-independent float to string effect.
setlocale(LC_ALL, "fr_FR");
// PHP < 8.0
printf("%s", 1.618); // 1,618
// PHP >= 8.0
printf("%s", 1.618); // 1.618
Further, %g
and %G
specifiers are locale-aware, and continue to be so in PHP >= 8.0 as well. There are new %h
and %H
specifiers in PHP 8.0, that provide the same functionality as %g
and %G
specifiers, but in a locale-independent way.
setlocale(LC_ALL, "fr_FR");
// All PHP versions
printf("%g", 1.618); // 1,618
printf("%G", 1.618); // 1,618
// New in PHP 8.0
printf("%h", 1.618); // 1.618
printf("%H", 1.618); // 1.618
PHP < 8.0 | PHP >= 8.0 | |
---|---|---|
%s (string) |
Locale-aware | Locale-independent |
%f (float) |
Locale-aware | Locale-aware |
%F (float) |
Locale-independent | Locale-independent |
%g (general) |
Locale-aware | Locale-aware |
%G (general) |
Locale-aware | Locale-aware |
%h (general) |
- | New, Locale-independent |
%H (general) |
- | New, Locale-independent |
Locale-aware float to string coercion
To make a locale-aware float
to string
coercion, it is still possible to use the sprintf
function with %f
or %g
specifiers. These specifiers are in locale-aware in all PHP versions.
setlocale(LC_ALL, "fr_FR");
- echo (string) 1.618; // 1.618
+ echo sprintf("%g", 1.618); // 1,618
Note that setlocale
function can be the root of side-effects due to the way it works; the locale is set per-process, and not per-thread, which opens up the possibility of one script/request setting the locale, and having that effect in other threads in the same process.
An ideal approach would be to use the intl
extension and its format helpers which are arguably much more flexible, easy to test, and use with different locales simultaneously.
$formatter = new \NumberFormatter("fr_FR", \NumberFormatter::DECIMAL);
echo $formatter->format(1.618); // 1,618
intl
NumberFormatter
performance 100K iterations of theNumberFormatter::format
method took0.012 sec
, compared to0.004 sec
forsprintf
. While it is a 3x performance difference, it is an absolutely minimal performance impact that wouldn't even register.
Backwards Compatibility Impact
This change can cause backwards-compatibility issues, which can be difficult to spot at once.
When strict types are not enabled, PHP silently converts float and string types back and forth when cast, or when used in parameter, property, or return types. Applications with strict types enabled would have already raised on errors when type silent coercion occur; For the rest this change will silently treat float-strings as locale-independent.
If you use setlocale
function to set a locale, and relied on this PHP behavior to automatically format float values, it will now be necessary to explicitly format them to a locale-aware format.
Related Changes
- PHP 7.4: Underscore numeric separator
- Union Types
- New
Stringable
interface - New
mixed
pseudo type - New
%h
and%H
printf
specifiers - New
*
precision and width modifiers inprintf