Сократить числа с плавающей точкой с помощью PHP

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

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

Ответ 1

floor будет делать по вашему желанию.

floor(45.8976 * 100) / 100;

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

Ответ 2

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

/**
 * @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;
}

Ответ 3

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, но и во всех langauges, в зависимости от относительной ошибки используемого langauge. PHP использует формат двойной точности IEEE 754, который имеет относительную погрешность около 1.11e-16. (ресурс)

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

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

Ответ 4

Чтобы усечь числа, нужно использовать "лучшее" (я взял здесь номер, который работает для моего примера):

$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
} ?>

Ответ 5

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

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

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

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

Ответ 6

Чтобы сделать это точно для обоих + 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() эффективно округляет числа до большего абсолютного значения
например 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>';
?>

Ответ 7

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

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

$truncated = floor($value*100)/100;

Ответ 8

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

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

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

Ответ 9

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

function truncate($val,$p = 0)
{
  $strpos = strpos($val, '.');
  return $p < 0 ? round($val,$p) : ($strpos ? (float)substr($val,0,$strpos+1+$p) : $val);
}

... Я думаю, что это просто и быстро использовать.

Ответ 10

Причиной этого является внутреннее двоичное представление чисел с плавающей запятой. Данное значение сверху 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....

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

<?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)

Ответ 11

Чтобы преодолеть проблему с 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

Ответ 12

Поскольку функции BCMath действительно усекаются, мое решение - умножить на 1 с помощью bcmul и преобразовать полученную строку в число с плавающей точкой. Таким образом мы избегаем делений и умножения, которые могут привести к ошибкам округления:

+bcmul(strval(45.8976), '1', 2);

Я использую входные значения в качестве строки, потому что я читаю с использованием чисел с плавающей точкой, BCMath может выводить странные результаты

Ответ 13

Вы можете попробовать это:

bcdiv($inputNumber, '1', $precision = 2);