PHP 小数运算(取整、四舍五入、算术运算等)


PHP 小数运算有向上取整、向下取整、四舍五入、丢弃小数等,还有加减乘除等基本运算


正文

取整

1、向上取整,有小数位就整数部分加1:

ceil(5/2); // 输出 3
ceil(-2.9); // 输出 -2

2、向下取整:

floor(5/2); // 输出 2
floor(-2.1); // 输出 -3

3、丢弃小数部分:

intval(5/2); // 输出2 
(int)2.5  // 输出2 

四舍五入

round(2.5); // 输出 3
round(2.4); // 输出 2
round(-2.4); // 输出 -2
round(-2.5); // 输出 -3
round(1.955, 2); // 输出 1.96
round(1.954, 2); // 输出 1.95
round(-1.955, 2); // 输出 -1.96
round(-1.954, 2); // 输出 -1.95
round(1.9545, 2); // 输出 1.95
round(-1.9545, 2); // 输出 -1.95 

任意精度数学运算

因为一次减法操作38.4 - 30,发现出现了8.399999999999999这样的结果,感觉问题出现的还挺意外的, 所以需要研究解决是怎么回事、怎么解决。

php的bug?不是,这是所有计算机语言基本上都会遇到的问题,所以基本上大部分语言都提供了精准计算的类库或函数库。

要搞明白这个原因, 首先我们要知道浮点数的表示(IEEE 754 :是IEEE二进位浮点数算数标准的编号)。

PHP 官方手册解释如下:

浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。 非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。永远不要相信浮点数结果精确到了最后一位, 也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数 或者 gmp 函数。

这里的关键在于,浮点数的小数用二进制的表示,以64位的长度(双精度)为例, 会采用1位符号位(E)、11指数位(Q),、52位尾数(M)表示(一共64位)。

符号位:最高位表示数据的正负,0表示正数,1表示负数。
指数位:表示数据以2为底的幂,指数采用偏移码表示
尾数:表示数据小数点后的有效数字。

这里的关键点就在于, 小数在二进制的表示, 小数如何转化为二进制呢

算法是乘以2直到没有了小数为止。这里举个例子,0.9表示成二进制数:

0.9*2=1.8 取整数部分 1
0.8(1.8的小数部分)*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 0
.........

0.9 二进制表示为(从上往下): 11100100100100……

注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。 很显然,小数的二进制表示有时是不可能精确的 。 其实道理很简单,十进制系统中能不能准确表示出 1/3 呢?同样二进制系统也无法准确表示 1/10。 这也就解释了为什么浮点型减法出现了”减不尽”的精度丢失问题。

0.58的二进制表示基本上(52位)是:0010100011110101110000101000111101011100001010001111;
0.57的二进制表示基本上(52位)是:0010001111010111000010100011110101110000101000111101;

而两者的二进制, 如果只是通过这52位计算的话,分别是:

0.58 -> 0.57999999999999996
0.57 -> 0.56999999999999995

至于0.58 * 100的具体浮点数乘法, 我们模糊的以心算来看,0.58 * 100 = 57.999999999,那么 intval 后,就是57了。

换句话说:我们看到十进制小数,在计算机内存储的不是一个精确的数字,也不可能精确。所以在数字加减乘除后出现意想不到的结果。

基于以上原因,所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。 如果确实需要更高的精度,应该使用BCMath任意精度数学函数或者 gmp 函数。

可用方法

  1. 通过乘100的方式转化为整数加减,然后再除以100转化回来。
  2. 使用number_format转化成字符串,然后再使用(float)强转回来。
  3. php提供了高精度计算的函数库,实际上就是为了解决这个浮点数计算问题而生的。

BCMath主要函数

简介:对于任意精度的数学,PHP提供了支持用字符串表示的任意大小和精度的数字的二进制计算,最多为2147483647-1(或0x7FFFFFFF-1)。

BCMath 主要函数有:

  • bcadd — 将两个高精度数字相加
  • bccomp — 比较两个高精度数字,返回-1, 0, 1
  • bcdiv — 将两个高精度数字相除
  • bcmod — 求高精度数字余数
  • bcmul — 将两个高精度数字相乘
  • bcpow — 求高精度数字乘方
  • bcpowmod — 求高精度数字乘方求模,数论里非常常用
  • bcscale — 配置默认小数点位数,相当于就是Linux bc中的”scale=”
  • bcsqrt — 求高精度数字平方根
  • bcsub — 将两个高精度数字相减

函数原型,如:

bcsub ( string $left_operand , string $right_operand [, int $scale = 0 ] ) : string
bcadd ( string $left_operand , string $right_operand [, int $scale = 0 ] ) : string

