How to ceil, floor and round bcmath numbers?

13,195

Solution 1

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

Solution 2

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

Solution 3

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;
}
Share:
13,195

Related videos on Youtube

Alix Axel
Author by

Alix Axel

If you need to, you can contact me at: alix [dot] axel [at] gmail [dot] com. I'm #SOreadytohelp Some of my GitHub repositories: phunction, a minimalistic PHP HMVC Framework. halBox, bash script to bootstrap Debian/Ubuntu servers. ArrestDB, RESTful API for SQLite, MySQL and PostgreSQL databases. genex.js, Genex module for Node.js. If you know how to work with regexes, have a look at http://namegrep.com/. ;)

Updated on April 16, 2021

Comments

  • Alix Axel
    Alix Axel about 3 years

    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!

  • Alix Axel
    Alix Axel over 14 years
    I haven't tested it yet but I believe bcround(99.999, 2) wrongly returns 99.100, no?
  • reko_t
    reko_t over 14 years
    Nope: $ php -r 'include "bc.php"; var_dump(bcround(99.999, 2));' string(3) "100"
  • reko_t
    reko_t over 14 years
    The "if (strlen($val2) > $precision)" part is there to prevent that. :)
  • reko_t
    reko_t over 14 years
    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
    reko_t over 14 years
    I meant would return 5, not 3.
  • Alix Axel
    Alix Axel over 14 years
    @reko_t: Fixed the bug on bcceil() but I was unable to reproduce the bug you mentioned on the bcfloor() function.
  • Admin
    Admin over 13 years
    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.
  • Greg
    Greg over 13 years
    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
    Greg over 13 years
    I just don't understand how this got voted up higher than the correct answer. :\
  • Silver Light
    Silver Light over 13 years
    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);
  • Alix Axel
    Alix Axel over 13 years
    @Silver Light: Thanks, I'll look into this ASAP.
  • Артур Курицын
    Артур Курицын over 6 years
    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));
  • Glutexo
    Glutexo over 6 years
    @SzczepanHołyszewski Problem?
  • Matt Raines
    Matt Raines almost 6 years
    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_zero
  • Theodore R. Smith
    Theodore R. Smith about 5 years
    OK I tested this thoroughly and it is awesome!
  • Matt Raines
    Matt Raines over 4 years
    Doesn't work with negative numbers. eg bcceil(-5.4) gives -4, not -5.
  • Coloured Panda
    Coloured Panda over 2 years
    @АртурКурицын bcround('5.0445', 2) rounds down correctly to 5.04. It's correct.