Обрезать числа с плавающей запятой с помощью PHP - PullRequest
11 голосов
/ 12 января 2011

Когда число с плавающей запятой необходимо усечь до определенной цифры после числа с плавающей запятой, оказывается, что это сделать нелегко.Например, если усечение должно быть выполнено до второй цифры после точки, цифры должны быть

45.8976 => 45.89, 0.0185 => 0.01

(вторая цифра после точки не округляется в соответствии с третьей цифрой после точки).

Такие функции, как round(), number_format(), sprintf(), округляют число и распечатывают

45.8976 => 45.90, 0.0185 => 0.02

Я нашел два решения, и мне интересно, достаточно ли они хороши икакой из них лучше использовать

1.

function truncNumber( $number, $prec = 2 )
{
    return bccomp( $number, 0, 10 ) == 0 ? $number : round( $number - pow( 0.1, bcadd(   $prec, 1 ) ) * 5, $prec );
}

2.

function truncNumber($number, $prec = 2 )
{
    return sprintf( "%.".$prec."f", floor( $number*pow( 10, $prec ) )/pow( 10, $prec ) );
}

Ответы [ 11 ]

20 голосов
/ 12 января 2011

floor будет делать так, как вы просите.

floor(45.8976 * 100) / 100;

Вы не найдете более прямой функции для этого, так как спросить это странноОбычно вы округляете математически правильно.Из любопытства - для чего тебе это нужно?

16 голосов
/ 03 октября 2012

это мое решение:

/**
 * @example truncate(-1.49999, 2); // returns -1.49
 * @example truncate(.49999, 3); // returns 0.499
 * @param float $val
 * @param int f
 * @return float
 */
function truncate($val, $f="0")
{
    if(($p = strpos($val, '.')) !== false) {
        $val = floatval(substr($val, 0, $p + 1 + $f));
    }
    return $val;
}
9 голосов
/ 27 января 2016
function truncate($value)
{
    if ($value < 0)
    {
        return ceil((string)($value * 100)) / 100;
    }
    else
    {
        return floor((string)($value * 100)) / 100;
    }
}

Почему PHP не обрабатывает математические формулы так, как мы ожидаем, например, floor(10.04 * 100) = 1003, когда мы все знаем, что это должно быть 1004.Эта проблема не только в PHP, но может быть найдена во всех языках, в зависимости от относительной ошибки в используемом языке.PHP использует формат двойной точности IEEE 754, который имеет относительную ошибку около 1.11e-16. (ресурс)

Реальная проблема заключается в том, что функция floor преобразует значение с плавающей точкой в ​​значение int, например (int)(10.04 * 100) = 1003, как мы видели в функции floor ранее. (ресурс)

Таким образом, чтобы преодолеть эту проблему, мы можем привести тип float к строке, строка может точно представлять любое значение, а затем функция floor точно приведет к типу int.

6 голосов
/ 03 октября 2011

Для усечения чисел «лучше» использовать (здесь я взял число, которое работает для моего примера):

$number = 120,321314;

$truncate_number  = number_format($number , 1); // 120,3
$truncate_number  = number_format($number , 2); // 120,32
$truncate_number  = number_format($number , 3); // 120,321

Надеюсь, эта помощь проще, чем другие ответы здесь, но она проста толькодля случаев, для которых это работает.Вот случай, для которого это не работает ( demo ):

   $number = 10.046;

    echo number_format($number , 2); // 10.05

Функция number_format хитрая, вы можете решить вашу проблему следующим образом (с php.net):

Для предотвращения округления, которое происходит, когда следующая цифра после последней значащей десятичной дроби равна 5 (упоминается несколькими людьми ниже):

<?php

function fnumber_format($number, $decimals='', $sep1='', $sep2='') {

        if (($number * pow(10 , $decimals + 1) % 10 ) == 5)
            $number -= pow(10 , -($decimals+1));

        return number_format($number, $decimals, $sep1, $sep2);
}

$t=7.15;
echo $t . " | " . number_format($t, 1, '.', ',') .  " | " . fnumber_format($t, 1, '.', ',') . "\n\n";
//result is: 7.15 | 7.2 | 7.1

$t=7.3215;
echo $t . " | " . number_format($t, 3, '.', ',') .  " | " . fnumber_format($t, 3, '.', ',') . "\n\n";
//result is: 7.3215 | 7.322 | 7.321
} ?>
2 голосов
/ 04 февраля 2015

Хотя этот вопрос старый, я выложу другой ответ на случай, если кто-то наткнется на эту проблему, как я.Простое решение для положительных чисел:

function truncate($x, $digits) { 
  return round($x - 5 * pow(10, -($digits + 1)), $digits); 
}

Я проверил его на основе строкового решения с 10 миллионами случайных дробей, и они всегда совпадали.Это не демонстрирует проблему точности, которую делают решения на основе floor.

