Skip to content

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5 #12143

Closed

Description

@TimWolla

Updated description:

<?php
$number = 0.49999999999999994;
var_dump($number, round($number, 0, PHP_ROUND_HALF_UP));

$number = 1.4999999999999998;
var_dump($number, round($number, 0, PHP_ROUND_HALF_UP));

$number = 4503599627370495.5;
var_dump($number, round($number, 0, PHP_ROUND_HALF_UP));

Resulted in this output:

float(0.49999999999999994)
float(1)
float(1.4999999999999998)
float(2)
float(4503599627370495.5)
float(4503599627370495.5)

But I expected this output instead:

float(0.49999999999999994)
float(0)
float(1.4999999999999998)
float(1)
float(4503599627370495.5)
float(4503599627370496)

see: https://3v4l.org/jsMKb


Description

Old Description

The following code:

<?php

var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP), 0.49999999999999994);

Resulted in this output:

float(1)
float(0.49999999999999994)

But I expected this output instead:

float(0)
float(0.49999999999999994)

see: https://3v4l.org/d6AK6 / https://3v4l.org/VvInn


Extracted from #12056 (comment) into a dedicated issue. The solution consists of using modf() and then explicitly checking against 0.5, instead of adding or subtracting 0.5, since the result of the addition / subtraction might not be exactly representable.

/cc @jorgsowa Are you interested in developing the fix?

PHP Version

8.2.10

Operating System

No response

Activity

  1. bukka commented on Sep 7, 2023

    @bukka
    Member

    Yeah this seems like a bug to me.

  2. jorgsowa commented on Sep 7, 2023

    @jorgsowa
    Contributor

    TBH, I have no idea how to fix this and whether it counts into the floating point precision problem.

    I also found out that displaying this number and numbers of close range is problematic.

    https://3v4l.org/PdcJW

    <?php
    
    var_dump(0.49999999999999991, 0.49999999999999992, 0.49999999999999993, 0.49999999999999994, 0.49999999999999995, 0.49999999999999996, 0.49999999999999997, 0.49999999999999998, 0.49999999999999999, 0.5);
    

    Result:

    float(0.4999999999999999)
    float(0.49999999999999994)
    float(0.49999999999999994)
    float(0.49999999999999994)
    float(0.49999999999999994)
    float(0.49999999999999994)
    float(0.49999999999999994)
    float(0.5)
    float(0.5)
    float(0.5)
    
  3. jorgsowa commented on Sep 7, 2023

    @jorgsowa
    Contributor

    I prepared test case for set of similar numbers and it completely gives up with rounding: https://3v4l.org/A12Z0

  4. TimWolla commented on Sep 7, 2023

    @TimWolla
    MemberAuthor

    Those numbers you tested cannot be exactly represented as double-precision IEEE-754 floating point numbers and thus are internally rounded to the nearest representable number which is either 0.49999999999999994 or 0.5.

    This issue is about the fact that 0.4999999999999999 + 0.5 is not exactly representable and thus internally rounded to 1.0, thus floor(float + 0.5) as the current implementation of PHP_ROUND_HALF_UP takes the floor(1.0) and erroneously returns 1.0 instead of the correct result 0.0.

  5. SakiTakamachi commented on Sep 7, 2023

    @SakiTakamachi
    Member

    It's only morning in Japan, so I'm really sorry if I'm just half asleep.

    Apparently, PHP_ROUND_HALF_DOWN seems to be worse.

    <?php
    
    var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN), 0.49999999999999994);
    
    float(1)
    float(0.49999999999999994)
    

    https://3v4l.org/0rmFT

    It's probably a combination of subtraction and rounding error.

  6. hormus commented on Sep 8, 2023

    @hormus

    Hello good morning everyone, using the number_format function you can extend the unrepresented decimals. Storing floats is often lossy in precision.

    <?php
    
    //See https://wiki.php.net/rfc/precise_float_value
    ini_set('precision', -1);
    ini_set('serialize_precision', -1);
    $zn = -1 * 0.0;
    $float = 0.499999999999999944; // valid 18 double-precision IEEE-754 floating point numbers
    var_dump(($float) + (0.5)); // var_dump use serialize_precision
    ?>

    Expected result:

    float(1)
    

    The round function implicitly uses + 0.5 before creating the file patch
    When I write this into the function round(0.499999999999999944, 0); with or without a constant PHP_ROUND_HALF_UP the result 1 is incorrect since the first digit is 4 and therefore not equal to 5 or greater

  7. SakiTakamachi commented on Sep 8, 2023

    @SakiTakamachi
    Member

    Trying to fix this problem, I tried debugging with GDB, and it turned out to be a surprisingly messy process.
    Honestly, I think it's a miracle that it's working now.

    If I'm not mistaken, the entire round() function needs modification.

  8. TimWolla commented on Sep 8, 2023

    @TimWolla
    MemberAuthor

    Folks, the cause of this issue is clear and the necessary fix is already outlined in the initial issue: Use modf() and explicitly compare the decimal part to 0.5.


    I'd write a fix myself, but as @jorgsowa is currently working on an RFC for round() I did not want to trample on his feet and leave him the opportunity to develop the fix, as the issue was detected during review of his RFC implementation.

  9. SakiTakamachi commented on Sep 8, 2023

    @SakiTakamachi
    Member

    @TimWolla

    Unfortunately, it wasn't that simple.
    Please see my issue.

    #12153

  10. TimWolla commented on Sep 8, 2023

    @TimWolla
    MemberAuthor

    @SakiTakamachi The cause of your issue is identical to this issue, you just used different numbers to show the problem.

  11. 23 remaining items

  12. SakiTakamachi commented on Sep 15, 2023

    @SakiTakamachi
    Member

    @TimWolla

    Certainly, using modf() seems reasonable.

  13. bukka commented on Sep 15, 2023

    @bukka
    Member

    Yeah I agree that it's unreliable and that's why I think we can just strip it because that's what user asked for. Doing nothing seems worse to me really. The error is already there just by saving the number and converting it to double. The round will just do what requested.

  14. SakiTakamachi commented on Sep 15, 2023

    @SakiTakamachi
    Member

    I see.

    If you think accuracy is important, you can use it in a way that prioritizes accuracy yourself, and it may be a good thing to have more options.

  15. jorgsowa commented on Sep 15, 2023

    @jorgsowa
    Contributor

    Thank you @TimWolla for handling this issue using modf. It's reasonable to me and probably even faster than my solution. I'm away now for 2 weeks and I will not be able to contribute much this time, so that's why I'm little "silent" last days.

  16. TimWolla commented on Sep 15, 2023

    @TimWolla
    MemberAuthor

    @jorgsowa Thank you for the acknowledgement.

  17. changed the title [-]Incorrect `round($num, 0, PHP_ROUND_HALF_UP)` result for `$num = 0.49999999999999994 / 1.4999999999999998 / 4503599627370495.5`[/-] [+]Incorrect `round($num, 0, PHP_ROUND_HALF_UP)` result for `$num = 1.4999999999999998 / 4503599627370495.5`[/+] on Sep 19, 2023
  18. TimWolla commented on Sep 19, 2023

    @TimWolla
    MemberAuthor

    I've dropped 0.49999999999999994 from the title, because that one is fixed now.

  19. added a commit that references this issue on Feb 3, 2024
    78970ef
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions