Allocate Money in Cents Without Rounding Drift
Split an integer cent total across weighted shares with the largest remainder method so every cent is assigned exactly once.
Code
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]));Report a problem with this exampleOpens a pre-filled issue on GitHub.