使用BCMath函数,需要给php安装BCMath拓展库。

举例:

echo bcsub(38.4, 30, 6);  // 8.400000

有关BCMath相关内容,看这里 https://www.php.net/manual/zh/book.bc.php

计算原理

浮点数的处理原理就是把数字转为字符串,然后用字符串一位一位运算,再把结果拼接起来返回就行了

//就是小学时候学的手算模式
 0.58(string)
+0.58(string)
_______
 1.16(string)

 3(string)
*0.58(string)
_______
 1.74(string)
小结

通过浮点数精度的问题,了解到浮点数的小数用二进制的表示。

对于浮点数的运算,使用BCMath库来处理,数据库最好存整数,如果不能存整数, 可以用decimal多存一点位数,(50,15)保留15位小数,然后用BCMath运算,就可以保证万无一失了。

实战

整数保留小数位

一次项目开发中碰到的情况,一般网上交易,货币单位都是整数到分,数据库可能也是按分单位整数保存的,但显示时要以元为单位, 保留两位小数到分,怎么实现呢。

可以使用 sprintf() 格式化函数。

<?php
$pay_total = 123;  // 单位:分

// 转为元
$a = sprintf("%.2f", $pay_total * 0.01);  // 输出 1.23

sprintf(format, arg1, arg2, arg++) 参数说明:

参数     描述
format     必需。规定字符串以及如何格式化其中的变量。

        可能的格式值:
        
            %% - 返回一个百分号 %
            %b - 二进制数
            %c - ASCII 值对应的字符
            %d - 包含正负号的十进制数(负数、0、正数)
            %e - 使用小写的科学计数法(例如 1.2e+2)
            %E - 使用大写的科学计数法(例如 1.2E+2)
            %u - 不包含正负号的十进制数(大于等于 0)
            %f - 浮点数(本地设置)
            %F - 浮点数(非本地设置)
            %g - 较短的 %e 和 %f
            %G - 较短的 %E 和 %f
            %o - 八进制数
            %s - 字符串
            %x - 十六进制数(小写字母)
            %X - 十六进制数(大写字母)
        
        附加的格式值。必需放置在 % 和字母之间(例如 %.2f):
        
            + (在数字前面加上 + 或 - 来定义数字的正负性。默认情况下,只有负数才做标记,正数不做标记)
            ' (规定使用什么作为填充,默认是空格。它必须与宽度指定器一起使用。例如:%'x20s(使用 "x" 作为填充))
            - (左调整变量值)
            [0-9] (规定变量值的最小宽度)
            .[0-9] (规定小数位数或最大字符串长度)
        
        注释:如果使用多个上述的格式值,它们必须按照上面的顺序进行使用,不能打乱。
arg1     必需。规定插到 format 字符串中第一个 % 符号处的参数。
arg2     可选。规定插到 format 字符串中第二个 % 符号处的参数。
arg++     可选。规定插到 format 字符串中第三、四等等 % 符号处的参数。

也可以使用 number_format(number,decimals,decimalpoint,separator) 函数通过千位分组来格式化数字。

<?php
echo number_format("1000000")."<br>";  // 1,000,000
echo number_format("1000000", 2)."<br>";  // 1,000,000.00
echo number_format("1000000", 2, ",", ".");  // 1.000.000,00






参考资料

PHP 手册 函数参考 数学扩展 https://www.php.net/manual/zh/refs.math.php

PHP 手册 函数参考 数学扩展 Math Math 函数 https://www.php.net/manual/zh/ref.math.php

PHP 手册 函数参考 数学扩展 BCMath 任意精度数学 https://www.php.net/manual/zh/book.bc.php

bcsub https://www.php.net/manual/en/function.bcsub.php

bcadd https://www.php.net/manual/en/function.bcadd.php

http://blog.csdn.net/nsrainbow/article/details/7065399

ceil

floor

round

intval

Math 函数

BCMath 任意精度数学 https://www.php.net/manual/en/reserved.constants.php

浮点数计算 https://www.cnblogs.com/phpfensi/p/8143367.html

php对于浮点数的精确运算 https://blog.csdn.net/weihuiblog/article/details/79134771

PHP sprintf() 函数 https://www.runoob.com/php/func-string-sprintf.html

PHP number_format() 函数 https://www.runoob.com/php/func-string-number-format.html

MySQL 中float、double、decimal说明 https://ibaiyang.github.io/blog/mysql/2022/06/17/MySQL-中float-double-decimal说明.html

PHP浮点数精度问题 https://mp.weixin.qq.com/s/l6PuENW6SJmNinCoBZYpGw


返回