← 中文资源

简体中文版本

用 Decimal 精确拆分金额

按整数权重拆分金额并保持分位总额精确,余数按最大余数法确定性分配。

阅读英文原文

功能说明

这段代码把一笔金额按权重拆成多份,保证每一份都是精确到分的 Decimal,并且各份之和严格等于原始金额。典型场景是订单分摊、按比例分账、优惠券金额拆分或发票行项目分摊。

浮点数无法精确表示 0.01,直接用 float 做除法再四舍五入,经常出现总额差一分的问题。这里先把金额换算成整数分,再用整数运算完成分配,从根源上消除累计误差。

关键代码

split_amount 先用 quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) 把输入规范到分,并拒绝负数、空权重、负权重和权重和为零。随后把金额转成整数 cents,对每个权重计算 cents * weight,整除权重和得到基础份额。

剩余的分采用“最大余数法”:按 raw % weight_sum 从大到小排序,余数相同时按原始下标排序,给前 remainder 份各加一分。这样结果确定、可复现,并且不会出现某一份被多加多次。

使用注意

代码只支持两位小数,日元等零位小数货币或三位小数货币需要调整 quantize 的单位。输入应使用字符串或 Decimal,不要从 float 构造,否则误差在进入函数前就已经产生。

余数数量一定小于权重个数,因此 order[:remainder] 不会越界。权重为 0 的份会得到 0,这是预期行为;如果要“至少一分”,需要额外的业务规则。输出仍是 Decimal,写数据库时应按定点数或整数分存储,避免再次转成浮点。

验证方式

100.00 按 [1, 1, 1] 拆分得到 33.34、33.33、33.33,总和仍为 100.00。0.05 按 [1, 1, 2] 拆分时,前两份各 0.01,第三份 0.03,断言列表完全相等。权重全为零会抛出 ValueError,可通过捕获异常确认校验生效。

代码示例

from __future__ import annotations

from decimal import ROUND_HALF_UP, Decimal


def split_amount(total: str, weights: list[int]) -> list[Decimal]:
    amount = Decimal(total).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    if amount < 0:
        raise ValueError("amount must be non-negative")
    if not weights or any(weight < 0 for weight in weights):
        raise ValueError("weights must be non-negative and non-empty")
    weight_sum = sum(weights)
    if weight_sum == 0:
        raise ValueError("weights must not sum to zero")

    cents = int(amount * 100)
    raw = [cents * weight for weight in weights]
    shares = [value // weight_sum for value in raw]
    remainder = cents - sum(shares)

    order = sorted(
        range(len(weights)),
        key=lambda index: (-(raw[index] % weight_sum), index),
    )
    for index in order[:remainder]:
        shares[index] += 1

    return [Decimal(share) / 100 for share in shares]


def main() -> None:
    thirds = split_amount("100.00", [1, 1, 1])
    print([str(part) for part in thirds], sum(thirds))
    assert sum(thirds) == Decimal("100.00")

    uneven = split_amount("0.05", [1, 1, 2])
    print([str(part) for part in uneven])
    assert [str(part) for part in uneven] == ["0.01", "0.01", "0.03"]

    try:
        split_amount("10.00", [0, 0])
    except ValueError as exc:
        print(f"rejected: {exc}")


if __name__ == "__main__":
    main()