This RFC proposes the addition of two new functions to PHP's standard library: is_representable_as_float()
and
is_representable_as_int()
.
These functions provide developers with a standardized way to check whether values can be losslessly converted between integer and floating-point representations. Among other things, this is particularly useful for:
Here is an example of a real use case: when serializing data, it is currently challenging to determine whether a numeric value can be safely represented as a float or an integer without loss of precision:
<?php // 9007199254740993 is NOT representable as a float // currently, we can use is_numeric() to check if a value is numeric // but this does not guarantee that the value can be represented as a float or an int $data = ['price' => 9007199254740993]; if (/* how do I make sure the price can be represented in JSON? */) { // handle error, e.g., log it or throw an exception // or cast to string } // with this RFC $data = ['price' => 9007199254740993]; if (!is_representable_as_float($data['price'])) { $data['price'] = (string) $data['price']; } ?>
Add the following two functions:
function is_representable_as_float(mixed $value): bool
function is_representable_as_int(mixed $value): bool
When does is_representable_as_float() return true?
About the loss of precision: it is important to know that IEEE 754 floating-point numbers can represent integers exactly up to 2^53 - 1 (inclusive) because the mantissa has 52 bits of precision plus an implicit leading bit. Therefore, any integer within this range can be represented as a float without loss of precision. An integer outside this range could lose precision when converted to a float. This is called the “safe integer range” for floats. MDN provides a good overview of this topic[1].
IEEE 754 floating-point numbers can represent all consecutive integers exactly up to 2^53 - 1 (inclusive). Beyond this range, only specific integers can be represented exactly, following a pattern based on the available mantissa bits:
Additionally, pure powers of 2 (2^0, 2^1, 2^2, ..., up to 2^1023) can always be represented exactly because they require only a single bit in the mantissa.
The function returns true for any value that can be exactly represented as a float, including these special cases.
<?php is_representable_as_float(2**53); // true is_representable_as_float(2**53 + 1); // false, precision loss when cast to float is_representable_as_float(42); // true is_representable_as_float("123.456"); // true is_representable_as_float("1e308"); // false, not exactly representable as float is_representable_as_float("1e400"); // false // beyond safe range is_representable_as_float(2**54); // true (exact power of 2) is_representable_as_float(2**54 + 2); // true (multiple of 2 in this range) is_representable_as_float(2**54 + 1); // false (odd number, not representable) // large pure powers of 2 is_representable_as_float(2**200); // true (exact power of 2) is_representable_as_float(2**1023); // true (largest normal float) is_representable_as_float(2**1024); // true (overflow to infinity) ?>
When does is_representable_as_int() return true?
The function behavior depends on the platform's integer range:
is_representable_as_int(2.0**31); // true on 64-bit, false on 32-bit platforms is_representable_as_int('2147483648'); // true on 64-bit, false on 32-bit platforms
More examples of the function return values:
is_representable_as_int(42); // true is_representable_as_int(3.14); // false, has a fractional part is_representable_as_int("123"); // true is_representable_as_int("123.456"); // false, has a fractional part is_representable_as_int("1e10"); // true on 64-bit platforms, false on 32-bit platforms is_representable_as_int("1e400"); // false, not exactly representable as int
It's still unsure if the current naming of functions are a prefect fit. Here are a few other propositions to name the new functions:
is_safe_float()
/ is_safe_int()
fits_float()
/ fits_int()
can_cast_to_float()
/ can_cast_to_int()
<?php // validate before serialization $data = ['price' => 9007199254740993]; if (!is_representable_as_float($data['price'])) { // handle error, e.g., log it or throw an exception // or cast to string! $data['price'] = (string) $data['price']; } // validate data from external API function validateNumericInput($value) { if (is_numeric($value) && is_representable_as_int($value)) { return (int) $value; } // ... } // edge cases is_representable_as_float(INF); // true is_representable_as_float(-INF); // true is_representable_as_float(NAN); // true is_representable_as_float(""); // false is_representable_as_float(null); // false is_representable_as_float(false); // false is_representable_as_float(true); // false is_representable_as_float(" 42 "); // true, like "is_numeric()" is_representable_as_float("\r42\n"); // true, like "is_numeric()" is_representable_as_float("0x2A"); // false, like "is_numeric()" is_representable_as_float("0b01110"); // false, like "is_numeric()" is_representable_as_int(2.0); // true is_representable_as_int(-0.0); // true class MyClass { public function __toString() { return "42"; } } is_representable_as_int(new MyClass()); // true because of __toString() is_representable_as_float(new MyClass()); // true as well ?>
Note: these functions use locale-independent parsing, similar to is_numeric()
.
Decimal separator is always “.” regardless of locale settings.
Existing code that declares a function with the same name will result in an error. However, this is unlikely to happen. A quick search on GitHub with the function names shows no results.
Next minor version, likely PHP 8.6
The pull request is available here: https://github.com/php/php-src/pull/19308