====== PHP RFC: Rounding Integers as int ====== * Version: 0.2 * Date: 2023-09-26 * Author: Marc Bennewitz, php@mabe.berlin * Status: Declined * First Published at: https://wiki.php.net/RFC/integer-rounding ===== Introduction ===== PHP contains several build-in rounding functions [[https://www.php.net/round|round]], [[https://www.php.net/ceil|ceil]], [[https://www.php.net/floor|floor]] and [[https://www.php.net/number_format|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'' the resulting value will be 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 a rounded int value if possible by default. In case of integer under-/overflow the value will be cast 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. ---- Rounding a given float will perform rounding on floating point number directly and return float as it's done currently. There will be no implicit cast to int because floats (double) can represent a much wider range of numbers than int (32 or 64 bit). Floating point numbers on the other hand gets imprecise on representing numbers > 2^53 but passing a float to be rounded such imprecision must be known in first place already. ---- 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=false'' 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 precisely 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(round(10000000000000055296)); // input is float 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(round(10000000000000055296, force_float=true)); // input is float 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(round(10000000000000055296, force_float=false)); // input is float 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(1.0000000000000055E+19) 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(1.0000000000000055E+19) float(9.87654321098766E+17) float(9.87654321098766E+17) ######################### float(1) float(1) float(9.87654321098766E+17) float(9.8765432109877E+17) float(1.0000000000000055E+19) float(9.87654321098766E+17) float(9.87654321098766E+17) ######################### int(1) float(1) int(987654321098765000) float(9.8765432109877E+17) float(1.0000000000000055E+19) 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(1.0000000000000055E+19) float(9.87654321098766E+17) float(9.87654321098766E+17) ######################### float(1) float(1) float(9.87654321098766E+17) float(9.8765432109877E+17) float(1.0000000000000055E+19) float(9.87654321098766E+17) float(9.87654321098766E+17) ######################### int(1) float(1) int(987654321098765000) float(9.8765432109877E+17) float(1.0000000000000055E+19) 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 affected 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 ===== As per the voting RFC a yes/no vote with a 2/3 majority is needed for this proposal to be accepted. Voting started on 2024-03-17 and will end on 2024-04-02 00:00 GMT. * Yes * No ===== Patches and Tests ===== * Branch: https://github.com/marc-mabe/php-src/tree/integer_rounding * PR: https://github.com/php/php-src/pull/12301 ===== Implementation ===== After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) ===== References ===== * https://externals.io/message/120373 * https://externals.io/message/121147 (RFC discussion) ===== 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.