php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #81697 Since 8.0, php is not multiplying float values correctly
Submitted: 2021-12-03 17:30 UTC Modified: 2021-12-03 18:35 UTC
From: neil+php at neilmasters dot co dot uk Assigned: cmb (profile)
Status: Not a bug Package: Math related
PHP Version: 8.0.13 OS: linux, mac
Private report: No CVE-ID: None
 [2021-12-03 17:30 UTC] neil+php at neilmasters dot co dot uk
Description:
------------
Hi folks,
I tried a searching prior to submitting because I can not see myself being the only person to have come across this, apologies if it is a duplicate. I also have to admit to not really knowing what it is I need to look for.

I am seeing odd results when multiplying specific values since 8.0 on linux and mac - I can not test on windows currently.

2114.20 * 100: 211420 right? no. According to PHP since 8.0 it is 211419.99999999997

1.15 * 100: 115 yes? no. It results in 114.99999999999999

1.08 * 100: 108? Yes actually :)

This does not seem to be an issue with ints, just floats.

https://3v4l.org/rnTis is a quick 3v4l with a few examples and I have attached a test script which you can use to highlight the issue across any number range you want.


Test script:
---------------
<?php
/**
 * Test script to demo the issue of multiplication of specific numbers
 * results in the wrong value.
 *
 * Using start of 1 and stop of 2 multiply every 2dp value(i.e. 1.01 - 1.99) by
 * 100 and var dump the value.
 *
 * Example results:
 * 1.06: 106
 * 1.07: 107
 * 1.08: 108
 * 1.09: 109.00000000000001
 * 1.10: 110.00000000000001
 * 1.11: 111.00000000000001
 * 1.12: 112.00000000000001
 * 1.13: 112.99999999999999
 * 1.14: 113.99999999999999
 * 1.15: 114.99999999999999
 * 1.16: 115.99999999999999
 * 1.17: 117
 * 1.18: 118
 * ...
 *
 * Now here is the barking mad weirdness... bump up that start and stop by 1 and you get
 * totally different results, showing a lack of consistency in its wrongness.
 *
 * 2.00: 200
 * 2.01: 200.99999999999997
 * 2.02: 202
 * 2.03: 202.99999999999997
 * ...
 */
$start = 1;
$stop = 2;

for ($i = $start; $i < $stop; $i++) {
    for ($j = 0; $j < 100; $j++) {
        $number = (float)sprintf(
            '%s.%s',
            $i,
            $j < 10
                ? sprintf('0%s', $j)
                : $j
        );

        var_dump($number * 100);

        echo PHP_EOL;
    }
}

Expected result:
----------------
float(100)
float(101)
float(102)
float(103)
float(104)
float(105)
float(106)
float(107)
float(108)
float(109)
float(110)
float(111)
float(112)
float(113)
float(114)
float(115)
float(116)
float(117)
float(118)
float(119)
float(120)
float(121)
float(122)
float(123)
float(124)
float(125)
float(126)
float(127)
float(128)
float(129)
float(130)
float(131)
float(132)
float(133)
float(134)
float(135)
float(136)
float(137)
float(138)
float(139)
float(140)
float(141)
float(142)
float(143)
float(144)
float(145)
float(146)
float(147)
float(148)
float(149)
float(150)
float(151)
float(152)
float(153)
float(154)
float(155)
float(156)
float(157)
float(158)
float(159)
float(160)
float(161)
float(162)
float(163)
float(164)
float(165)
float(166)
float(167)
float(168)
float(169)
float(170)
float(171)
float(172)
float(173)
float(174)
float(175)
float(176)
float(177)
float(178)
float(179)
float(180)
float(181)
float(182)
float(183)
float(184)
float(185)
float(186)
float(187)
float(188)
float(189)
float(190)
float(191)
float(192)
float(193)
float(194)
float(195)
float(196)
float(197)
float(198)
float(199)

Actual result:
--------------
float(100)
float(101)
float(102)
float(103)
float(104)
float(105)
float(106)
float(107)
float(108)
float(109.00000000000001)
float(110.00000000000001)
float(111.00000000000001)
float(112.00000000000001)
float(112.99999999999999)
float(113.99999999999999)
float(114.99999999999999)
float(115.99999999999999)
float(117)
float(118)
float(119)
float(120)
float(121)
float(122)
float(123)
float(124)
float(125)
float(126)
float(127)
float(128)
float(129)
float(130)
float(131)
float(132)
float(133)
float(134)
float(135)
float(136)
float(137)
float(138)
float(139)
float(140)
float(141)
float(142)
float(143)
float(144)
float(145)
float(146)
float(147)
float(148)
float(149)
float(150)
float(151)
float(152)
float(153)
float(154)
float(155)
float(156)
float(157)
float(158)
float(159)
float(160)
float(161)
float(162)
float(163)
float(164)
float(165)
float(166)
float(167)
float(168)
float(169)
float(170)
float(171)
float(172)
float(173)
float(174)
float(175)
float(176)
float(177)
float(178)
float(179)
float(180)
float(181)
float(182)
float(183)
float(184)
float(185)
float(186)
float(187)
float(188)
float(189)
float(190)
float(191)
float(192)
float(193)
float(194)
float(195)
float(196)
float(197)
float(198)
float(199)

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2021-12-03 17:40 UTC] cmb@php.net
-Status: Open +Status: Not a bug -Assigned To: +Assigned To: cmb
 [2021-12-03 17:40 UTC] cmb@php.net
> 2114.20 * 100: 211420 right?

When it comes to floating point arithmethic, no.  See
<http://www.floating-point-gui.de/> for details.

Anyhow, this is caused by using the value of the INI setting
serialize_precision instead of precision[1].

[1] <https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.standard>
 [2021-12-03 18:03 UTC] neil+php at neilmasters dot co dot uk
Cheers for the lightning fast response.

Apologies for being annoying - I have to admit my knowledge with regards to php 8 ini settings is a bit lack luster and I am using default ini' so just trying to wrap my head around this.

Would that also then apply to the following example?

<?php

$calc = 2114.20 * 100;

echo $calc; // 211420
echo (int)$calc; // 211419
 [2021-12-03 18:25 UTC] cmb@php.net
seralize_precision and precision are INI_ALL, so you can change
that from within the script.  E.g. <https://3v4l.org/dOu3t> shows
how you could restore the behavior prior to PHP 8.0.0.

The (int) cast is not related to (serialize_)precision at all:
<https://3v4l.org/nHFkv>.  These settings are only relevant for
float to string conversion (in the broadest sense).  In the given
example, you want to use number_format().

Note though that you never should do monetary calculations using
floats.  Either use ints and work with cents instead of dollars
(or whatever currency you're dealing with), or use BCMath or some
other arbitrary precision library.  There are still caveats, but
using floats for monetary calculations is the recipe to desaster
in the first place.
 [2021-12-03 18:35 UTC] neil+php at neilmasters dot co dot uk
> Note though that you never should do monetary calculations using
floats.  

> Either use ints and work with cents instead of dollars
(or whatever currency you're dealing with).


_I_ do, unfortunately the api I am interacting with do not hence the requirement of conversion.

> There are still caveats, but
using floats for monetary calculations is the recipe to desaster
in the first place.

I could not agree more.
 [2021-12-06 20:38 UTC] a at b dot c dot de
If you're getting a float (and it looks like you're trying to turn that into an integer number of cents for exactly these reasons):

$cents = (int)round($dollars * 100);
 
PHP Copyright © 2001-2023 The PHP Group
All rights reserved.
Last updated: Sun Feb 05 01:03:38 2023 UTC