[RFC] [Discussion] Rounding Integers as int

php.internals

Marc

2 years ago
Hi internals I'd like to put a new RFC under discussion: https://wiki.php.net/rfc/integer-rounding Best, Marc

Larry Garfield

2 years ago
On Tue, Sep 26, 2023, at 10:39 AM, Marc Bennewitz wrote:
> Hi internals > > I'd like to put a new RFC under discussion: > https://wiki.php.net/rfc/integer-rounding > > Best, > Marc
I'm honestly kind of confused by this. The basic concept, as I understand it, is sound. But the rest of the description seems to suggest that the returned type will depend on the passed-in type? That makes little sense to me. Since we already know that int is contravariant to float, I would expect round to return int if it can, float if there is a decimal precision set. ceil and floor should always return ints, unconditionally, because that's literally what their purpose is. But that RFC doesn't say that, which makes me quite confused. Similarly, the BC shim would therefore be (float)ceil($x), not ceil((float)$x), which... I don't think that would even do anything, would it? --Larry Garfield

Marc

2 years ago
Hi Larry, On 26.09.23 20:41, Larry Garfield wrote:
> On Tue, Sep 26, 2023, at 10:39 AM, Marc Bennewitz wrote: >> Hi internals >> >> I'd like to put a new RFC under discussion: >> https://wiki.php.net/rfc/integer-rounding >> >> Best, >> Marc > I'm honestly kind of confused by this. The basic concept, as I understand it, is sound. But the rest of the description seems to suggest that the returned type will depend on the passed-in type? That makes little sense to me. Since we already know that int is contravariant to float, I would expect round to return int if it can, float if there is a decimal precision set. ceil and floor should always return ints, unconditionally, because that's literally what their purpose is. But that RFC doesn't say that, which makes me quite confused. > > Similarly, the BC shim would therefore be (float)ceil($x), not ceil((float)$x), which... I don't think that would even do anything, would it?
Currently, all three functions return float, no matter what. This is an implicit cast and results in inaccurate values for numbers above 2^53. The RFC proposes to keep the given type if possible. Given that, the return value is (and was) guaranteed to be float if the given number is float. If the given number is int the return value will most likely be int except in case of integer over- / underflow. If instead all integers would be returned as int-type, we would have additional issues... * additional BC due to additional implicit cast from float to int * less accuracy on 32bit platforms (numbers > 2^31) * what to do on integer overflow?
> --Larry Garfield
Marc

Saki Takamachi

2 years ago
Hi, Marc. There is one additional case to consider. It's backwards compatible. If `strict_types` is not `1`, the code currently out there may be passing `int` to these functions. Therefore, it is better to be aware that such code can have destructive effects, and to consider whether or not to allow it. Regards. Saki

Marc

2 years ago
Hi Saki, On 27.09.23 02:18, Saki Takamachi wrote:
> Hi, Marc. > > There is one additional case to consider. > It's backwards compatible. > If `strict_types` is not `1`, the code currently out there may be passing `int` to these functions. > > Therefore, it is better to be aware that such code can have destructive effects, and to consider whether or not to allow it.
I'm not sure I understand. All of these functions already take an `int|float` and therefore the setting of `strict_types` is irrelevant here on passing float vs. int.

Saki Takamachi

2 years ago
Hi, Marc.
> All of these functions already take an `int|float` and therefore the setting of `strict_types` is irrelevant here on passing float vs. int.
Oops, excuse me, I completely misunderstood the signature. Saki

Derick Rethans

2 years ago
On Tue, 26 Sep 2023, Larry Garfield wrote:
> ceil and floor should > always return ints, unconditionally, because that's literally what > their purpose is. But that RFC doesn't say that, which makes me quite > confused.
They can't do that. ceil(2.4e102) can't be repesented as an integer.
> Similarly, the BC shim would therefore be (float)ceil($x), not > ceil((float)$x), which... I don't think that would even do anything, > would it?
(float) $int *could* also do something: $ php -dprecision=20 -r 'echo (float) 1152921504606846974;' 1152921504606846976 $ php -dprecision=20 -r 'var_dump(ceil((float) 1152921504606846974));' double(1152921504606846976) (see how the 74 changes into 76 at the end). Integers in the ranges LONG_MAX to 2^53, and -2^53 to LONG_MIN can't be represented as whole numbers as float, and some precision is lost. cheers, Derick

Tim Düsterhus

2 years ago
Hi On 9/26/23 12:39, Marc Bennewitz wrote:
> I'd like to put a new RFC under discussion: > https://wiki.php.net/rfc/integer-rounding
I find the proposal reasonable, but I don't like the deprecation proposal, because it does not allow me to opt into the *new* behavior before PHP 9.0 arrives. Getting rid of the deprecation would also require me to either add a cast everywhere or add the error suppression operator everywhere and then revert that with PHP 9 depending on whether I want the new behavior or not. As far as I can tell all the other deprecations allow me to use the replacement functionality right away without needing to go through a two-step process. As the results of not casting to float first are arguably more correct, perhaps this could be considered a bugfix. While it would cause breakage for users that rely on strict types, obtaining the old behavior is just one `(float)` cast away. Best regards Tim Düsterhus

Marc

2 years ago
Hi Tim, On 29.09.23 17:28, Tim Düsterhus wrote:
> Hi > > On 9/26/23 12:39, Marc Bennewitz wrote: >> I'd like to put a new RFC under discussion: >> https://wiki.php.net/rfc/integer-rounding > > I find the proposal reasonable, but I don't like the deprecation > proposal, because it does not allow me to opt into the *new* behavior > before PHP 9.0 arrives. > > Getting rid of the deprecation would also require me to either add a > cast everywhere or add the error suppression operator everywhere and > then revert that with PHP 9 depending on whether I want the new > behavior or not. > > As far as I can tell all the other deprecations allow me to use the > replacement functionality right away without needing to go through a > two-step process. > > As the results of not casting to float first are arguably more > correct, perhaps this could be considered a bugfix. While it would > cause breakage for users that rely on strict types, obtaining the old > behavior is just one `(float)` cast away. >
The deprecation would act as an information for upcoming behavior change, not classical deprecation of future removal. If the new behavior is what you want, than you don't need to change anything as a deprecation message should not break anything. At the same time people relying on the old behavior of implicit cast gets informed that they have to cast explicit now. I first was playing around with adding a new argument. First by default keep existing behavior, in next changing default to new behavior and later again get rid of the argument. But this would force users to migrate multiple times and it would take years to finish. Changing the behavior without further notice (deprecation) this could be a huge BC ending up in a lot of uncaught bugs especially on such high frequently used functions.
> Best regards > Tim Düsterhus
Best, Marc

Tim Düsterhus

2 years ago
Hi On 9/30/23 08:26, Marc Bennewitz wrote:
> The deprecation would act as an information for upcoming behavior > change, not classical deprecation of future removal. If the new behavior > is what you want, than you don't need to change anything as a > deprecation message should not break anything. At the same time people
Correct, the deprecation message itself would not break anything. However when the new behavior is the behavior I desire, it would: 1. Cause noise in my logs, because the deprecation would continue to be emitted until the behavioral change is finally implemented. 2. Be confusing when working in preparing my application for future PHP versions by handling all deprecations. The only solutions would be either adding an '@' in front of every call to round, ceil, floor (which might suppress additional errors I *do* care about) or ignoring this specific deprecation within the error handler of the application after verifying that I checked all locations for correctness. There is no way for me to reliably mark only this specific deprecation as acknowledged / handled for a specific place within my code. This also extends to library developers who are already under pressure by users to make their libraries deprecation free on or before the release of a new PHP version. As the library doesn't control the error handler, they can't just ignore this specific deprecation for their library, without casting the input to float, despite being happy with the new behavior that will arrive in PHP 9. In other words, this deprecation is not really actionable to me as a developer, because I can't migrate to the new behavior on my own pace. Best regards Tim Düsterhus

Marc

2 years ago
Hi, On 03.10.23 14:41, Tim Düsterhus wrote:
> Hi > > On 9/30/23 08:26, Marc Bennewitz wrote: >> The deprecation would act as an information for upcoming behavior >> change, not classical deprecation of future removal. If the new behavior >> is what you want, than you don't need to change anything as a >> deprecation message should not break anything. At the same time people > > Correct, the deprecation message itself would not break anything. > However when the new behavior is the behavior I desire, it would: > > 1. Cause noise in my logs, because the deprecation would continue to > be emitted until the behavioral change is finally implemented. > 2. Be confusing when working in preparing my application for future > PHP versions by handling all deprecations. > > The only solutions would be either adding an '@' in front of every > call to round, ceil, floor (which might suppress additional errors I > *do* care about) or ignoring this specific deprecation within the > error handler of the application after verifying that I checked all > locations for correctness. There is no way for me to reliably mark > only this specific deprecation as acknowledged / handled for a > specific place within my code. > > This also extends to library developers who are already under pressure > by users to make their libraries deprecation free on or before the > release of a new PHP version. As the library doesn't control the error > handler, they can't just ignore this specific deprecation for their > library, without casting the input to float, despite being happy with > the new behavior that will arrive in PHP 9. > > In other words, this deprecation is not really actionable to me as a > developer, because I can't migrate to the new behavior on my own pace.
I do understand and I agree that it's not optimal but the issue is the lack of good alternatives. I only see three possible ways and all of them are suboptimal: 1. Don't trigger upcoming behavior change message (deprecation message) * impossible to opt-in new behavior in 8.4 * This could lead to different application behavior overlook on upgrade to 9.0 2. trigger upcoming behavior change message without possibility to opt-in new behavior (as proposed) * impossible to opt-in new behavior in 8.4 * either force old behavior or noise in logs 3. introduce new argument * possibility to opt-in new behavior in 8.4 * introducing a new argument which is already deprecated is just weird * forcing users to migrate twice (set argument in 8.4 and remove in 9.0 or 10.0) * very long migration phase in case of removing argument in 10.0 I think it would be great in this case to have a way of triggering an upcoming behavior change message even lower prio than a deprecation message but I don't see this being possible. As far as I understand you are in favor of option 1. (considering it a bugfix) but how do you make sure this will not lead to different application behavior overlooked on upgrading? Also there is no way to opt-in new behavior.
> Best regards > Tim Düsterhus
Best, Marc

