0.1 转成二进制,写出来是 0.00011001100110011001100110011001,后面还有没写完的 0011 一直在循环。
我用一个单文件网页工具跑了 21 组边界用例,页面自己报「自检结果:21 / 21 通过」。
我又用 Python 另写了一份实现,把 40 组输入逐位对着页面比,两边输出零处不一致。
比对的路上撞到三个真实的边界问题,其中一个连 2 的幂都会踩。
在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-number-base-converter
一、先说结论:几个容易写错的数
| 输入 | 二进制 | 八进制 | 十进制 | 十六进制 | 容易错在哪 |
|---|---|---|---|---|---|
| 255 | 1111 1111 | 377 | 255 | FF | 二进制按 4 位一组 |
| 0.5 | 0.1 | 0.4 | 0.5 | 0.8 | 分母是 2 的幂,除得尽 |
| 0.1 | 0.000110011… 截断 | 0.06314631… | 0.1 | 0.1999999… | 除不尽,要标截断 |
| -128 | -1000 0000 | -200 | -128 | -80 | 负数多一张补码卡 |
| 9007199254740993 | 10 0000…0001 | 400000000000000001 | 9007199254740993 | 20000000000001 | 2 的 53 次方加 1 不丢精度 |
表里的值我逐格对过页面输出,也逐格对过 Python 的结果。
二、为什么 0.1 转二进制除不尽
判据只有一条:分母是 2 的幂,二进制才写得完。
0.5 是 1/2,0.25 是 1/4,0.125 是 1/8,这三个都除得尽。
0.1 是 1/10,分母 10 里带一个因子 5。
二进制换算的每一步是乘 2 取整数位,因子 5 永远除不干净,于是 0011 无限循环。
页面按 32 位截断,标一个「已截断」,实际写下来的值是 0.099999999976716935634613037109375。
和真值差 1.4e-10,我按 3 / 21474836480 复算过一遍。
这跟 0.1 加 0.2 不等于 0.3 是同一件事:不是运算坏了,是这个数在二进制里根本没有有限表示。
三、准备环境:进入码道 Web
浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz
登录后直接在对话窗口输入需求,不用装软件。
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。
本文用 WebUI 版演示。
四、需求原文:把用例先写死
需求我写成 7 条,重点是把要验的数写进要求里。
- 一个输入框加一个进制选择,2/8/10/16 四选一,实时给出四种进制结果,二进制按 4 位一组加空格。
- 支持小数,0.1 这种除不尽的小数显示 32 位二进制并标「已截断」。
- 支持负数,额外给出 32 位和 64 位补码。
- 支持大整数,用 BigInt,超过 2 的 53 次方的整数不能丢精度。
- 非法输入给中文提示,并保留上一次的有效结果。
- 页面顶部一个「运行自检」按钮,跑 20 组固定用例并显示通过数。
- 深色界面,中文标签,全部本地计算。
最后单独加一句:完成后打开预览,让我直接看到运行效果。
五、页面上有哪几块
页面就四块,从上到下。
顶部是工具栏,一个「运行自检」按钮加一行统计。
下面一行是输入区,左边填数值,右边选输入进制。
中间是四张结果卡,二进制、八进制、十进制、十六进制各一张。
最下面是补码卡,只在输入负数时出现,给出 32 位和 64 位两串。
自检跑起来以后,底部会多出一块用例明细,每一行一条,绿色是通过。
六、验证与踩坑
我把它落到本地打开,点了一次「运行自检」。
页面顶部写着「自检结果:21 / 21 通过」,明细里 21 行全是绿勾。
需求里写的是 20 组,它自己扩到 21 组,多出来的一条是超长输入。
光看它自己说自己对不算数,我又写了一份 Python 实现去对。
两边比了 40 组输入,覆盖整数、小数、负数、大整数、2 的幂和四种进制的混合输入,逐位对照零处不一致。
大整数那条确实靠 BigInt 撑着。
9007199254740993 是 2 的 53 次方加 1,换成普通的 Number 会变成 9007199254740992,页面给出的是原值。
接着是三个真实问题。
第一个在补码卡上。32 位补码没有范围校验,绝对值超过 2 的 31 次方就开始失真。
| 输入 | 页面给的 32 位补码 | 问题 |
|---|---|---|
| -1 | 11111111111111111111111111111111 | 正常 |
| -128 | 11111111111111111111111110000000 | 正常 |
| -9007199254740993 | 11111111111111111111111111111111 | 和 -1 一模一样 |
| -4294967296 | 00000000000000000000000000000000 | 变成全 0 |
-4294967296 是 -2 的 32 次方,取模之后正好归零,页面把这个全 0 的串当成补码显示出来,没有任何提示。
第二个在截断标注上。只有二进制卡有「已截断」标,八进制、十进制、十六进制一样截断,却一声不响。
四个进制的小数位数上限分别是 32、32、60、32 位,换成实际精度是 32、96、199、128 个二进制位。
同一个 0.1,从四种进制看过去的精度并不一样。
第三个在分组上。整数部分按 4 位一组,小数部分没分,0.1 的二进制尾巴是一长串没断开的数字。
失败路径我挨个试过,报错原文有四句。
二进制里填 2,说包含二进制不合法的字符。
填 1.2.3,说小数点不能多于一个。
清空输入框,说请输入数值,输入为空。
粘贴 1001 个字符,说输入过长,超过 1000 个字符。
报错的同时结果栏不清空,我实测输入 255 之后切到二进制填 2,四张卡片还是 255 那一组值。
这算有意为之,需求里写了要保留上一次有效结果。
最后一条最小:切到 0.5 时标签看不见了,但它在页面结构里的文字还留着上一次的「已截断」,只是被隐藏。
本地复现很简单,把 index.html 存下来双击就能开。
要起个本地服务也行:
python3-mhttp.server8080浏览器访问 http://localhost:8080/index.html 就是同一份页面。
七、使用码道的体会
把要验的数写进需求,是这次最值的一条。
21 组用例是我在需求里点名的,它照着做进了自检面板,还自己补了一条超长输入。
需求里那句「完成后打开预览」也省事,省掉了来回描述效果的时间。
不足的地方也有:补码的范围校验没做,截断标注只做了二进制这一路。
这两条我都留在文章里,没有回头去改它。
八、直接下载试玩
整页只有一个 index.html,没有任何外部依赖,拷走双击就能用。
在线案例与源码在这里:https://atomgit.com/deli007/demo_park/tree/main/codearts-number-base-converter
要查某个数的二进制或者补码,把输入框当计算器用就行。