views:

248

answers:

3

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!

+3  A: 

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
I haven't tested it yet but I believe bcround(99.999, 2) wrongly returns 99.100, no?
Alix Axel
Nope:$ php -r 'include "bc.php"; var_dump(bcround(99.999, 2));'string(3) "100"
reko_t
The "if (strlen($val2) > $precision)" part is there to prevent that. :)
reko_t
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
I just don't understand how this got voted up higher than the correct answer. :\
Greg
+3  A: 

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 ($number[0] != '-')
     {
      return bcadd($number, 1, 0);
     }

     return bcsub($number, 0, 0);
    }

    return $number;
}

function bcfloor($number)
{
    if (strpos($number, '.') !== false)
    {
     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
Alix Axel
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
I meant would return 5, not 3.
reko_t
@reko_t: Fixed the bug on bcceil() but I was unable to reproduce the bug you mentioned on the bcfloor() function.
Alix Axel
Why the downvote?
Alix Axel
A: 

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.