How to ceil, floor and round bcmath numbers?
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;
}
Related videos on Youtube
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, 2021Comments
-
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 over 14 yearsI haven't tested it yet but I believe bcround(99.999, 2) wrongly returns 99.100, no?
-
reko_t over 14 yearsNope: $ php -r 'include "bc.php"; var_dump(bcround(99.999, 2));' string(3) "100"
-
reko_t over 14 yearsThe "if (strlen($val2) > $precision)" part is there to prevent that. :)
-
reko_t over 14 yearsbcceil('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 over 14 yearsI meant would return 5, not 3.
-
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 over 13 yearsPlease 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 over 13 yearsThis 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 over 13 yearsI just don't understand how this got voted up higher than the correct answer. :\
-
Silver Light over 13 yearsAlix 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 over 13 years@Silver Light: Thanks, I'll look into this ASAP.
-
Артур Курицын over 6 yearsThose 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 over 6 years@SzczepanHołyszewski Problem?
-
Matt Raines almost 6 yearsThe 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 asnumber_format('-4.44555', 0)
(ie -4). To test this, simply doround(-4.44555)
. php.net/manual/en/function.round.php en.wikipedia.org/wiki/Rounding#Round_half_away_from_zero -
Theodore R. Smith about 5 yearsOK I tested this thoroughly and it is awesome!
-
Matt Raines over 4 yearsDoesn't work with negative numbers. eg
bcceil(-5.4)
gives-4
, not-5
. -
Coloured Panda over 2 years@АртурКурицын
bcround('5.0445', 2)
rounds down correctly to5.04
. It's correct.