How to ceil, floor and round bcmath numbers?
Asked Answered
I

6

24

I need to mimic the exact functionality of the ceil(), floor() and round() functions on bcmath numbers, I've already found a very similar question but unfortunately the answer provided isn't good enough for me since it lacks support for negative numbers and the precision argument for the round() function is missing.

I was wondering if anyone can come up with a rather short and elegant solution to this problem.

All input is appreciated, thanks!

Interlanguage answered 29/10, 2009 at 9:53 Comment(0)
I
32

After a night lost trying to solve this problem I believe I've found a rather simple solution, here it is:

function bcceil($number)
{
    if (strpos($number, '.') !== false) {
        if (preg_match("~\.[0]+$~", $number)) {
            return bcround($number, 0);
        }
        
        if ($number[0] != '-') {
            return bcadd($number, 1, 0);
        }
        
        return bcsub($number, 0, 0);
    }
    return $number;
}

function bcfloor($number)
{
    if (strpos($number, '.') !== false) {
        if (preg_match("~\.[0]+$~", $number)) {
            return bcround($number, 0);
        }
        
        if ($number[0] != '-') {
            return bcadd($number, 0, 0);
        }
        
        return bcsub($number, 1, 0);
    }
    return $number;
}

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;
}

I think I didn't miss anything, if someone can spot any bug please let me know. Here are some tests:

assert(bcceil('4') == ceil('4')); // true
assert(bcceil('4.3') == ceil('4.3')); // true
assert(bcceil('9.999') == ceil('9.999')); // true
assert(bcceil('-3.14') == ceil('-3.14')); // true

assert(bcfloor('4') == floor('4')); // true
assert(bcfloor('4.3') == floor('4.3')); // true
assert(bcfloor('9.999') == floor('9.999')); // true
assert(bcfloor('-3.14') == floor('-3.14')); // true

assert(bcround('3', 0) == number_format('3', 0)); // true
assert(bcround('3.4', 0) == number_format('3.4', 0)); // true
assert(bcround('3.5', 0) == number_format('3.5', 0)); // true
assert(bcround('3.6', 0) == number_format('3.6', 0)); // true
assert(bcround('1.95583', 2) == number_format('1.95583', 2)); // true
assert(bcround('5.045', 2) == number_format('5.045', 2)); // true
assert(bcround('5.055', 2) == number_format('5.055', 2)); // true
assert(bcround('9.999', 2) == number_format('9.999', 2)); // true
Interlanguage answered 31/10, 2009 at 8:59 Comment(8)
bcceil('4') would return '3' instead of 4 as it should. Same problem with bcsub. Good idea to use bcadd($number, 0, 0) to truncate the decimals though, didn't thought of that myself.Eparchy
I meant would return 5, not 3.Eparchy
@reko_t: Fixed the bug on bcceil() but I was unable to reproduce the bug you mentioned on the bcfloor() function.Interlanguage
Alix Axel, bcceil() and bcfloor() don'w work right with argument '3.00000'. Result should be 3, but 4 is returned. One more check needed: if (preg_match("/\.[0]+$/i", $number)) return bcround($number, 0);Carousal
@Silver Light: Thanks, I'll look into this ASAP.Interlanguage
Those are failing: var_dump(bcround('5.0445', 2) == number_format('5.045', 2)); var_dump(bcround('5.0445', 1) == number_format('5.05', 1)); var_dump(bcround('5.04455', 2) == number_format('5.045', 2));Bonanno
@SzczepanHołyszewski Problem?Floater
@АртурКурицын bcround('5.0445', 2) rounds down correctly to 5.04. It's correct.Burkett
E
9
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);
}
Employer answered 17/7, 2018 at 21:29 Comment(1)
OK I tested this thoroughly and it is awesome!Distinguished
E
2

Here's ones that support negative numbers and precision argument for rounding.

function bcceil($val) {
    if (($pos = strpos($val, '.')) !== false) {
        if ($val[$pos+1] != 0 && $val[0] != '-')
            return bcadd(substr($val, 0, $pos), 1, 0);
        else
            return substr($val, 0, $pos);
    }
    return $val;
}

function bcfloor($val) {
    if (($pos = strpos($val, '.')) !== false) {
        if ($val[$pos+1] != 0 && $val[0] == '-')
            return bcsub(substr($val, 0, $pos), 1, 0);
        else
            return substr($val, 0, $pos);
    }
    return $val;
}

