关于Lua中IEEE754双精度浮点数紧凑精确格式化的技术问询
问题背景
修复Lua JSON实现时,为解决数字转字符串的精度损失问题,把原来的"%.14g"格式化替换成了"%.17g"——"%.17g"能保证IEEE 754双精度浮点数(Lua默认数字类型)转成字符串后,再用tonumber还原得到原数,但会产生冗余输出,比如("%.17g"):format(0.1)会得到"0.10000000000000001"。
为此提出的优化方案是:先用"%.16g"生成紧凑格式的字符串,若tonumber(compact)和原数相等,就返回这个紧凑字符串;否则返回"%.17g"的格式化结果。这个方案能让0.1、0.3这类数得到紧凑的正确表示,但Lua的tostring虽然能生成紧凑格式,却无法保证精度,而且Lua手册也没规范它的输出格式。
针对这个方案和场景,有两个核心问题:
问题1:该方案是否通用,有没有更紧凑的数字表示被遗漏?
这个方案在绝大多数场景下是通用的,但存在极个别极端情况会错过更短的精确表示。
IEEE 754双精度浮点数的最短精确字符串表示,有效数字位数可能小于16位,但%.16g会自动将数字格式化到16位有效数字(截断或补位),这就导致某些可以用更少位数精确表示的数,被生成了更长的字符串。不过这种情况非常罕见,在日常业务中几乎碰不到。
而且你的方案里有校验步骤(判断tonumber(compact)是否等于原数),所以返回的字符串一定是精确的——只是不是绝对最短的。但JSON对数字格式没有“必须最短”的要求,只要精确即可,所以这个方案的通用性完全能覆盖绝大多数使用场景。
如果追求绝对的最短精确表示,这个方案确实会漏掉一些情况,但从工程实用性来看,这种遗漏几乎可以忽略。
问题2:Lua中有没有更高效的紧凑精确数字格式化方式?
有两种更高效的思路:
1. 直接使用tostring(推荐)
虽然Lua手册没有规范tostring的数字输出格式,但主流Lua版本(5.1及以后的官方实现)的tostring实际上会生成最短的精确字符串表示。比如:
print(tostring(0.1)) -- 输出 "0.1" print(tonumber(tostring(0.1)) == 0.1) -- 输出 true
唯一的潜在风险是未来Lua版本可能修改tostring的实现逻辑,但从Lua的历史版本迭代来看,这个行为一直非常稳定。如果能接受这个极小的风险,直接用tostring是最高效的——因为它是底层C实现,比Lua层面做格式化+校验快得多。
如果用的是LuaJIT,它的tostring同样遵循这个逻辑,性能更强,完全可以放心使用。
2. 实现标准的最短精确转换算法
如果必须依赖标准化的实现,不想赌tostring的行为,可以参考IEEE 754的Dragon4算法(专门用于生成浮点数的最短精确字符串表示)。不过自己实现Dragon4比较复杂,你可以找Lua社区里已经实现好的轻量库;或者用循环校验的方式:从%.12g开始,逐步增加有效数字位数,直到找到能被tonumber还原为原数的最短字符串。不过这种方式比你的原方案效率低,只适合对格式有严格要求的场景。
内容的提问来源于stack exchange,提问作者Luatic

