问题出现:
在项目中之前完成的一个精灵汇总属性展示的需求,今天测试突然提了个bug:
如
某A号精灵 攻击力为 :20.50
某B号精灵 攻击力为 :17.98
汇总的总攻击力应该是: 38.48,但实际上得到的是 38.47
实现代码大概是这样的:
local num1 = 38.48
print("num1 = "..num1) -- num1 = 38.48
local num2 = math.floor(num1 * 100)/100
print("num2 = "..num2) -- num2 = 38.47
Lua 版本:
由于Lua中的Number类型实际上是double(双精度浮点型),在为小数的时候应该特别注意:两个小数相等,不能用等号,而应该用它们的差的绝对值小于一个很小的数(比如math.abs(a - b) < 10e-6)!在程序中如何判断两个浮点数相等
因为电脑是用二进制存储数据的,而类似0.1这样的数字在电脑里存储起来其实是一个很长的数字,在二进制里0.1可能是一个无限不循环小数,所以我们会在一定程度上截取下来,截取之后就难免导致了实际值和我们存储的值有很小很小的差异!
这就导致了5562.9999999997和5563在我们看来是相等的两个数,在进行math.floor之后就出现了偏差,因为math.floor(5562.9999999997)会向下取整从而等于5562!
当时用的规避方式就是做这种运算的时候先保留2位有效数字 然后再做math.floor
思考:
一、浮点机制的理解偏差
看下面这段代码:
print("num1 = ",1.99999999999995)
print("num2 = ",math.floor(1.9999999999999999))
不过这里并不能说明math.floor是异常的。因为这里其实是浮点型的机制引起的"理解偏差",浮点型在内存中其实是一个2进制的分数(C#本质论,值类型那一章有详细讲解,一般的C++书应该也会讲),我们将其转换成十进制进行输出或者其他运算操作时,2进制分数和十进制小数无法严格一一对应,所以会有一个取近似值的操作,特别是精度值溢出后,甚至会直接做四舍五入和截取的操作。
所以这里其实不是math.floor问题,而是lua直接将1.99999999999999999,直接当成了2来处理(注意第一个tostring输出时,已经将原值认为成2了)。那么1.99999999999999999在lua中被识别成了2,math.floor截取也是2,结果还是正确的。
二、string.format()的 “四舍六入五成双”
(1)被修约的数字小于5时,该数字舍去;
(2)被修约的数字大于5时,则进位;
(3)被修约的数字等于5时,要看5前面的数字,若是奇数则进位,若是偶数则将5舍掉,即修约后末尾数字都成为偶数;若5的后面还有不为“0”的任何数,则此时无论5的前面是奇数还是偶数,均应进位。
local num1 = 1.551111111111111
local num2 = 1.550000000000000
local num3 = 1.650000000000000
local num4 = 1.661111111111111
local num5 = 1.666666666666666
print("原值:"..num1.."。取一位小数后:"..string.format("%.1f", num1))
print("原值:"..num2.."。取一位小数后:"..string.format("%.1f", num2))
print("原值:"..num3.."。取一位小数后:"..string.format("%.1f", num3))
print("原值:"..num4.."。取一位小数后:"..string.format("%.1f", num4))
print("原值:"..num5.."。取一位小数后:"..string.format("%.1f", num5))
-- 原值:1.5511111111111。取一位小数后:1.6
-- 原值:1.55。取一位小数后:1.6
-- 原值:1.65。取一位小数后:1.6
-- 原值:1.6611111111111。取一位小数后:1.7
-- 原值:1.6666666666667。取一位小数后:1.7
原值:1.5511111111111 处理后:1.6 分析 – 原值1.55,5后面有值,进了一位
原值:1.55 处理后:1.6 分析 – 1.55 – 5后面没有值,但是5前面还有个奇数5,进了一位。
原值:1.65 处理后:1.6 分析 – 1.65 – 5后面没有值,但是5前面有个偶数6,舍弃。
三、使用math.floor模拟进制转换的方案
-- 保留n位小数
function Format3(val, n)
local n = math.pow(10, n or 1)
val = tonumber(val)
return math.floor(val * n) / n
end
print(Format3(math.pi,2)) -- 3.14