function bcround($val, $precision = 0) {
    if (($pos = strpos($val, '.')) !== false) {
        if ($precision > 0) {
            $int = substr($val, 0, $pos);
            $pos2 = ++$pos+$precision;
            if ($pos2 < strlen($val)) {
                $val2 = sprintf('%s.%s', substr($val, $pos, $pos2-$pos), substr($val, $pos2));
                $val2 = $val2[0] >= 5 ? bcceil($val2) : bcfloor($val2);
                if (strlen($val2) > $precision)
                    return bcadd($int, $val[0] == '-' ? -1 : 1, 0);
                else
                    return sprintf('%s.%s', $int, rtrim($val2, '0'));
            }
            return $val;
        } else {
            if ($val[$pos+1] >= 5)
                return ($val[0] == '-' ? bcfloor($val) : bcceil($val));
            else
                return ($val[0] == '-' ? bcceil($val) : bcfloor($val));
        }
    }
    return $val;
}
Eparchy answered 29/10, 2009 at 10:54 Comment(6)
I haven't tested it yet but I believe bcround(99.999, 2) wrongly returns 99.100, no?Interlanguage
Nope: $ php -r 'include "bc.php"; var_dump(bcround(99.999, 2));' string(3) "100"Eparchy
The "if (strlen($val2) > $precision)" part is there to prevent that. :)Eparchy
Please be aware that the answer from 'reko_t' doesn't work correctly. If you want to round properly, go to "php.net/manual/en/function.bcscale.php" and look at mwgamera's post.Cashmere
This function produces completely erratic and incorrect results for me. Eg: bcround('323.346',2) produces '323.34'; bcround('323.006', 2) produces '323.' --- am I missing something here? I am assuming this is supposed to be 'half up' rounding? Either way it's wrong, because there's no predictable pattern.Pulchritudinous
I just don't understand how this got voted up higher than the correct answer. :\Pulchritudinous
B
0

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.

Birchfield answered 15/4, 2021 at 11:40 Comment(0)
O
-1

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
Oaf answered 20/8, 2019 at 9:44 Comment(1)
Doesn't work with negative numbers. eg bcceil(-5.4) gives -4, not -5.Employer
T
-2
function getBcRound($number, $precision = 0)
{
    $precision = ($precision < 0)
               ? 0
               : (int) $precision;
    if (strcmp(bcadd($number, '0', $precision), bcadd($number, '0', $precision+1)) == 0) {
        return bcadd($number, '0', $precision);
    }
    if (getBcPresion($number) - $precision > 1) {
        $number = getBcRound($number, $precision + 1);
    }
    $t = '0.' . str_repeat('0', $precision) . '5';
    return $number < 0
           ? bcsub($number, $t, $precision)
           : bcadd($number, $t, $precision);
}

function getBcPresion($number) {
    $dotPosition = strpos($number, '.');
    if ($dotPosition === false) {
        return 0;
    }
    return strlen($number) - strpos($number, '.') - 1;
}

var_dump(getBcRound('3', 0) == number_format('3', 0));
var_dump(getBcRound('3.4', 0) == number_format('3.4', 0));
var_dump(getBcRound('3.56', 0) == number_format('3.6', 0));
var_dump(getBcRound('1.95583', 2) == number_format('1.95583', 2));
var_dump(getBcRound('5.045', 2) == number_format('5.045', 2));
var_dump(getBcRound('5.055', 2) == number_format('5.055', 2));
var_dump(getBcRound('9.999', 2) == number_format('9.999', 2));
var_dump(getBcRound('5.0445', 5) == number_format('5.044500', 5));
var_dump(getBcRound('5.0445', 4) == number_format('5.04450', 4));
var_dump(getBcRound('5.0445', 3) == number_format('5.0445', 3));
var_dump(getBcRound('5.0445', 2) == number_format('5.045', 2));
var_dump(getBcRound('5.0445', 1) == number_format('5.05', 1));
var_dump(getBcRound('5.0445', 0) == number_format('5.0', 0));//
var_dump(getBcRound('5.04455', 2) == number_format('5.045', 2));
var_dump(getBcRound('99.999', 2) == number_format('100.000', 2));
var_dump(getBcRound('99.999') == number_format('99.999', 0));
var_dump(getBcRound('99.999', 'a') == number_format('99.999', 0));
var_dump(getBcRound('99.999', -1.5) == number_format('99.999', 0));
var_dump(getBcRound('-0.00001', 2) == number_format('-0.000', 2));
var_dump(getBcRound('-0.0000', 2) == number_format('0', 2));
var_dump(getBcRound('-4.44455', 2) == number_format('-4.445', 2));
var_dump(getBcRound('-4.44555', 0) == number_format('-4.5', 0));
var_dump(getBcRound('-4.444444444444444444444444444444444444444444445', 0) == number_format('-4.5', 0));
Tierza answered 18/9, 2017 at 19:53 Comment(1)
The result of rounding -4.44555 to 0 decimal places shouldn't be the same as the result of number_format('-4.5', 0) (ie -5). It should be the same as number_format('-4.44555', 0) (ie -4). To test this, simply do round(-4.44555). php.net/manual/en/function.round.php en.wikipedia.org/wiki/Rounding#Round_half_away_from_zeroEmployer

© 2022 - 2024 — McMap. All rights reserved.