Search code examples
phpprecisiondivisionbcbcmath

PHP BC Math library ignore rounding rules


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?

Solution

  • 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