How to ceil, floor and round bcmath numbers?

Viewed 13894
6 Answers
function bcnegative($n)
{
    return strpos($n, '-') === 0; // Is the number less than 0?
}

function bcceil($n)
{
    return bcnegative($n) ? (($v = bcfloor(substr($n, 1))) ? "-$v" : $v)
                          : bcadd(strtok($n, '.'), strtok('.') != 0);
}

function bcfloor($n)
{
    return bcnegative($n) ? '-' . bcceil(substr($n, 1)) : strtok($n, '.');
}

function bcround($n, $p = 0)
{
    $e = bcpow(10, $p + 1);
    return bcdiv(bcadd(bcmul($n, $e, 0), bcnegative($n) ? -5 : 5), $e, $p);
}

I chose the Alix Axel's variant for rounding as it is the fastest since it only uses addition and subtraction, not multiplication and division. To round with negative precision at the beginnig I used standard function:

sprintf('%.0F', round($result, $operand_value))

But I faced the problem described here. So I extended this variant for negative precision:

function bcround($number, $precision)
{
    if($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;
    }
    else
    {
        $mod = bcmod($number, bcpow(10, -$precision));
        $sub = bcsub($number, $mod);
        if($mod[0] != '-')
        {
            $add = $mod[0] >= 5 ? bcpow(10, strlen($mod)) : 0;
        }
        else
        {
            $add = $mod[1] >= 5 ? '-'.bcpow(10, strlen($mod)-1) : 0;
        }
        return bcadd($sub, $add);
    }
}

A more elegant and shorter option through recursion:

function bcround($number, $precision)
{
    if($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;
    }
    else
    {       
        $pow = bcpow(10, -$precision);
        return bcmul(bcround(bcdiv($number, $pow, -$precision), 0), $pow);
    }
}

But it is slower because it uses two operations (division and multiplication) versus one operation of finding the remainder of the division (mod) in the first case. Speed tests have confirmed this:

First variant. Total iterations:10000. Duration: 0.24502515792847 seconds.

Second variant. Total iterations:10000. Duration: 0.35303497314453 seconds.

Only use bcmath functions to do that:

function bcceil($number, $precision = 0) {
    $delta = bcdiv('9', bcpow(10, $precision + 1), $precision + 1);
    $number = bcadd($number, $delta, $precision + 1);
    $number = bcadd($number, '0', $precision);
    return $number;
}

function bcfloor($number, $precision = 0) {
    $number = bcadd($number, '0', $precision);
    return $number;
}

For test:

$numbers = [
    '1', '1.1', '1.4', '1.5', '1.9',
    '1.01', '1.09', '1.10', '1.19', '1.90', '1.99',
    '2'
];

foreach ($numbers as $n) {
    printf("%s (ceil)--> %s\n", $n, bcceil($n, 1));
}

printf("\n");

foreach ($numbers as $n) {
    printf("%s (floor)--> %s\n", $n, bcfloor($n, 1));
}

And the test results:

1 (ceil)--> 1.0
1.1 (ceil)--> 1.1
1.4 (ceil)--> 1.4
1.5 (ceil)--> 1.5
1.9 (ceil)--> 1.9
1.01 (ceil)--> 1.1
1.09 (ceil)--> 1.1
1.10 (ceil)--> 1.1
1.19 (ceil)--> 1.2
1.90 (ceil)--> 1.9
1.99 (ceil)--> 2.0
2 (ceil)--> 2.0

1 (floor)--> 1.0
1.1 (floor)--> 1.1
1.4 (floor)--> 1.4
1.5 (floor)--> 1.5
1.9 (floor)--> 1.9
1.01 (floor)--> 1.0
1.09 (floor)--> 1.0
1.10 (floor)--> 1.1
1.19 (floor)--> 1.1
1.90 (floor)--> 1.9
1.99 (floor)--> 1.9
2 (floor)--> 2.0
Related