rfc:integer-rounding

This is an old revision of the document!


PHP RFC: Rounding Integers as int

Introduction

PHP contains several build-in rounding functions round, ceil, floor and number_format.

Except number_format all of these functions take a float|int, do a cast to float, process on floating point number only and finally return a float.

As a result of round, ceil and floor you get a floating point number rounded to your needs.

In most cases this is sufficient but in cases of handling integer values above 2^53 you start to end up with unexpected results due to floating point arithmetic and precision loss.

number_format on the other hand produces human readable numbers as string and since PHP 8.3 it also performs rounding integers without casting it to floating point numbers to fix the above issue mentioned.

Proposal

This RFC proposes to perform rounding on given int and return the resulting int if possible by default. In case of integer under-/overflow the value will be casted to a float (double) and rounded based on the floating point number as it's done currently.

For ceil, floor and round with precision >= 0 this means a given int gets returned as is.

An additional argument will be introduced bool force_float. In PHP 8.next this will default to force_float=true to keep current behavior but the new behavior can already be used by passing force_float=false. In PHP 9.0 the default will change to force_float=true to get the new behavior by default but the previous behavior can be forced with force_float=true.

The behavior will be as follows:

// No change, rounding floating point number will keep processing and returning float as it does now
ceil(float, force_float=false): float
ceil(float, force_float=true): float
floor(float, force_float=false): float
floor(float, force_float=true): float
round(float, force_float=false): float
round(float, force_float=true): float

// rounding integers with force_float=false will process and return int if possible
ceil(int, force_float=false): int
ceil(int, force_float=true): float
floor(int, force_float=false): int
floor(int, force_float=true): float
round(int, precision: >= 0, force_float=false): int
round(int, precision: < 0, force_float=false): int|float // implicit cast to float only in case of integer under-/overflow
round(int, force_float=true): float

This will result in less implicit casts and more precise rounding of integers above 2^53.

The int-type is compatible to float, returning an int instead of a float will still be a number accepted for type-hints of int, float, int|float or mixed with strict_types enabled.

As a result PHP will behave more precise on rounding integer values.

Examples

function takeFloat(float $v) { return $v; }
function returnFloat($v): float { return $v; }
 
var_dump(round(1));
var_dump(round(1.0));
var_dump(round(987654321098765432, precision: -3));
var_dump(round(987654321098765432, precision: -4)); // integer overflow
var_dump(takeFloat(round(987654321098765432, precision: -3)));
var_dump(returnFloat(round(987654321098765432, precision: -3)));
 
echo "\n#########################\n";
 
var_dump(round(1, force_float=true));
var_dump(round(1.0, force_float=true));
var_dump(round(987654321098765432, precision: -3, force_float=true));
var_dump(round(987654321098765432, precision: -4, force_float=true)); // integer overflow
var_dump(takeFloat(round(987654321098765432, precision: -3, force_float=true)));
var_dump(returnFloat(round(987654321098765432, precision: -3, force_float=true)));
 
echo "\n#########################\n";
 
var_dump(round(1, force_float=false));
var_dump(round(1.0, force_float=false));
var_dump(round(987654321098765432, precision: -3, force_float=false));
var_dump(round(987654321098765432, precision: -4, force_float=false)); // integer overflow
var_dump(takeFloat(round(987654321098765432, precision: -3, force_float=false)));
var_dump(returnFloat(round(987654321098765432, precision: -3, force_float=false)));

8.3 behavior (current):

float(1)
float(1)
float(9.87654321098766E+17)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)
#########################
Unknown named parameter $force_float

8.4 behavior (no change by default but possible to opt-in to new behavior):

float(1)
float(1)
float(9.87654321098766E+17)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)
#########################
float(1)
float(1)
float(9.87654321098766E+17)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)
#########################
int(1)
float(1)
int(987654321098765000)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)

9.0 behavior (Changed default behavior):

int(1)
float(1)
int(987654321098765000)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)
#########################
float(1)
float(1)
float(9.87654321098766E+17)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)
#########################
int(1)
float(1)
int(987654321098765000)
float(9.8765432109877E+17)
float(9.87654321098766E+17)
float(9.87654321098766E+17)

Backward Incompatible Changes

A BC break happens on type reporting functions like is_float, get_debug_type, gettype and strict comparison === will now report an int on rounding integers by default.

The old behavior can be forced by explicitly passing `force_float=true` argument or by explicitly casting the result back to float (float)round($num).

Proposed PHP Version(s)

  • Introduce new argument force_float=true in PHP 8.next
  • Change default to force_float=false as described in PHP 9.

RFC Impact

To SAPIs

none

To Existing Extensions

none

To Opcache

none

New Constants

none

php.ini Defaults

none

Open Issues

Make sure there are no open issues when the vote starts!

Unaffected PHP Functionality

Rounding floating point numbers will not be effected in any way.

Future Scope

After PHP 9 it can be considered to deprecate and remove the `force_float` argument again but due to very long future this is not part of this RFC.

Proposed Voting Choices

One primary vote (requires 2/3 majority): Round on int and return int?

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References

Rejected Features

Add new functions

PHP is a loosely typed langue and as such it's extremely uncommon to have to call different rounding functions for rounding int vs. float. Also the normal round already does the trick and mostly you won't know better until you get bitten by floating point.

rfc/integer-rounding.1709967294.txt.gz · Last modified: 2024/03/09 06:54 by mabe