Tim Düsterhus

2 years ago
Hi On 10/4/23 08:35, Marc Bennewitz wrote:
> As far as I understand you are in favor of option 1. (considering it a > bugfix) but how do you make sure this will not lead to different > application behavior overlooked on upgrading? Also there is no way to > opt-in new behavior. >
Yes, I would be in favor of option 1 and I don't even see a reason not to do this in PHP 8.4 right away. As I mentioned before, this effectively can be considered a bug fix: PHP does not correctly round integers > 2^53. While this is not a bug fix that is appropriate for the current stable branches, I don't see why it would not be appropriate for PHP 8.4 if there's an RFC for it. In fact there is already a rounding bugfix (no RFC for that) in PHP 8.4: https://github.com/php/php-src/blob/65a8c70f93ccb7e008de147cd4c357681c653bd0/UPGRADING#L75-L82 While PHP tries hard to keep backwards compatibility within a given major, keeping full compatibility is effectively impossible for the huge API surface PHP exposes. Every version includes *some* changes that are technically breaking for one reason or another. Best regards Tim Düsterhus

Marc

2 years ago
Hi Tim, On 05.10.23 21:57, Tim Düsterhus wrote:
> Hi > > On 10/4/23 08:35, Marc Bennewitz wrote: >> As far as I understand you are in favor of option 1. (considering it a >> bugfix) but how do you make sure this will not lead to different >> application behavior overlooked on upgrading? Also there is no way to >> opt-in new behavior. >> > > Yes, I would be in favor of option 1 and I don't even see a reason not > to do this in PHP 8.4 right away. As I mentioned before, this > effectively can be considered a bug fix: PHP does not correctly round > integers > 2^53. > > While this is not a bug fix that is appropriate for the current stable > branches, I don't see why it would not be appropriate for PHP 8.4 if > there's an RFC for it. > > In fact there is already a rounding bugfix (no RFC for that) in PHP 8.4: > > https://github.com/php/php-src/blob/65a8c70f93ccb7e008de147cd4c357681c653bd0/UPGRADING#L75-L82 > > > While PHP tries hard to keep backwards compatibility within a given > major, keeping full compatibility is effectively impossible for the > huge API surface PHP exposes. Every version includes *some* changes > that are technically breaking for one reason or another.
Fixing edge cases like having 0.49999999999999994 rounded down correctly is a different story than having something simple like "round(10 / 2) === 5.0" to return false instead.
> > Best regards > Tim Düsterhus >
Best, Marc

Jakub Zelenka

2 years ago
On Thu, Oct 5, 2023 at 8:57 PM Tim Düsterhus <tim@bastelstu.be> wrote:
> While this is not a bug fix that is appropriate for the current stable > branches, I don't see why it would not be appropriate for PHP 8.4 if > there's an RFC for it. > > In fact there is already a rounding bugfix (no RFC for that) in PHP 8.4: > > > https://github.com/php/php-src/blob/65a8c70f93ccb7e008de147cd4c357681c653bd0/UPGRADING#L75-L82 > >
This is however very different case as it is still following the currently voted rounding RFC so it is a more bug fix. Changing the return type of a heavily used function is a completely different matter. It will break for anyone using is_float or expecting the float in some other way. I realise that there is an implicit conversion so it's not such a huge problem most of the time but there might be case where it is an issue. I don't see it as a bug fix in any way. It's more just an implication of the return type being a float. I think we should always weight the implication of the break. The way that I see it that some small breaks are acceptable for minor version but bigger breaks should be done only in major version. I would consider this (and anything changing a return types) as a bigger break. Cheers Jakub

Marc

2 years ago
Hi, On 26.09.23 12:39, Marc Bennewitz wrote:
> I'd like to put a new RFC under discussion: > https://wiki.php.net/rfc/integer-rounding
I tried to make the RFC text a bit more clear. As I'm not a native English speaker I would be very welcome for wording suggestions and/or grammar fixes especially for the deprecation message. About the deprecation - as it's not a classical deprecation but a notice about upcoming behavior change. Would it make sense to have an additional vote, if or if not to trigger it in 8.next? Marc

Girgias

