杂记 › 技术笔记 › 小数位格式化实测:负数取整只剩两个方向,1.005 被舍成 1.00

小数位格式化实测:负数取整只剩两个方向,1.005 被舍成 1.00

一、回填之后,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,三缺一。

-4 -3 -2 -1 输入 -2.5 四舍五入 -3 离零 向下取整 -2 朝零 向上取整 -3 离零 截断 -2 朝零 floor 与 trunc 同值,round 与 ceil 同值

图 1:-2.5 取 0 位只剩两个落点

三、1.005 保留 2 位为什么得到 1.00

这一段没有方向可言,只有二进制近似值落在哪一侧。第三位小数正好是 5 的 8 个样本,取 2 位、四舍五入,5 个被舍下、3 个被进上:

输入乘 100 的中间值输出
1.005100.499999999999991.00
8.995899.49999999999998.99
1.015101.499999999999991.01
1.045104.51.05
1.055105.51.06
2.675267.52.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 位、选截断。

输入 输出 判定 1.2.3 → 1.2 已格式化 10.0.1.25 → 10 已变成整数 2.2.3 → 2.2 已格式化 取 1 位、截断:count=3 skipped=0 没有一行报错,数据已经变形

图 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。列表非空不等于跑成功,那两个数才是要读的东西。

📎 相关文章

文本合并后行数对不上:缺尾换行、被吃掉的 BOM 和不可靠的顺序 2026-10-26 Excel 粘贴转 JSON:日期、长数字和精度在「粘贴那一刻」就已经变了 2026-10-26 条形码解码只吐原文:EAN-13 校验位、UPC-A 的前导零与 CODE128 的模 103 2026-10-26 二维码解码实测:纠错等级救不了遮挡,每模块 2 像素才是生死线 2026-10-26 字频还是词频:同一句中文,一份 23 项榜单和一份空表 2026-10-26