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