2 years ago
On Tue, 26 Sept 2023 at 11:39, Marc Bennewitz <marc@mabe.berlin> wrote:
> Hi internals > > I'd like to put a new RFC under discussion: > https://wiki.php.net/rfc/integer-rounding
I don't understand the point of the deprecation phase at all. Frankly, I consider this RFC a bug fix of the current "broken" implementation, which was broken with 7.0 and the move to 64bit integers. As such, I would just make the behaviour of returning integers the default behaviour in 8.4. If, for some weird reason, people want less accurate numbers, they can always cast the input to float. Best regards, George P. Banyard

Marc

2 years ago
On 05.10.23 01:38, G. P. B. wrote:
> On Tue, 26 Sept 2023 at 11:39, Marc Bennewitz<marc@mabe.berlin> wrote: > >> Hi internals >> >> I'd like to put a new RFC under discussion: >> https://wiki.php.net/rfc/integer-rounding > > I don't understand the point of the deprecation phase at all. > Frankly, I consider this RFC a bug fix of the current "broken" > implementation, which was broken with 7.0 and the move to 64bit integers. > As such, I would just make the behaviour of returning integers the default > behaviour in 8.4. > If, for some weird reason, people want less accurate numbers, they can > always cast the input to float.
I don't see a bug or broken behavior here as these functions were processing floating point numbers since .... forever. https://3v4l.org/PrrmO
> Best regards, > > George P. Banyard
Best, Marc

Girgias

2 years ago
On Thu, 5 Oct 2023 at 07:40, Marc Bennewitz <marc@mabe.berlin> wrote:
> I don't see a bug or broken behavior here as these functions were > processing floating point numbers since .... forever. > > https://3v4l.org/PrrmO >
That's not my point, the point is about the function being broken for large 64bit integers, which your RFC is fixing. I would also expect most people that actually use ceil/floor/round to dump the return value of it into a property/value that is expected to be an int. Which is also compatible for floats that fit in an int (and since 8.2 for those that have a fractional part a warning is emitted). I see no benefit in doing this weird approach instead of just changing the behaviour. Yes this is a theoretical BC break [1], but unless you can show me an actual use case that breaks (and even then) I don't think the approach of this RFC is good. Best regards, George P. Banyard [1] And this is coming from me, who does love finding weird edge cases and remove them from the language, just look at the 8.3 range() RFC

Marc

