今天我们要学习数学的第一次拓展。对于数学运算来说,无非就是各种数学运算。当然,数学运算也是整个程序软件开发过程中最基础、最基础的事情之一。不管学什么专业,说到底基本都要学习数据结构和算法,算法其实就是关于如何利用数学优化各种排序和搜索能力。PHP已经在底层为我们准备了很多数学计算函数,让我们一个一个来学习。
什么是精度问题
关于准确性,很多从事过金融工作的小伙伴可能都比较熟悉。尤其是前端学生,如果在js中实现1.1 2.2,结果不会如你所愿。这就把我们带到了浮点数的存储。众所周知,在程序世界中,任何数据实际上都是以二进制形式存在于底层的。另一方面,由于小数点的存在,浮点数的存储更加复杂,所以这种精度损失经常发生。
但是很多人会惊讶于PHP中直接执行1.1 2.2的结果是正确的,似乎不存在精度损失这样的问题。呵呵,那你只能说太年轻不简单。准确性的丧失不是哪种语言的问题。基本上所有语言都有这样的问题,只是表现形式不同。
bc 精度运算
我们先来看看在PHP环境下如何展现精度损失。
$ a=0.58
echo $a * 100,PHP _ EOL//58
echo intval($a * 100),PHP _ EOL//57
echo (int) ($a * 100),PHP _ EOL//57
echo intval(bcmul($a,100)),PHP _ EOL//58
我们定义了一个变量$a,它的内容是0.58。这时候我们直接把他乘以100,结果好像没问题。但是如果我们强转换成int类型,就会出现问题。显然是58。为什么是57?
其实经过浮点运算,结果不是58,而是像57这样的数字。如果我们直接回显,它会被字符串强转换,这个会直接输出58。但是如果用int进行强转换,无论是inval()还是(int),都会按照用int丢弃小数的规则进行转换。所以,结果变成了57。
通过直接回显,我们经常觉得PHP中似乎不存在精度损失的问题,但实际上这个问题确实存在。例如,在许多情况下,将其保存在数据库中或将其转换为json格式会发现问题。如果想要精确计算,可以使用bc来扩展相关函数,也就是我们在最后演示的bcmul()函数。它的作用是将第一个参数乘以第二个参数,得到的结果也是高精度的,也就是精确的结果。
接下来,我们通过json格式的转换来看看各种情况下加减乘除的准确性。
echo json_encode([
a1'=$a,///' a1 ' :0.58
' a2'=$a * 100,///' a2 ' : 999999999996
a3'=intval($a * 100),//'a3':57
a4'=浮动版(a * 100美元),//' a4 ' : 999999999996
a5'=floatval($a),//'a5':0.58
a6'=intval(bcmul($a,100)),//'a6':58
' a7'=1.1 2.2,///' a7 ' :3.300000000000003
' a8'=floatval(bcadd(1.1,2.2,10)),//'a8':3.3
' a9'=2 - 1.1,///' a9 ' : 99999999999
a10'=floatval(bcsub(2,1.1,10)),//'a10':0.9
' a11'=floatval($a * 100/10),//' a11 ' 59960 . 99999999961
a12'=floatval(bcdiv($a * 100,10,10)),//'a12':5.8
a13'=10 % 2.1,///' a13 ' :0
a14'=bcmod(10,2.1),//'a14':'1 '
a15'=pow(1.1,2),//' a15 ' :1.2100000000002
a16'=bcpow(1.1,2,30),//' a16 ' : ' 1.210000000000000000000000000000000000 '
a17'=sqrt(1.1),///' a17 ' 38860 . 88888888861
a18'=bcsqrt(1.1,30),//' a18 ' : ' 1 . 48381 . 88888888861
51546991453513679" ]), PHP_EOL;通过这段代码大家应该就能清楚地看到 PHP 中的精度丢失问题是否存在了。json_encode() 在转换数据的时候会根据字段的类型进行转换,所以精度问题会比较明显,这也是很多同学在后端计算的时候明明没有问题,但通过 json 输出到前端就会发现数据发生了精度问题的原因。
a1~a6 就是我们第一段测试代码的内容,可以很明显地看到普通地使用 $a * 100 的结果真的是 57.99999999999999 了吧。
a7、a8 是加法的演示,怎么样,在 PHP 中,1.1+2.2 的结果其实也和 JS 中是一样的吧,通过 bcadd() 就可以处理加法的精度问题。同理,a9、a10 是减法的问题,通过 bcsub() 就可以获得减法的高精度计算结果。bcdiv() 则是用于处理除法。注意,这几个函数都有第三个参数,它表示的是保留小数点的位数,我们都给了保留 10 位小数点,目的是希望如果出现丢失精度的问题可以和原计算比对。
bcmod() 的余数计算,对应的也就是 % 计算符号的作用。正常情况下,10 % 2 的结果为 0 是正常的,但这里我们计算的是 10 % 2.1 结果也是 0 ,而在使用 bcmod() 之后,结果为 1 ,这才是正确的结果。bcpow() 是乘方的计算,对应的是普通函数中的 pow() 函数,同样在这里我们在普通函数的计算中 1.1 的 2 次方出现了精度问题,使用 bcpow() 我们显示 30 位的小数也没有找到精度异常。这里需要注意的是,bcpow() 如果指定了小数位数,是会显示出来的,即使计算结果是没有小数的,也会以 0 全部显示出来。而上面其它的函数则不会这样,只会在确实有小数的情况下才显示出来。
最后则是 bcsqrt() 函数,也就是二次方根,这个没有找到有溢出的数可以供我们测试,如果有使用过并发现过溢出的小伙伴可以留言哦。
比较函数
上面说完了各种精度计算的函数,接下来我们看一下数字比较的问题。
echo bccomp(1, 2), PHP_EOL; // -1
echo bccomp(1.00001, 1, 3), PHP_EOL; // 0
echo bccomp(1.00001, 1, 5), PHP_EOL; // 1
bccomp() 函数就是用来根据小数点位数进行精度比较的函数。它的返回结果是如果参数1小于参数2返回 -1 ,大于返回 1,等于则返回 0 。第三个参数用户确定比较到哪一位。在这个例子中,我们可以看到,如果只比较到第三位小数的话,1.00001 和 1 的结果是相等的。而如果比较到第五位小数的话,它们的差异就体现出来了。
设置小数点及 bcpowmod 函数
最后我们再看两个函数。
bcscale(30);
echo bcmod(bcpow(5, 2), 2), PHP_EOL; // 1.000000000000000000000000000000
echo bcpowmod(5, 2, 2), PHP_EOL; // 1.000000000000000000000000000000
bcscale() 是在全局设置小数点的位数。设置这个函数后,上面介绍过的所有函数如果不写第三个小数点位数函数的话,都会以 bcscale() 设置的为准。
bcpowmod() 函数的作用就和第二行的测试代码一样,就是先进行一次 bcpow() 再进行一次 bcmod() 。它的使用场景不多,不过写法很方便。
总结
今天的内容除了 bc 相关的计算函数之外,也讲到了精度问题这个各种语言都存在的问题。其实说实话,我们在日常开发中,对于金额这类带小数点的数据,最好都是以分为单位进行存储。也就是说,在后台,保存和计算的数据都是整型的数据,在前端展示的时候,直接除 100 再保留两位小数就可以了。这样就可以极大地保证数据的精度不会丢失。
另外,关于 PHP 中精度问题相关的参考大家可以看看下方第二个链接中鸟哥博客上的说明。我们的例子 0.58 * 100 也是摘自他的博客中的示例。

2021-11-07 16:27:25