Расширить его для нуля и негатива довольно просто.

1 голос
/ 03 марта 2015

Чтобы сделать это точно для + ve и -ve номеров, вам нужно использовать:
- функция php floor() для + ve номеров
- функция php ceil() для -ve номеров

function truncate_float($number, $decimals) {
    $power = pow(10, $decimals); 
    if($number > 0){
        return floor($number * $power) / $power; 
    } else {
        return ceil($number * $power) / $power; 
    }
}

причина этого в том, что floor() всегда округляет число вниз , а не к нулю.
т.е. floor() эффективно округляет -ve числа в сторону большего абсолютного значения
например, floor(1.5) = 1 в то время как floor(-1.5) = 2

Поэтому для метода усечения, используя multiply by power, round, then divide by power:
- floor() работает только для положительных чисел
- ceil() работает только для отрицательных чисел

Чтобы проверить это, скопируйте следующий код в редактор http://phpfiddle.org/lite (или аналогичный):

<div>Php Truncate Function</div>
<br>
<?php
    function truncate_float($number, $places) {
        $power = pow(10, $places); 
        if($number > 0){
            return floor($number * $power) / $power; 
        } else {
            return ceil($number * $power) / $power; 
        }
    }

    // demo
    $lat = 52.4884;
    $lng = -1.88651;
    $lat_tr = truncate_float($lat, 3);
    $lng_tr = truncate_float($lng, 3);
    echo 'lat = ' . $lat . '<br>';
    echo 'lat truncated = ' . $lat_tr . '<br>';
    echo 'lat = ' . $lng . '<br>';
    echo 'lat truncated = ' . $lng_tr . '<br><br>';

    // demo of floor() on negatives
    echo 'floor (1.5) = ' . floor(1.5) . '<br>';
    echo 'floor (-1.5) = ' . floor(-1.5) . '<br>';
?>
1 голос
/ 12 января 2011

Я вижу другой способ выполнить это:

function trunc($float, $prec = 2) {
    return substr(round($float, $prec+1), 0, -1);
}

Но это не лучше, чем любой другой ... round тоже можно заменить на sprintf.

1 голос
/ 12 января 2011

Функция round() имеет параметр точности, а также третий параметр для указания метода округления, но вы правы, усечение не выполняется.

Что вы ищетеявляются функциями floor() и ceil().Недостатком является то, что у них нет параметра точности, поэтому вам придется сделать что-то вроде этого:

$truncated = floor($value*100)/100;
0 голосов
/ 03 сентября 2018

Чтобы преодолеть проблему с floor для позитивов и ceil для негативов , вы можете просто разделить число на единицу и получить целую часть, используя intdiv(). А проблему с плавающей запятой можно решить с помощью round() после умножения.

Итак, это должно работать:

<?php
function truncate($number, $precision = 2) {
    $prec = 10 ** $precision;
    return intdiv(round($number * $prec), 1) / $prec;
}

Выходы:

>>> truncate(45.8976)
=> 45.89
>>> truncate(-45.8976)
=> -45.89
>>> truncate(3)
=> 3
>>> truncate(-3)
=> -3
>>> truncate(10.04)
=> 10.04
>>> truncate(-10.04)
=> -10.04
0 голосов
/ 10 октября 2016

Причиной этого является внутреннее двоичное представление чисел с плавающей точкой. Данное значение сверху 10.04 должно быть внутренне представлено как степень основания 2.

Потенциальный показатель степени для части с плавающей запятой равен ln(0,04)/ln(2) = -4,64.... Это уже показывает, что 10.04 не может быть представлен точно как двоичное число. В итоге получается что-то вроде 10.04 ~ 2^3 + 2^1 + 2^-5 + 2^-7 + 2^-11 + ... с максимальной точностью для части с плавающей запятой.

Это причина того, что 10.04 на самом деле немного меньше и может быть представлен как 10.039....

Чтобы обойти это поведение, есть две возможности - либо возиться напрямую со строковыми операциями, либо использовать библиотеку произвольной точности, например BcMath для PHP.

<?php
function test($value)
{
    // direct string operations
    $fromString = (float)preg_replace(
        '#(?:(\.\d{1,2})\d*)?$#',
        '${1}',
        (string)$value
    );
    // using arbitrary precision
    // @see http://php.net/manual/en/function.bcadd.php
    $fromBcMath = (float)bcadd(
        (string)$value,
        '0',
        2
    );

    var_dump($fromString, $fromBcMath);
}

test(3);
test(4.60);
test(10.04);
test(45.8976);

Будет сгенерирован вывод для кода выше (я добавил разделительные строки для удобства чтения):

double(3)
double(3)

double(4.6)
double(4.6)

double(10.04)
double(10.04)

double(45.89)
double(45.89)
...