2 years ago
Hi George, On 05.10.23 14:12, G. P. B. wrote:
> On Thu, 5 Oct 2023 at 07:40, Marc Bennewitz<marc@mabe.berlin> wrote: > >> I don't see a bug or broken behavior here as these functions were >> processing floating point numbers since .... forever. >> >> https://3v4l.org/PrrmO >> > That's not my point, the point is about the function being broken for large > 64bit integers, which your RFC is fixing. > > I would also expect most people that actually use ceil/floor/round to dump > the return value of it into a property/value that is expected to be an int. > Which is also compatible for floats that fit in an int (and since 8.2 for > those that have a fractional part a warning is emitted). > > I see no benefit in doing this weird approach instead of just changing the > behaviour. > Yes this is a theoretical BC break [1], but unless you can show me an > actual use case that breaks (and even then) I don't think the approach of > this RFC is good.
Running a quick code search /round\([^\(]+\)\s===/ language:PHP on GitHub finds possible breakages based on strict comparison:     // if width and height are ints     $x = ( $height1 / $width1 ) * $width2;     if ( round( $x ) === round( $height2 ) ) {     // if number is int     public static function isZero($number, $precission = 2)     {         return round($number, $precission) === 0.0;     }     // where ecb is int     return $ecb && round(5000 / $ecb) === 2.0;

Jakub Zelenka

2 years ago
On Tue, Sep 26, 2023 at 11:39 AM Marc Bennewitz <marc@mabe.berlin> wrote:
> Hi internals > > I'd like to put a new RFC under discussion: > https://wiki.php.net/rfc/integer-rounding > >
I would personally prefer a new function for rounding integers if anyone wants to round large integers. The things is that the current round behaviour is consistent with a way how floats to int conversion is done for strict types. <?php declare(strict_types=1); function test(float $a) { echo $a; } test(987654321098765432); So it won't really help that much if this function returns long for long in cases where the result is passed to another function expecting float. The main problem that I see with the current approach is that it changes return type for an edge case that most people are not impacted with. For example it is quite usual that people had a float value with 0 fraction which gets json encode to int. When they decode it and use round, the return type is suddenly int. Even though it's usually not a problem, there might be some code that expects float and maybe even assert that with is_float. Such code will break. On the other hand I see use some case for rounding large integers with negative precision. But for that to work I think it would be better to have a special function. If you really want to make such change to round, then I would be prefer targeting just 9.0 without any deprecation as I don't think the deprecation should be informational only and not fixable in the code. Cheers Jakub

Robert Landers

2 years ago
On Fri, Oct 13, 2023 at 1:26 PM Jakub Zelenka <bukka@php.net> wrote:
> > On Tue, Sep 26, 2023 at 11:39 AM Marc Bennewitz <marc@mabe.berlin> wrote: > > > Hi internals > > > > I'd like to put a new RFC under discussion: > > https://wiki.php.net/rfc/integer-rounding > > > > > I would personally prefer a new function for rounding integers if anyone > wants to round large integers. > > The things is that the current round behaviour is consistent with a way how > floats to int conversion is done for strict types. > > <?php > declare(strict_types=1); > > function test(float $a) { > echo $a; > } > test(987654321098765432); > > > So it won't really help that much if this function returns long for long in > cases where the result is passed to another function expecting float. > > The main problem that I see with the current approach is that it changes > return type for an edge case that most people are not impacted with. For > example it is quite usual that people had a float value with 0 fraction > which gets json encode to int. When they decode it and use round, the > return type is suddenly int. Even though it's usually not a problem, there > might be some code that expects float and maybe even assert that with > is_float. Such code will break. > > On the other hand I see use some case for rounding large integers with > negative precision. But for that to work I think it would be better to have > a special function. > > If you really want to make such change to round, then I would be prefer > targeting just 9.0 without any deprecation as I don't think the deprecation > should be informational only and not fixable in the code. > > Cheers > > Jakub
Just to add some nuance, if you are going anywhere near the edges of ints (e.g., custom encryption prototypes from papers), you generally know that will happen -- it's math and pretty easy to verify. In those cases, you likely will be using GMP or some other extension to handle the large numbers. The point is, I highly doubt people are unknowingly using round() with exceptionally large numbers. If they are doing so, they probably know exactly what they are doing and already handle all potential edge cases or they use an extension specifically for dealing with large numbers.

Marc

2 years ago
Hi Robert, On 13.10.23 13:44, Robert Landers wrote:
> On Fri, Oct 13, 2023 at 1:26 PM Jakub Zelenka <bukka@php.net> wrote: >> On Tue, Sep 26, 2023 at 11:39 AM Marc Bennewitz <marc@mabe.berlin> wrote: >> >>> Hi internals >>> >>> I'd like to put a new RFC under discussion: >>> https://wiki.php.net/rfc/integer-rounding >>> >>> >> I would personally prefer a new function for rounding integers if anyone >> wants to round large integers. >> >> The things is that the current round behaviour is consistent with a way how >> floats to int conversion is done for strict types. >> >> <?php >> declare(strict_types=1); >> >> function test(float $a) { >> echo $a; >> } >> test(987654321098765432); >> >> >> So it won't really help that much if this function returns long for long in >> cases where the result is passed to another function expecting float. >> >> The main problem that I see with the current approach is that it changes >> return type for an edge case that most people are not impacted with. For >> example it is quite usual that people had a float value with 0 fraction >> which gets json encode to int. When they decode it and use round, the >> return type is suddenly int. Even though it's usually not a problem, there >> might be some code that expects float and maybe even assert that with >> is_float. Such code will break. >> >> On the other hand I see use some case for rounding large integers with >> negative precision. But for that to work I think it would be better to have >> a special function. >> >> If you really want to make such change to round, then I would be prefer >> targeting just 9.0 without any deprecation as I don't think the deprecation >> should be informational only and not fixable in the code. >> >> Cheers >> >> Jakub > Just to add some nuance, if you are going anywhere near the edges of > ints (e.g., custom encryption prototypes from papers), you generally > know that will happen -- it's math and pretty easy to verify. In those > cases, you likely will be using GMP or some other extension to handle > the large numbers. The point is, I highly doubt people are unknowingly > using round() with exceptionally large numbers. If they are doing so, > they probably know exactly what they are doing and already handle all > potential edge cases or they use an extension specifically for dealing > with large numbers.
This very much depends on where you define your edges. Basically we are talking about 2^53 to 2^63 which is 1024 times higher number. Forcing people into such extensions much earlier than necessary isn't very helpful either as they all come with it's own downsides as well. Best, Marc

Marc

2 years ago
Hi Jakub, On 13.10.23 13:25, Jakub Zelenka wrote:
> On Tue, Sep 26, 2023 at 11:39 AM Marc Bennewitz <marc@mabe.berlin> wrote: > >> Hi internals >> >> I'd like to put a new RFC under discussion: >> https://wiki.php.net/rfc/integer-rounding >> >> > I would personally prefer a new function for rounding integers if anyone > wants to round large integers. > > The things is that the current round behaviour is consistent with a way how > floats to int conversion is done for strict types. > > <?php > declare(strict_types=1); > > function test(float $a) { > echo $a; > } > test(987654321098765432); > > > So it won't really help that much if this function returns long for long in > cases where the result is passed to another function expecting float. > > The main problem that I see with the current approach is that it changes > return type for an edge case that most people are not impacted with. For > example it is quite usual that people had a float value with 0 fraction > which gets json encode to int. When they decode it and use round, the > return type is suddenly int. Even though it's usually not a problem, there > might be some code that expects float and maybe even assert that with > is_float. Such code will break. > > On the other hand I see use some case for rounding large integers with > negative precision. But for that to work I think it would be better to have > a special function.
Your JSON example is a bit unrelated because if you care about your types your should have used JSON_RESERVE_ZERO_FRACTION in the first place else you should not care about int vs float at all. It's true that passing/returning int to/from a function expecting float will cast but currently with these rounding functions it's a different deal as they expect an `int|float` instead of just `float`. So it's not cast on passing the argument but the functions itself are casting. Where another set of functions would avoid the BC break it also would be against having PHP as a loosely typed languageputting the burden to everyone caring. I already see hundreds of `is_int($v) ? round_int($v) : round($v)` everywhere around.
> If you really want to make such change to round, then I would be prefer > targeting just 9.0 without any deprecation as I don't think the deprecation > should be informational only and not fixable in the code. > > Cheers > > Jakub
Best, Marc

Jakub Zelenka

2 years ago
Hi Marc, On Sat, Oct 14, 2023 at 1:21 AM Marc Bennewitz <marc@mabe.berlin> wrote:
> Hi Jakub, > > On 13.10.23 13:25, Jakub Zelenka wrote: > > On Tue, Sep 26, 2023 at 11:39 AM Marc Bennewitz <marc@mabe.berlin> > wrote: > > > >> Hi internals > >> > >> I'd like to put a new RFC under discussion: > >> https://wiki.php.net/rfc/integer-rounding > >> > >> > > I would personally prefer a new function for rounding integers if anyone > > wants to round large integers. > > > > The things is that the current round behaviour is consistent with a way > how > > floats to int conversion is done for strict types. > > > > <?php > > declare(strict_types=1); > > > > function test(float $a) { > > echo $a; > > } > > test(987654321098765432); > > > > > > So it won't really help that much if this function returns long for long > in > > cases where the result is passed to another function expecting float. > > > > The main problem that I see with the current approach is that it changes > > return type for an edge case that most people are not impacted with. For > > example it is quite usual that people had a float value with 0 fraction > > which gets json encode to int. When they decode it and use round, the > > return type is suddenly int. Even though it's usually not a problem, > there > > might be some code that expects float and maybe even assert that with > > is_float. Such code will break. > > > > On the other hand I see use some case for rounding large integers with > > negative precision. But for that to work I think it would be better to > have > > a special function. > > Your JSON example is a bit unrelated because if you care about your > types your should have used JSON_RESERVE_ZERO_FRACTION in the first > place else you should not care about int vs float at all. > >
The thing is that JSON might be encoded by another application that doesn't care about types that much. In fact in JSON there is no difference between 2.0 and 2. So this is quite possible situation IMHO.
> It's true that passing/returning int to/from a function expecting float > will cast but currently with these rounding functions it's a different > deal as they expect an `int|float` instead of just `float`. So it's not > cast on passing the argument but the functions itself are casting. > >
Well internally yeah but effectively it is a cast on the return type. What I meant is that this wont help everywhere when it's passed to function expecting just float unless it is changed by user to accept int|float. Where another set of functions would avoid the BC break it also would be
> against having PHP as a loosely typed languageputting the burden to > everyone caring. I already see hundreds of `is_int($v) ? round_int($v) : > round($v)` everywhere around. > >
But for me it's sort of an edge as I would think it is not that usual rounding values that are greater than 2^53. I think it would be good to note in the RFC what other language do about this issue. I quickly checked Go and it seems to only have float64 rounding (Math.Round). Rust seems to also round just f64. I'm starting to feel that the problem is that the input type is defined as int|float. I think we should just correct it to float. Regards Jakub

Marc

2 years ago
Hi Jakub, On 16.10.23 00:17, Jakub Zelenka wrote:
>> Your JSON example is a bit unrelated because if you care about your >> types your should have used JSON_RESERVE_ZERO_FRACTION in the first >> place else you should not care about int vs float at all. >> >> > The thing is that JSON might be encoded by another application that doesn't > care about types that much. In fact in JSON there is no difference between > 2.0 and 2. So this is quite possible situation IMHO.
That's obviously true but on the same time you should validate and filter your input data. And if you expect a floating point number you need to handle that case explicitly before processing and not leave it to an implicit transformation just hoping everything will be fine.
>> It's true that passing/returning int to/from a function expecting float >> will cast but currently with these rounding functions it's a different >> deal as they expect an `int|float` instead of just `float`. So it's not >> cast on passing the argument but the functions itself are casting. >> >> > Well internally yeah but effectively it is a cast on the return type. What > I meant is that this wont help everywhere when it's passed to function > expecting just float unless it is changed by user to accept int|float. > > Where another set of functions would avoid the BC break it also would be >> against having PHP as a loosely typed languageputting the burden to >> everyone caring. I already see hundreds of `is_int($v) ? round_int($v) : >> round($v)` everywhere around. >> >> > But for me it's sort of an edge as I would think it is not that usual > rounding values that are greater than 2^53. I think it would be good to > note in the RFC what other language do about this issue. I quickly checked > Go and it seems to only have float64 rounding (Math.Round). Rust seems to > also round just f64.
At least python's build-in round function keeps the input type, as long as ndigits is given, else it even goes further and returns an int.
> I'm starting to feel that the problem is that the input type is defined as > int|float. I think we should just correct it to float.
With the current behavior ... yes, but this does not make these functions any better. Sure, I'm trying to improve a case for that does not happen often, but it happens at least for me on working with representing statistical data where using GMP/bcmath ... does not outweigh it's overhead. On the same time the results can be improved without adding overhead.
> Regards > > Jakub
Best, Marc

Marc

2 years ago
Hi, On 26.09.23 12:39, Marc Bennewitz wrote:
> Hi internals > > I'd like to put a new RFC under discussion: > https://wiki.php.net/rfc/integer-rounding > > Best, > Marc >
Based on the discussion I see two groups. The one that would like to change this behavior without message (bug fix) and others that see a to big BC break never willing to touch it at all. On the same time my current approach of finding a compromise by introducing an upcoming behavior change as deprecation message doesn't make anyone happy. This makes me go back and collect other ideas: 1. Argument $force_float   * $force_float=true in PHP 8.4 + deprecation on passing int and omitting this new argument   * $force_float=false in PHP 9.0 This allows to opt-in new behavior asap, warns about upcoming behavior change on passing integers and allows a clean way to fix the deprecation. But do we would like to keep this argument forever? If not it requires another deprecation phase forcing people again to fix deprecation messages. Would it be more logical for `$force_float=false` to return the best possible mathematical correct value instead of trying to keep the input type if possible? Means a float input could also be cast to int if int is a better fit in this case. 2. Integer specific function   * round_int This also would allow opt-in proposed behavior without BC break. But this comes with a couple of downsides:   * Does not improve out-of-the-box behavior   * Requires type-checks before opt-in and goes against PHP as a loosely typed language   * New inconsistency on function naming `round` vs. `round_int` - or just another alias   * New questionable behavior - Why does `round_int` return a float in some cases? Or should this be an error now?   * No logical ceil/floor variant - using these requires type-checks to not loose integer precision For me this is a no-go, I wouldn't vote for it and obviously wouldn't propose this in my own RFC. 3. Any other ideas for making all happy? Best, Marc

Marc

2 years ago
Hi all, I now have updated the RFC to allow to opt-in to new behavior on PHP 8.4 and be able to opt-out to previous behavior in PHP 9.0 via new parameter "force_float". Also the deprecation has been removed as nobody liked it. Best regards, Marc On 26.09.23 12:39, Marc Bennewitz wrote:

Marc

2 years ago
Hi internals, Just want to resent this note about the updated RFC as there where no comments last time and I'm unsure if this was due to mailing list issues. I now have updated the RFC to allow to opt-in to new behavior on PHP 8.4 and be able to opt-out to previous behavior in PHP 9.0 via new parameter "force_float". Also the deprecation has been removed as nobody liked it. If no further comments arrive I'll like to start voting next week Friday. Best regards, Marc On 25.02.24 18:09, Marc Bennewitz wrote:

Larry Garfield

2 years ago
On Sat, Mar 9, 2024, at 1:01 AM, Marc wrote:
> Hi internals, > > Just want to resent this note about the updated RFC as there where no > comments last time and I'm unsure if this was due to mailing list issues. > > I now have updated the RFC to allow to opt-in to new behavior on PHP 8.4 > and be able to opt-out to previous behavior in PHP 9.0 via new parameter > "force_float". > > Also the deprecation has been removed as nobody liked it. > > If no further comments arrive I'll like to start voting next week Friday. > > Best regards, > Marc
I am still opposed to this. Logically, ceil/float/round should be returning ints, not floats. Only returning ints if it was given an int is, er, kinda pointless, as you'll just get back the value you passed in. (Because it's already rounded/floored, etc.) So this doesn't get us any new type safety, but does make the return type less consistent than it is today. That's a step backwards. If there's some math reason that we cannot have those functions return int (someone mentioned there was, but I don't really understand it and the RFC does not explain it at all), then we should at least keep consistency in the return type. "Sometimes I have to cast the return value before I can actually use it in the obvious way, sometimes I don't" is not a good situation. --Larry Garfield

