21

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!

Community
  • 1
  • 1
Alix Axel
  • 141,486
  • 84
  • 375
  • 483

6 Answers6

31

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
Glavić
  • 39,315
  • 12
  • 70
  • 99
Alix Axel
  • 141,486
  • 84
  • 375
  • 483
  • 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. – reko_t Nov 01 '09 at 10:04
  • I meant would return 5, not 3. – reko_t Nov 01 '09 at 10:04
  • @reko_t: Fixed the bug on bcceil() but I was unable to reproduce the bug you mentioned on the bcfloor() function. – Alix Axel Nov 02 '09 at 10:38
  • 2
    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); – Silver Light Nov 23 '10 at 13:24
  • @Silver Light: Thanks, I'll look into this ASAP. – Alix Axel Nov 24 '10 at 12:03
  • 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)); – Артур Курицын Sep 18 '17 at 19:46
  • @SzczepanHołyszewski Problem? – Glutexo Jan 11 '18 at 11:50
4
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);
}
Matt Raines
  • 3,881
  • 8
  • 26
  • 32
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;
}
reko_t
  • 51,428
  • 9
  • 82
  • 76
  • I haven't tested it yet but I believe bcround(99.999, 2) wrongly returns 99.100, no? – Alix Axel Oct 31 '09 at 08:57
  • Nope: $ php -r 'include "bc.php"; var_dump(bcround(99.999, 2));' string(3) "100" – reko_t Oct 31 '09 at 14:13
  • The "if (strlen($val2) > $precision)" part is there to prevent that. :) – reko_t Oct 31 '09 at 14:14
  • 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. – Greg Sep 27 '10 at 03:49
  • I just don't understand how this got voted up higher than the correct answer. :\ – Greg Sep 27 '10 at 03:57
  • 2
    Please be aware that the answer from 'reko_t' doesn't work correctly. If you want to round properly, go to "http://php.net/manual/en/function.bcscale.php" and look at mwgamera's post. –  Sep 01 '10 at 02:04
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.

-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
green sea
  • 1
  • 1
-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));
  • 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)`. http://php.net/manual/en/function.round.php https://en.wikipedia.org/wiki/Rounding#Round_half_away_from_zero – Matt Raines Jul 20 '18 at 15:20