学习PHP中的任意精度扩展函数

今天来学习的是关于数学方面的第一个扩展。对于数学操作来说,无非就是那些各种各样的数学运算,当然,整个程序软件的开发过程中,数学运算也是最基础最根本的东西之一。不管你是学得什么专业,到最后基本上都会要学习数据结构与算法,而算法其实就是研究的如何利用数学来优化各种排序和查找能力。PHP 在底层已经帮我们准备好了很多的数学计算函数,就让我们一一来学习吧。
什么是精度问题 关于精度问题,可能很多做过金融方面的小伙伴都不会陌生。特别是前端的同学,如果你在 js 中执行 1.1+2.2 ,获得的结果往往不会如你所愿。这就要说到浮点数的存储问题了。我们都知道,在程序世界中,任何数据其实在底层都是以二进制的形式存在的。而浮点数,则由于小数点的存在,在存储时更为复杂,所以就会经常出现这类精度丢失的问题。
但是很多人会很奇怪,在 PHP 中直接执行 1.1+2.2 的结果是正确的呀,好像并不存在这种精度丢失的问题。呵呵,那只能说您 too young to simple 了。精度丢失的问题并不是哪个语言的问题,基本上所有语言都会存在这样的问题,只是表现的形式不一样。
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.99999999999999 这样的数,如果我们直接 echo 的话,会经过字符串强转,这个会直接输出 58 ,但如果是经过 int 强转的话,不管是 inval() 还是 (int) ,都会按照 int 强转的舍弃小数的规则进行转换。于是,结果就变成了 57 了。
通过直接的 echo 经常会让我们感觉到 PHP 中貌似不会出现精度丢失的问题,但其实这个问题还真是存在的。在很多情况下,比如存入数据库,或者转换成 json 格式就会发现问题。如果想要精确地计算,就可以使用 bc 扩展相关的函数,也就是我们最后演示的那个 bcmul() 函数。它的作用就是第一个参数乘以第二个参数,获得的结果也是高精度的,也就是精度准确的结果。
接下来我们通过 json 格式的转换来看看加减乘除各类情况下的精度问题。
echo json_encode([ 'a1' => $a, // "a1":0.58 'a2' => $a * 100, // "a2":57.99999999999999 'a3' => intval($a * 100), // "a3":57 'a4' => floatval($a * 100), // "a4":57.99999999999999 'a5' => floatval($a), // "a5":0.58 'a6' => intval(bcmul($a, 100)), // "a6":58'a7' => 1.1 + 2.2, // "a7":3.3000000000000003 'a8' => floatval(bcadd(1.1, 2.2, 10)), // "a8":3.3'a9' => 2 - 1.1, // "a9":0.8999999999999999 'a10' => floatval(bcsub(2, 1.1, 10)), // "a10":0.9'a11' => floatval($a * 100 / 10), // "a11":5.799999999999999 '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.2100000000000002 'a16' => bcpow(1.1, 2, 30), // "a16":"1.210000000000000000000000000000"'a17' => sqrt(1.1), // "a17":1.0488088481701516 'a18' => bcsqrt(1.1, 30), // "a18":"1.048808848170151546991453513679"]), 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 也是摘自他的博客中的示例。
测试代码:
https://github.com/zhangyue0503/dev-blog/blob/master/php/202012/source/7.学习PHP中的任意精度扩展函数.php
参考文档:
https://www.php.net/manual/zh/book.bc.php
https://www.laruence.com/2013/03/26/2884.html 【学习PHP中的任意精度扩展函数】各自媒体平台均可搜索【硬核项目经理】

    推荐阅读