Jim Winstead

2 years ago
On Sat, Mar 9, 2024, at 8:00 AM, Larry Garfield wrote: ]> If there's some math reason that we cannot have those functions return
> int (someone mentioned there was, but I don't really understand it and > the RFC does not explain it at all), then we should at least keep > consistency in the return type. "Sometimes I have to cast the return > value before I can actually use it in the obvious way, sometimes I > don't" is not a good situation.
The round() function has to be able to return float because has an optional precision parameter. So round(3.14, 1) returns 3.1. I agree that there doesn't appear to be much of a reason that ceil() and floor() should be returning float. The documentation for ceil() says: The return value of ceil() is still of type float as the value range of float is usually bigger than that of int. Jim

Gina P. Banyard

2 years ago
On Saturday, 9 March 2024 at 16:00, Larry Garfield <larry@garfieldtech.com> wrote:
> I am still opposed to this. Logically, ceil/float/round should be returning ints, not floats. Only returning ints if it was given an int is, er, kinda pointless, as you'll just get back the value you passed in. (Because it's already rounded/floored, etc.) So this doesn't get us any new type safety, but does make the return type less consistent than it is today. That's a step backwards. > > If there's some math reason that we cannot have those functions return int (someone mentioned there was, but I don't really understand it and the RFC does not explain it at all), then we should at least keep consistency in the return type. "Sometimes I have to cast the return value before I can actually use it in the obvious way, sometimes I don't" is not a good situation. > > --Larry Garfield
There are plenty of values that are exactly representable as floating point numbers but not as integers. One short example: $v = 1e10 + 0.6; var_dump($v); var_dump(round($v)); Gives you accurate precision and proper rounding behaviour. 1e10 *cannot* be represented as an integer. So round *must* be able to return a float. Best regards, Gina P. Banyard

