功能说明
这段代码把一笔金额按权重拆成多份,保证每一份都是精确到分的 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()