-1

I'm using bcdiv function from PHP to calculate some things, but result is different than it should be. Here is sample code:

$val1 = 599.60;
$val2 = 60;

var_dump(bcdiv($val1, $val2, 0));
// result string(1) "9"
// should be "10"

var_dump(bcdiv($val1, $val2, 2));
// result string(4) "9.99"
// result ok, but

var_dump(bcdiv($val1, $val2, 1));
// result string(4) "9.9"
// should be "10" too

Results from first var_dump is very strange for me, as it should be 10 not 9.

Same results are for other BCMath functions:

$val1 = 599.99;
$val2 = 1;

var_dump(bcmul($val1, $val2, 0));
// result string(3) "599"
// should be "600"

var_dump(bcadd($val1, $val2, 0));
// result string(3) "600"
// should be "601"

var_dump(bcsub($val1, $val2, 0));
// result string(3) "598"
// should be "599"

I have a lot of float calculations in my app and now I'm not sure how to handle them properly, normal math calculations have floating point problems, but that from bc math are not the best thing I should use.

So, here are my questions:

  1. How can I handle float calculations, considering that BCMath results are wrong, when you think about regular mathematics rounding rules?
  2. How do You (other PHP programmers) calculate float numbers? Converting them to integers is not possible in my app.
  3. What do you think about php-decimal?
ptyskju
  • 175
  • 1
  • 3
  • 18
  • 1
    The [first example](https://www.php.net/manual/en/function.bcdiv.php#100118) on the manual page shows this. Looks as though `bcdiv()` truncates and not rounds the answer – Nigel Ren Mar 18 '20 at 12:18
  • @NigelRen I saw it, but it wasn't helpful. – ptyskju Mar 18 '20 at 12:22

2 Answers2

1

bcdiv

bcdiv ( string $dividend , string $divisor [, int $scale = 0 ] ) : string

Parameters

  • dividend
    The dividend, as a string.
  • divisor
    The divisor, as a string.
  • scale
    This optional parameter is used to set the number of digits after the decimal place in the result. If omitted, it will default to the scale set globally with the bcscale() function, or fallback to 0 if this has not been set.

As you can see, bcdiv 3rd parameter is not for rounding, but for scale, which means it just keeps that number of digits.

There is a nice Q/A by Alix Axel on this specific problem which you can see here "How to ceil, floor and round bcmath numbers?".

In his answer, he has a custom function bcround which will do the rounding as you expect:

function bcround($number, $precision = 0)
{
  if (strpos($number, '.') !== false) {
    if ($number[0] != '-')
      return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision);
    return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision);
  }
  return $number;
}

$val1 = 599.60;
$val2 = 60;

var_dump(bcround(bcdiv($val1, $val2, 10), 0));
// string(2) "10"

var_dump(bcround(bcdiv($val1, $val2, 10), 2));
// string(4) "9.99"

var_dump(bcround(bcdiv($val1, $val2, 10), 1));
// string(4) "10.0"

Proper way to handle currency numbers

I am not sure if your numbers are referring to price and currencies, but if that's the case, then the best way to handle currency calculations, is to have all the values to cents and do the math using integers.

For your example:

$val1 = 59960; // 59960 cents == 599.60
$val2 = 6000;  // 6000 cents == 60.00

var_dump($val1 / $val2);
// float(9.9933333333333)

var_dump(round($val1 / $val2, 0));
// float(10)

var_dump(round($val1 / $val2, 2));
// float(9.99)