Larry Garfield

2 years ago
On Sun, Mar 10, 2024, at 10:31 AM, Gina P. Banyard wrote:
> On Saturday, 9 March 2024 at 16:00, Larry Garfield > <larry@garfieldtech.com> wrote: > >> I am still opposed to this. Logically, ceil/float/round should be returning ints, not floats. Only returning ints if it was given an int is, er, kinda pointless, as you'll just get back the value you passed in. (Because it's already rounded/floored, etc.) So this doesn't get us any new type safety, but does make the return type less consistent than it is today. That's a step backwards. >> >> If there's some math reason that we cannot have those functions return int (someone mentioned there was, but I don't really understand it and the RFC does not explain it at all), then we should at least keep consistency in the return type. "Sometimes I have to cast the return value before I can actually use it in the obvious way, sometimes I don't" is not a good situation. >> >> --Larry Garfield > > There are plenty of values that are exactly representable as floating > point numbers but not as integers. > > One short example: > > $v = 1e10 + 0.6; > var_dump($v); > var_dump(round($v)); > > Gives you accurate precision and proper rounding behaviour. > > 1e10 *cannot* be represented as an integer. > > So round *must* be able to return a float. > > Best regards, > > Gina P. Banyard
Please include some version of this in the RFC. Especially if it can be even more detailed. --Larry Garfield

