视频加载失败

一个 HTML 文件的分账工具:bill-splitter 纯函数设计赏析

698 字
3 分钟
一个 HTML 文件的分账工具:bill-splitter 纯函数设计赏析
一个 HTML 文件的分账工具:bill-splitter 纯函数设计赏析

bill-splitter 源码赏析:一个 HTML 文件的工程哲学#

bill-splitter 是一个合租账单分摊工具,整个应用只有一个 HTML 文件。但别被体积骗了——它的核心算法设计得非常严谨。

核心:__split 纯函数#

整个应用的计算逻辑收敛在一个函数里:

__split(totalCents, weights, ...rest) → number[]

作者给这个函数立了四条不变量(无条件成立):

编号约束含义
I1长度返回数组长度 === 权重数组长度
I2整数每个元素都是整数(金额用分表示)
I3守恒所有元素之和 === totalCents
I4纯同样输入永远得到同样输出

这四条不变量就是一份可执行的规格文档。不需要读实现,光看这四条就知道函数的行为边界。

最大余数法 + 零头集中#

分摊算法用的是最大余数法:

// 1. 权重预处理:NaN 和负数 → 0
// 2. 退化处理:权重总和为 0 → 全部置 1
// 3. 按比例取整:floor(总额 × 权重/总权重)
// 4. 零头分配:剩余零头全部给余数最大的人

举个例子,__split(101, [1, 1, 1]):

// floor(101/3) = 33, 33, 33 → 剩余 2 分
// 余数分别是 0.67, 0.67, 0.67(相等)
// 按下标从小到大,第一个承担全部零头
// 结果:[35, 33, 33]

为什么零头要集中给一个人? 作者的理由很实用——房东一眼能看出每月零头算在谁头上。这是工程思维:算法上的「公平」(轮流承担)在这里不如「可解释性」重要。

防御性边界处理#

函数对异常输入的处理也很教科书:

__split(100, [0, 0, 0]) // 权重全0 → 均分 [34, 33, 33]
__split(50, [NaN, -1, 30]) // NaN/负数 → 0 → [0, 0, 50]
__split(-100, [1, 2, 3]) // 负金额 → 守恒=-100
__split(0, [5, 5, 5]) // 零金额 → [0, 0, 0]

每个边界 case 都有明确的行为定义,不抛异常、不返回 NaN。

架构:数据驱动 + 全量重渲染#

页面架构极其简单:

配置数据(PERSONS / BILLS / DAYS)
↓ input 事件
render() 全量重渲染
↓ 每笔账单调 __split()

没有框架、没有虚拟 DOM、没有状态管理。因为数据量小(3 人 × 4 笔账单),全量重渲染的性能完全够用。在正确的场景用正确的复杂度——这也是工程能力。

学到的三件事#

  1. 不变量比实现重要。 先定义行为约束,再写代码,测试和 review 都有据可依。
  2. 金额必须用整数(分)计算。 浮点数在金额场景是万恶之源。
  3. 零头集中不是 bug 是 feature。 工程决策要考虑业务可解释性,不只是数学正确性。

项目地址:github.com/Melusine-ichnose/bill-splitter

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

一个 HTML 文件的分账工具:bill-splitter 纯函数设计赏析
https://blog-tyb.pages.dev/posts/bill-splitter-analysis/
作者
Melusine
发布于
2026-09-24
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Melusine
Java 后端 & AI Agent 应用开发者,记录项目实战与源码学习。
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
6
分类
2
标签
16
总字数
10,460
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录