【问题标题】:Truncate float numbers with PHP用 PHP 截断浮点数
【发布时间】:2011-06-07 19:20:11
【问题描述】:

当一个浮点数需要在浮点数后截断到某位时,事实证明这并不容易。例如,如果必须截断到该点之后的第二个数字,则数字应该是

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

【问题讨论】:

  • 嗨。你从哪里得到这个功能的?它们是否适合您?你选择了哪一个?为什么?

标签: php floating-point truncate floating-accuracy


【解决方案1】:
function truncNumber($number, $places){
 $strN = preg_split("/\./", (string)$number);
 $num = $strN[0];
 $frac = $strN[1];
 $num .= ($places > 0) ? ".".substr($frac, 0, $places): "";
 return (double)$num;
}

【讨论】:

    【解决方案2】:

    如果你想在 Js 中实现类似的功能,你可以使用以下:

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

    例子:

     function truncate_float($number, $decimals) {
            $power = Math.pow(10, $decimals); 
            if($number > 0){
                return Math.floor($number * $power) / $power; 
            } else {
                return Math.ceil($number * $power) / $power; 
            }
     }
     
     var orig_float = 0.38583929928947889839483;
     var the_float = truncate_float(orig_float,2);
     
     document.getElementById("orig").innerText = orig_float;
     document.getElementById("trunc").innerText = the_float;
    Original: <span id="orig"></span> <br>
    Truncated: <span id="trunc"></span>

    【讨论】:

      【解决方案3】:

      你可以试试这个:

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

      【讨论】:

        【解决方案4】:

        要克服 floor 表示正数ceil 表示负数 的问题,您只需将数字除以 1 并使用 intdiv() 获得整数部分。而浮点问题可以通过使用round()after乘法来解决。

        所以,这应该可行:

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

        【讨论】:

          【解决方案5】:

          我想通过我为我的需要使用的函数添加我对这个答案的贡献,该函数考虑了负值和正值的截断位置。

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

          ...我认为它使用起来简单快捷。

          【讨论】:

            【解决方案6】:

            原因是浮点数的内部二进制表示。上面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)
            

            【讨论】:

              【解决方案7】:

              要截断数字,“最好”是使用(我在这里取了一个适用于我的示例的数字):

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

              【讨论】:

              • 如果 $number 为 10.046,您的代码将返回 10.05,而不是 10.04。
              • 你们是对的人,错过了这种行为,但老实说,我认为这种行为很奇怪,我的意思是“number_format”这个函数的名称让你觉得就像格式化显示只是移动逗号一样。跨度>
              【解决方案8】:
              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。 (resource)

              真正的问题是 floor 函数将浮点值转换为 int 值,例如(int)(10.04 * 100) = 1003 正如我们之前在 floor 函数中看到的那样。 (resource)

              所以为了克服这个问题,我们可以将浮点数转换为字符串,字符串可以准确地表示任何值,然后 floor 函数会将字符串准确地转换为 int。

              【讨论】:

                【解决方案9】:

                要为 +ve -ve 数字准确地执行此操作,您需要使用:
                - +ve 数字的 php floor() 函数
                - -ve 数字的 php ceil() 函数

                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) = 1floor(-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>';
                ?>
                

                【讨论】:

                  【解决方案10】:

                  虽然这个问题很老,但我会发布另一个答案,以防有人像我一样偶然发现这个问题。正数的一个简单解决方案是:

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

                  我针对具有 1000 万个随机分数的基于字符串的解决方案对其进行了测试,它们始终匹配。它没有表现出基于floor 的解决方案的精度问题。

                  将其扩展到零和负数非常简单。

                  【讨论】:

                    【解决方案11】:

                    这是我的解决方案:

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

                    【讨论】:

                    • 没错。我真的不记得为什么我必须在这个函数中添加 abs()
                    • 所以有一些我们不知道是否有必要的东西在我看来不能是“最简单的”(请不要冒犯,只是说你应该改写一点点,并从您的答案中删除不必要的部分以获得更好的反馈)
                    • 注释代码没有帮助。你永远不明白为什么要注释代码。它要么是不需要的(那么它将被删除)或它是必需的(那么它不会被评论)。注释代码块真的很糟糕,改用版本控制(就像在这个网站上,有你的答案的修订版)。还要仔细检查您对strpos 函数的使用。你的用法在我看来是错误的,例如如果没有.。查手册,如果不直接发现是常见的错误。
                    • 顺便说一句,在像 truncate(.49999, 3) 这样的数字的情况下,将“if”与 strpos() 一起使用确实有问题 - 这是因为 strpos('.49999', '.' ) 返回 0 并且不使用与 !== false 的显式比较,该函数将有异常返回。再次感谢您的反馈
                    • 没错,这就是我所关心的。现在看起来好多了!顺便祝贺你在 Stackoverflow 上的第一个答案。
                    【解决方案12】:

                    我看到了另一种执行此操作的方法:

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

                    但它并不比任何其他更好......round也可以替换为sprintf

                    【讨论】:

                      【解决方案13】:

                      round() 函数确实有一个精度参数以及用于指定舍入方法的第三个参数,但你是对的,它不进行截断。

                      您正在寻找的是floor()ceil() 函数。缺点是它们没有精度参数,因此您必须执行以下操作:

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

                      【讨论】:

                        【解决方案14】:

                        floor 会按照你的要求去做。

                        floor(45.8976 * 100) / 100;
                        

                        你不会找到更直接的函数,因为这是一种奇怪的问题。通常你会在数学上四舍五入。出于好奇 - 你需要这个做什么?

                        【讨论】:

                        • 是的,这可能是截断正浮点数的最佳解决方案。要使用负浮点数,应采用数字的 abs() 并在其后添加符号。这是一个来自业务的公式,如果它产生太长的浮点数,则应忽略第二个之后的所有数字,而不是四舍五入的:)。
                        • 如果我们在浮点数之后已经有两位数,但不知道并尝试使用您的代码:echo floor(10.04 * 100) / 100; 将返回 10.03
                        • 与 jampag 相同。回声层(4.60 * 100)/100;将返回 4.59,这是错误的。
                        猜你喜欢
                        • 2013-12-31
                        • 2011-04-29
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-05-12
                        相关资源
                        最近更新 更多