Marc

2 years ago
Hi Larry, On 10.03.24 16:52, Larry Garfield wrote:
> On Sun, Mar 10, 2024, at 10:31 AM, Gina P. Banyard wrote: >> On Saturday, 9 March 2024 at 16:00, Larry Garfield >> <larry@garfieldtech.com> wrote: >> >>> I am still opposed to this. Logically, ceil/float/round should be returning ints, not floats. Only returning ints if it was given an int is, er, kinda pointless, as you'll just get back the value you passed in. (Because it's already rounded/floored, etc.) So this doesn't get us any new type safety, but does make the return type less consistent than it is today. That's a step backwards. >>> >>> If there's some math reason that we cannot have those functions return int (someone mentioned there was, but I don't really understand it and the RFC does not explain it at all), then we should at least keep consistency in the return type. "Sometimes I have to cast the return value before I can actually use it in the obvious way, sometimes I don't" is not a good situation. >>> >>> --Larry Garfield >> There are plenty of values that are exactly representable as floating >> point numbers but not as integers. >> >> One short example: >> >> $v = 1e10 + 0.6; >> var_dump($v); >> var_dump(round($v)); >> >> Gives you accurate precision and proper rounding behaviour. >> >> 1e10 *cannot* be represented as an integer. >> >> So round *must* be able to return a float. >> >> Best regards, >> >> Gina P. Banyard > Please include some version of this in the RFC. Especially if it can be even more detailed. > > --Larry Garfield
While float's get imprecise on represent integers > 2^53, int's can't represent such a bit range of numbers as floats can. On rounding a floating point number your already have to deal with the imprecise of floats in the first place. That's why rounding operations should take the input type into account and only cast if the input type can't represent the resulting number to avoid ending up in overflow or error. In case you where looking at python where round() is documented with ...
> The return value is an integer if /ndigits/ is omitted or |None|.
Otherwise, the return value has the same type as /number/. ... but in python int is limited only by memory and not by the type which makes it very different. I have added a description and updated example in the RFC. Best, Marc