小数向上, 向下精确

Posted by Kaen98 Blog on July 12, 2018

场景

电商项目中, 有一场景: 对商品打折优惠过后, 可能会有小数产生, 这时候是选择 四舍五入? 还是 向上精确? 向下精确? 产品决定: 为了追求, 商家最大利益, 采取的是 用户实际付款, 向上精确!

比如: 用户购买一个耳机 售价: 100.5 元, 商家活动 九五折 那么, 实际付款: 100.5 * 0.95 = 95.475 元

这时, 最大单位为分, 需对 实际付款, 向上精确2位小数, 预期结果如下: 95.470 => 95.47 95.471 => 95.48 95.475 => 95.48 95.477 => 95.48 95.4701 => 95.48

向上精确2位

<?php
// 向上精确2位
function ceil_dec($var) {
    $var = $var * 100;
    return ceil($var) / 100;
}
echo ceil_dec(95.470);echo '<br>'; // 95.47
echo ceil_dec(95.471);echo '<br>';// 95.48
echo ceil_dec(95.475);echo '<br>';// 95.48
echo ceil_dec(95.477);echo '<br>';// 95.48
echo ceil_dec(95.4701);echo '<br>';// 95.48
echo '<hr>';

当然, 肯定也可以, 向下精确2位

// 向下精确2位
function floor_dec($var) {
    $var = $var * 100;
    return floor($var) / 100;
}

对方法, 优化: 可以精确到指定小数位

<?php
// 向上精确指定位
function ceil_dec($var, $ext = 2) {
    $pow = pow(10, $ext);
    $var = $var * $pow;
    return ceil($var) / $pow;
}

// 向下精确指定位
function floor_dec($var, $ext = 2) {
    $pow = pow(10, $ext);
    $var = $var * $pow;
    return floor($var) / $pow;
}

但是, 还是有BUG! 偶然, 测试 0.07 向上取2位小数, 却得0.08, 理论应该是0.07

<?php
// 向上精确指定位
function ceil_dec($var, $ext = 2) {
    $pow = pow(10, $ext);
    $var = $var * $pow;
    return ceil($var) / $pow;
}
echo ceil_dec(0.07, 2);   // 输出: 0.08, 理论应该是0.07

排查了一番, 发现问题是小数运算精度丢失导致的:

,
,
,
 $var = $var * $pow;  // 7 = 0.07 * 100,  注意这里已经发生小数精度丢失
 ceil($var); // 8 = ceil(7),  这里ceil去整就会发生异常, 明明 ceil(7) 等于7 , 却等于8
,
,
,

测试代码:

var_dump(ceil(0.07 * 100));   // float(8)
var_dump(ceil(7)); //  float(7)

使用 PHP 高精度运算函数, 修复此BUG

<?php
// 向上精确指定位
function ceil_dec($var, $ext = 2) {
    $pow = pow(10, $ext);
    // $var = $var * $pow;
    $var = bcmul($var, $pow, 3);
    return ceil($var) / $pow;
}


echo ceil_dec(0.07, 2);  // 0.07


// 向下精确指定位
function floor_dec($var, $ext = 2) {
 $pow = pow(10, $ext);
    // $var = $var * $pow;
    $var = bcmul($var, $pow, 3);
    return floor($var) / $pow;
}