1100除以1.1等于多少?小学数学告诉你1000。但你的JavaScript计算器会吐出999.9999999999999,再向下取整变成999——一张税单差1日元,在日本这叫"无法上线"。
作者Sen在为日本消费税写计算器时踩进这个坑。10%标准税率,8%优惠税率(食品、报纸),双向换算,三种取整模式——80行代码,12个测试用例,却藏着浮点运算的经典陷阱。
浮点数的"1/3困境"
问题不在除法,在0.1。十进制的0.1转成二进制是无限循环小数,就像1/3在十进制里写不完一样。IEEE 754标准下,1 + 0.1实际是1.1000000000000001,1100除以这个数,结果比1000少了头发丝那么一点。
作者的原版代码长这样:
const exclTax = Math.floor(priceInclTax / (1 + rate)) // 1100 / 1.1 → 999.9999... → 999 ❌
Math.floor一巴掌拍下去,999.9999变成999。对便利店小票来说,这是灾难。
修复方案出人意料地轻量:加一个极小的epsilon(ε)。1e-9,也就是十亿分之一日元,在任何真实消费场景(0到1亿日元)里都是噪声。但它能把999.9999999999999顶到1000.0000000009,floor之后正确落地1000。
作者把这叫做"逆向epsilon"——通常我们用epsilon判断"两个浮点数是否足够接近",这里用它把浮点数推进正确的整数区间。
日本国税厅的"两种合法答案"
单商品计税只是热身。购物车里有多个商品时,日本国税厅允许两种算法并存,商家任选:
方法一:逐商品计税,再求和
每个商品先算税、四舍五入,最后加起来。适合小票需要展示每件商品税额的情况。
方法二:按税率分组,先求和再计税
10%税率的商品先加总,8%的另算一组,分别计税后合并。总金额可能和方法一差几日元。
作者的computeCart函数把两种都实现了。测试用例覆盖边界:混合税率购物车、双向换算、三种取整模式(floor/round/ceil)。
演示地址:https://sen.ltd/portfolio/shohizei-calc/ GitHub:https://github.com/sen-ltd/shohizei-calc
零依赖,原生JS,压缩后比一张表情包还小。
"正确"方案 vs 能跑的方案
浮点问题的"教科书答案"是任意精度算术,BigDecimal那一路。但这意味着为80行代码拖进一个依赖库。作者算过账:对于日本消费税的实际数值范围,epsilon修复足够安全,且省下的bundle size和认知负担是真实收益。
这不是妥协,是工程判断。就像你不会为家里换个灯泡调用起重机——IEEE 754的缺陷在特定区间是可预测的,而1e-9这个魔法数字恰好落在安全区。
代码里还埋了个细节:用户可以在10%和8%税率间切换,输入框实时响应。作者提到这个交互时用了个类比——"像老式收银机切换税率档位,但不用油"。
最后留个测试给你:你的计算器能正确处理1100÷1.1吗?如果答案是999,它可能正在某个日本便利店的POS机里等着被投诉。
热门跟贴