var_dump(round($val1 / $val2, 1));
// float(10)
Community
  • 1
  • 1
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
  • Look at my example, I'm not using `bcdiv` for rounding, but for regular divide calculations. And results are wrong, because of rounding rules. It will be hard, but it seems that the only way to handle calculations properly is to rewrite a lot of fragile functions in my app... – ptyskju Mar 23 '20 at 09:49
  • @ptyskju you describe a problem about rounding. The calculation and result is `599.60 / 60 = 9.993333333333334`. Why do you expect `bcdiv($val1, $val2, 0)` to return `10` when the result is `9.993`, isn't that rounding that you're expecting? Why do you expect `bcdiv($val1, $val2, 1)` to return `10` from the calculated number `9.993` instead of `9.9`, isn't that rounding? You should rewrite your question and try to be more clear on what your problem is. Also keep in mind that there is nothing wrong with the PHP `bcdiv` function, it works as expected. – Christos Lytras Mar 23 '20 at 11:04
  • _Also keep in mind that there is nothing wrong with the PHP bcdiv function, it works as expected._ - I know it, that is why I'm looking for other solutions. – ptyskju Mar 23 '20 at 11:15
  • It looks like you don't know it from your first question *"1. How can I handle float calculations, considering that **bc math has rounding problems**?"* and from your question title *"PHP BC Math library **ignore rounding rules**"*. **BCMath does not have any kind of problems**. You also do not answer on my question about your expectation results. – Christos Lytras Mar 23 '20 at 11:27
  • _Why do you expect bcdiv($val1, $val2, 0) to return 10 when the result is 9.993, isn't that rounding that you're expecting?_ - normally, when you divide 599.60 by 60 and want result as natural number, you will get 10, because `9.99 ≈ 10`. I understand that BCMath will remove all unwanted digits based on scale, but still I'm sure that `599.60 / 60 ≈ 10`, as normal mathematics describes. `9` is not a proper value, because of **wrong rounding**, whatever we say. – ptyskju Mar 23 '20 at 11:40
  • *PHP BC Math library ignore rounding rules". BCMath does not have any kind of problems* - question changed, thank you. – ptyskju Mar 23 '20 at 11:49
  • 1
    The symbol `≈` is about *"approximately equal"*. You expect to get approximate numbers using regular functions in PHP? If you want to get `10` from `9.99` then you either `round(9.99)` with `0` precision or you `ceil(9.99)`, there is no *"approximately"* functionality in PHP, you have to create your own. *normally, when you divide 599.60 by 60 and want result as natural number* no, that is your assumption and normally when you divide 599.60 by 60 you want to get the **actual** result, which is `9.993` so the rest of your calculation are based on correct results and not on approximate results. – Christos Lytras Mar 23 '20 at 12:24
  • *9 is not a proper value, because of **wrong rounding*** again you are talking about rounding when you say *I understand that BCMath will remove all unwanted digits based on scale*. If we round `9.993` with `1` precision, `9.9` is the proper value; if we round `9.993` with `0` precision `10` is the proper value and that is exactly the solution I provide to my answer because again **`bcdiv` does not round anything**. Also, I didn't said you'll get just `9`, you'll get `9.99` if you round with `2` precision. If you want to get `9` you'll have to use the `floor` function. – Christos Lytras Mar 23 '20 at 12:25
  • Thank you for great explanation. I will try to find solution for my problem, based on your comment. I will accept your solution, if nothing better appear. – ptyskju Mar 23 '20 at 12:43
0

Thank you Christos Lytras for pointing what I did wrong. Because I'm using BCMath calculations in multiple classes and I don't have enough time to rewrite all places with floats to integers, I decided to create simple trait. It solves all my problems with rounded values. Here is trait code:

trait FloatCalculationsTrait
{

    /**
     * Default precision for function results
     *
     * @var integer
     */
    protected $scale = 2;

    /**
     * Default precision for BCMath functions
     *
     * @var integer
     */
    protected $bcMathScale = 10;

    /**
     * Rounding calculation values, based on https://stackoverflow.com/a/60794566/3212936
     *
     * @param string $valueToRound
     * @param integer|null $scale
     * @return float
     */
    protected function round(string $valueToRound, ?int $scale = null): float
    {
        if ($scale === null) {
            $scale = $this->scale;
        }

        $result = $valueToRound;

        if (strpos($valueToRound, '.') !== false) {
            if ($valueToRound[0] != '-') {
                $result = bcadd($valueToRound, '0.' . str_repeat('0', $scale) . '5', $scale);
            } else {
                $result = bcsub($valueToRound, '0.' . str_repeat('0', $scale) . '5', $scale);
            }
        }

        return $result;
    }

    /**
     * Add floats
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function add(?float $firstElement, ?float $secondElement, ?int $scale = null): float
    {
        $result = bcadd($firstElement, $secondElement, $this->bcMathScale);

        return $this->round($result, $scale);
    }

    /**
     * Substract floats
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function substract(?float $firstElement, ?float $secondElement, ?int $scale = null): float
    {
        $result = bcsub($firstElement, $secondElement, $this->bcMathScale);

        return $this->round($result, $scale);
    }

    /**
     * Alias for `substract` function
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function sub(?float $firstElement, float $secondElement, ?int $scale = null): float
    {
        return $this->substract($firstElement, $secondElement, $scale);
    }

    /**
     * Multiply floats
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function multiply(?float $firstElement, ?float $secondElement, ?int $scale = null): float
    {
        $result = bcmul($firstElement, $secondElement, $this->bcMathScale);

        return $this->round($result, $scale);
    }

    /**
     * Alias for `multiply` function
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function mul(?float $firstElement, ?float $secondElement, ?int $scale = null): float
    {
        return $this->multiply($firstElement, $secondElement, $scale);
    }

    /**
     * Divide floats
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function divide(?float $firstElement, ?float $secondElement, ?int $scale = null): float
    {
        $result = bcdiv($firstElement, $secondElement, $this->bcMathScale);

        return $this->round($result, $scale);
    }

    /**
     * Alias for `divide` function
     *
     * @param float|null $firstElement
     * @param float|null $secondElement
     * @param integer|null $scale
     * @return float
     */
    protected function div(?float $firstElement, ?float $secondElement, ?int $scale = null): float
    {
        return $this->divide($firstElement, $secondElement, $scale);
    }
}

And here you can check results: http://sandbox.onlinephpfunctions.com/code/5b602173a1825a2b2b9f167a63646477c5105a3c

ptyskju
  • 175
  • 1
  • 3
  • 18