Как потолочные, напольные и круглые номера bcmath?
Мне нужно имитировать точную функциональность функций ceil (), floor () и round() для чисел bcmath, я уже нашел очень похожий вопрос, но, к сожалению, предоставленный ответ не достаточно хорош для меня, так как ему не хватает поддержки для отрицательных чисел и аргумента точности для функции round() отсутствует.
Мне было интересно, если кто-нибудь может придумать довольно короткое и элегантное решение этой проблемы.
Все отзывы приветствуются, спасибо!
6 ответов
После ночи, потерянной, пытаясь решить эту проблему, я считаю, что нашел довольно простое решение, вот оно:
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;
}
Я думаю, что я ничего не пропустил, если кто-то может обнаружить ошибку, пожалуйста, дайте мне знать. Вот несколько тестов:
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
function bcnegative($n)
{
return strpos($n, '-') === 0; // Is the number less than 0?
}
function bcceil($n)
{
return bcnegative($n) ? '-' . bcfloor(substr($n, 1))
: 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);
}
Вот те, которые поддерживают отрицательные числа и точность аргумента для округления.
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;
}
Я выбрал вариант Аликс Акселя для округления, так как он самый быстрый, поскольку в нем используются только сложение и вычитание, а не умножение и деление. Для округления с отрицательной точностью вначале я использовал стандартную функцию:
sprintf('%.0F', round($result, $operand_value))
Но я столкнулся с описанной здесь проблемой . Поэтому я расширил этот вариант для отрицательной точности:
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);
}
}
Более элегантный и короткий вариант за счет рекурсии:
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);
}
}
Но он медленнее, потому что он использует две операции (деление и умножение) по сравнению с одной операцией поиска остатка от деления (mod) в первом случае. Это подтвердили скоростные испытания:
Вариант первый. Всего итераций:10000. Длительность: 0,24502515792847 сек.
Второй вариант. Всего итераций:10000. Длительность: 0,35303497314453 секунды.
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));
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