PHP BC Math 库忽略舍入规则

PHP BC Math library ignore rounding rules

我正在使用 PHP 中的 bcdiv 函数来计算一些东西,但结果与应有的不同。这是示例代码:

$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

第一个 var_dump 的结果对我来说很奇怪,因为它应该是 10 而不是 9.

其他 BCMath 函数的结果相同:

$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"

我的应用程序中有很多浮点计算,现在我不确定如何正确处理它们,正常数学计算有浮点问题,但是来自bc math 不是我应该使用的最好的东西。

所以,这是我的问题:

  1. 当您考虑常规数学舍入规则时,考虑到 BCMath 结果是错误的,我该如何处理浮点数计算?
  2. 你(其他 PHP 程序员)如何计算 浮动数字?在我的环境中无法将它们转换为整数 应用
  3. 你觉得 php-decimal 怎么样?

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.

如你所见,bcdiv第三个参数不是用于舍入,而是用于缩放,这意味着它只是保留该位数。

Alix Axel on this specific problem which you can see here "How to ceil, floor and round bcmath numbers?".

有一篇不错的 Q/A

在他的回答中,他有一个自定义函数 bcround,它将按照您的预期进行舍入:

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"

处​​理货币数字的正确方法

我不确定您的数字是否指的是价格和货币,但如果是这样,那么处理货币计算的最佳方法是将所有值都设为美分并使用整数进行数学运算。

以你的例子为例:

$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)

感谢 Christos Lytras 指出我做错的地方。因为我在多个 类 中使用 BCMath 计算并且我没有足够的时间将所有带有浮点数的地方重写为整数,所以我决定创建简单的特征。它用四舍五入的值解决了我所有的问题。这是特征代码:

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 
     *
     * @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);
    }
}

您可以在这里查看结果:http://sandbox.onlinephpfunctions.com/code/5b602173a1825a2b2b9f167a63646477c5105a3c