apportionium
v1.0.1
Published
Distribute integers proportionally with optional per-bucket caps.
Maintainers
Readme
apportionium
Split whole numbers proportionally without losing units to rounding.
Apportionium is a small, dependency-free TypeScript library for distributing a non-negative integer across weighted buckets - a problem known as apportionment. It uses Hamilton's largest-remainder method, so every result is made up of integers and the allocated values add up to the original amount.
Use the uncapped function for ordinary proportional allocation, or set a capacity for every bucket and let excess flow into the remaining buckets.
import { distributeInteger, distributeIntegerCapped } from "apportionium";
distributeInteger(
10, // <-- distribute 10
[1, 2, 3], // <-- with these factors/ratios between buckets
);
// [2, 3, 5]
distributeIntegerCapped(
20, // <-- distribute 20
[1, 2, 4], // <-- with these factors
[2, 100, 100], // <-- and these caps
);
// {
// undistributedAmount: 0,
// distribution: [2, 6, 12]
// }At a glance
- Produces deterministic integer allocations from relative weights
- Compensates integer rounding errors in a fair manner, minimizing relative error
- Supports constrained distribution
Installation
Install from npm:
npm install apportioniumFor Deno, install from JSR:
deno add jsr:@elemental/apportioniumConcepts
Largest-remainder rounding
Apportionium first calculates each bucket's ideal fractional share and rounds it down. It then awards the units left over to buckets with the largest fractional remainders. Equal remainders are resolved from left to right, making results deterministic.
For 10 units with weights [1, 2, 3], the ideal shares are approximately
[1.67, 3.33, 5]. Flooring gives [1, 3, 5]; the remaining unit goes to the
first bucket because it has the largest remainder, producing [2, 3, 5].
Caps and overflow
In a capped distribution, a bucket stops participating once it reaches its cap.
The amount that would overflow is apportioned across the still-active buckets
using their original relative weights. A bucket with a zero weight or zero cap
receives nothing. When no eligible capacity remains, the unallocated portion is
returned as undistributedAmount.
API
distributeInteger(amount, factors)
function distributeInteger(amount: number, factors: number[]): number[];Distributes amount according to factors and returns one non-negative integer
per factor. The returned values always sum to amount.
amountmust be a non-negative safe integer.factorsmust contain at least one value.- Every factor must be finite and non-negative, and at least one must be positive.
The function throws an Error when these requirements are not met.
distributeIntegerCapped(amount, factors, caps)
function distributeIntegerCapped(
amount: number,
factors: number[],
caps: number[],
): {
distribution: number[];
undistributedAmount: number;
};Distributes amount according to factors without assigning more than the
corresponding value in caps. The returned distribution has one entry per
factor. Together, its entries and undistributedAmount add up to amount.
amountmust be a non-negative safe integer.factorsandcapsmust be non-empty and have the same length.- Every factor must be finite and non-negative.
- Every cap must be a non-negative safe integer or
Infinity. An infinite cap represents an uncapped bucket.
The function throws an Error when these requirements are not met. Unlike the
uncapped function, a capped distribution with no active buckets is valid; its
complete amount is returned as undistributedAmount.
License
MIT
