PHP 8.0: Locale-independent float to string casting

Version8.0
TypeChange

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 the NumberFormatter::format method took 0.012 sec, compared to 0.004 sec for sprintf. 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


RFC Discussion Implementation