← 中文资源

简体中文版本

无舍入漂移的金额分摊

用最大余数法按权重拆分整数分金额,确保每一分都被准确分配且总和不变。

阅读英文原文

功能说明

该示例把一笔以分为单位的整数金额按权重分摊到多个账户或条目,保证所有结果之和严格等于原始金额。直接对每份四舍五入会出现一分钱漂移,最大余数法先取整分配,再把剩余的分按余数从大到小逐个补齐,适合分账、优惠券分摊、税费拆分与结算对账。

关键代码

allocateCents 先校验总额为安全整数、权重数组非空且每个权重非负,权重和必须大于零且有限。取绝对值后按比例向下取整得到基础份额,计算剩余分数。随后为每个权重计算整数余数,按余数降序、下标升序排序,依次给余数最大的份额加一,直到剩余为零。最后按原始符号恢复正负。

使用注意

权重可以是小数,但金额计算全程使用整数分,不要用浮点元。负数总额按绝对值分摊后再取负,保证对称。余数相同时按原顺序优先,结果可复现。该函数只做数学分摊,不处理货币精度、汇率或最小结算单位。权重为零的条目会得到零,但至少需要一个正权重。

验证方式

断言一千分按三个等权拆分得到 334、333、333,总和为一千。一百分按三比七得到 30 与 70。五分按三等分得到 2、2、1,负一百分得到负的对称结果。测试全零权重、负数权重、空数组与非整数总额分别抛错。

代码示例

export function allocateCents(totalCents: number, weights: readonly number[]): number[] {
  if (!Number.isSafeInteger(totalCents)) throw new TypeError("totalCents must be a safe integer");
  if (!Array.isArray(weights) || weights.length === 0) {
    throw new TypeError("weights must be a non-empty array");
  }
  let weightSum = 0;
  for (const weight of weights) {
    if (!Number.isFinite(weight) || weight < 0) {
      throw new RangeError("weights must be non-negative numbers");
    }
    weightSum += weight;
  }
  if (weightSum === 0) throw new RangeError("at least one weight must be positive");
  if (!Number.isFinite(weightSum)) throw new RangeError("weight sum is too large");
  const sign = totalCents < 0 ? -1 : 1;
  const absolute = Math.abs(totalCents);
  const base = weights.map((weight) => Math.floor((absolute * weight) / weightSum));
  let remaining = absolute - base.reduce((sum, value) => sum + value, 0);
  const remainders = weights
    .map((weight, index) => ({ index, remainder: (absolute * weight) % weightSum }))
    .sort((left, right) => right.remainder - left.remainder || left.index - right.index);
  for (const entry of remainders) {
    if (remaining === 0) break;
    const current = base[entry.index];
    if (current === undefined) continue;
    base[entry.index] = current + 1;
    remaining -= 1;
  }
  return base.map((value) => value * sign);
}

console.log(allocateCents(1000, [1, 1, 1]));
console.log(allocateCents(100, [3, 7]));
console.log(allocateCents(5, [1, 1, 1]));
console.log(allocateCents(-100, [1, 1, 1]));