一、回填之后,12.30 再也回不去 12.30
上周把一张对账表统一成 2 位小数,导之前顺手取消了补零。第二天同事来问:3 月那行 12.30 怎么只剩 12.3,两位的账面精度对不上了。丢失发生在格式化的那一刻,回写之后找不到原始串。
那一页很薄:输入框、一根保留小数位数的滑杆、一个舍入方式下拉、补零与千分位两个勾选框,外加「处理数值数」和「跳过无效项」两个计数。滑杆只能停在 0 到 10,一共十一档,默认 2 位;除此之外页面上没有任何关于取整方向、精度损失、科学计数法的提示文案。
二、四个舍入选项在负数上只剩两个方向
下拉里排着四件事:四舍五入、向下取整(地板)、向上取整(天花板)、截断(直接舍去)。把 -2.5 取 0 位,四条分别给 -3、-2、-3、-2——四个名字、两个结果:floor 与 trunc 逐条同值,round 与 ceil 在 .5 上同值,"四种模式给出四种结果"在这里不成立。
原因在唯一的算式里:四个分支都先对绝对值取整,再按符号把负号补回来。取整发生在绝对值轴上,负数上的"向下"其实是朝零,落到 -2。跟原生函数对一遍更清楚:Math.floor(-2.5) 是 -3,页面的向下取整给 -2;Math.round(-2.5) 是 -2,页面的四舍五入给 -3。-2.4 更彻底:-2、-2、-3、-2,三缺一。
图 1:-2.5 取 0 位只剩两个落点
三、1.005 保留 2 位为什么得到 1.00
这一段没有方向可言,只有二进制近似值落在哪一侧。第三位小数正好是 5 的 8 个样本,取 2 位、四舍五入,5 个被舍下、3 个被进上:
| 输入 | 乘 100 的中间值 | 输出 |
|---|---|---|
| 1.005 | 100.49999999999999 | 1.00 |
| 8.995 | 899.4999999999999 | 8.99 |
| 1.015 | 101.49999999999999 | 1.01 |
| 1.045 | 104.5 | 1.05 |
| 1.055 | 105.5 | 1.06 |
| 2.675 | 267.5 | 2.68 |
分界不是"见 5 就进",而是乘完 100 之后那个二进制近似值有没有严格压过 .5:100.49999999999999 在 100.5 之下,舍;104.5 正好落在 .5 上,进。整条链就是乘一次 10 的位数次方、取整、再除回去,没有字符串移位、也没有十进制库参与——代码里那句"修正负数四舍五入"的注释只修了符号,没修精度。钱数只要第三位可能是 5,方向就由不得我。
四、补零一关,尾随零永久丢失
回到开头那次故障。补零默认是勾着的,标签写着「补零(如 3.5 → 3.50)」。勾着时 12.3 取 2 位得 12.30,取消勾选就只出来 12.3;同一批里 100 从 100.00 退回 100,1.005 更直接——连小数都不剩,输出 1。尾随零在数值层面本来就不存在,输出的是字符串,回写后没有任何地方还留着"原本按 2 位记"。
位数 0 时补零完全不介入:5 仍是 5、5.5 截断仍是 5,勾得再坚决也看不到 .00。位数档位本身也不负责补零,12.3 取 4 位、关着补零,输出还是 12.3。
五、批量粘贴:三条版本号全部"成功",其中一条已变成整数
批量入口是真的:输入按换行、半角逗号、全角逗号三种分隔切成 token,一次跑完整批,全角与半角完全等价。我把一列版本号粘进去,取 1 位、选截断。
图 2:三行版本号 count=3、skipped=0,其中一行已被截成整数
没有一行报错,也没有一行被标掉,跳过无效项老老实实是 0,第二条其实只剩一个 10。计数只回答"能不能读成数",不回答"读成了什么"。
混排一批更能说明问题:5 行文本得到 tokens=6、count=4、skipped=2——带千分位的那一行被逗号切成 2 个 token,于是编号 #1 到 #6 比行数多,它是切分之后的下标、不等于行号。同一批里 2024.01.15 被写成 2024.01、1.2.3 被写成 1.20,而「价格 3.5 元」和 v1.2.3 整行判无效:它只认每个 token 开头的那一个数,行中间的 3.5 不会被提取。
「复制全部」是第二条管线,无效项按原文回填:那批粘出来是 1.00|234.56|价格 3.5 元|v1.2.3|2024.01|1.20,屏幕上标红的 2 行变成 2 行原文混在同一份数据里。反过来说,整批全无效时结果面板也不回落空态:三行 abc 给 tokens=3、skipped=3,列表照样显示,只有完全空的输入才退回「请输入数值后自动格式化」。
六、越界的那一头:1e21 与长整数
1e21 取 2 位输出 999999999999999900000.00——只是乘 100 再除 100,原值少了 100000000000;取 0 位时不乘不除,科学计数法原样透出 1e+21。1e-7 取 10 位再补零得到畸形串 1e-7.0000000000,千分位在这种指数串上一个逗号也加不上,同一个数取 2 位则直接是 0.00。
长整数更安静:9007199254740993 进了 parseFloat 就成了 9007199254740992,末位变了而界面看不出区别;123456789012345.67 取 2 位被写成 123456789012345.69,整数部分已经吃掉 15 位有效数字。订单号、卡号这类值不该进这条路,想按别的进制读它们得去进制转换那篇——同一串字符,这一页连 0x10 都不认。
这几个开关在小数位数格式化那一页都能复现:同一份输入只动一个勾选框,把两个计数一起读。跑完一批,我会把输入与输出各粘一份进两段文本对比,确认差额只落在该落的那几行。
FAQ
Q:负数上的"向下取整"到底往哪边?
往零。四个分支都对绝对值取整再补负号,-2.5 取 0 位给 -2,与截断同值;真正的向下是 -3,这一档反倒由"向上取整"给出。
Q:能拿它统一金额保留 2 位吗?
第三位可能是 5 的时候不行:1.005 → 1.00、8.995 → 8.99,而 1.045 → 1.05、2.675 → 2.68。要它当显示层可以,当结算层不行。
Q:整批都是无效数值,为什么列表还在?
回到空态要求两个计数同时为 0,三行 abc 是 count=0、skipped=3。列表非空不等于跑成功,那两个数才是要读的东西。