education.fee-instalments@1.0.0
impl/typescript.ts
1,842 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { allocate } from "./money_allocate.ts"; ← from money.allocate ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type FeeInstalment } from "./education_fee_instalments_types.ts";
/**
* Split a fee into dated instalments that add up to it exactly.
*
* The split is money.allocate's: floors, then the leftover pennies to the
* largest remainders, ties to the earlier instalment.
*/
export function feeInstalments(total: Money, shares: readonly number[], dueDates: readonly string[]): readonly FeeInstalment[] {
if (!Number.isInteger(total.minor) || total.minor < 0) {
throw new RangeError(`total must not be negative, received ${total.minor}`);
}
if (shares.length === 0) {
throw new RangeError("shares must name at least one instalment");
}
if (shares.length !== dueDates.length) {
throw new RangeError(`shares and dueDates must be the same length, received ${shares.length} and ${dueDates.length}`);
}
let sum = 0;
for (const s of shares) {
if (!Number.isInteger(s) || s < 0) {
throw new RangeError(`each share must be a whole number of 0 or more, received ${s}`);
}
sum += s;
}
if (sum === 0) {
throw new RangeError("shares must not all be zero");
}
for (let i = 0; i < dueDates.length; i++) {
parseIsoDate(dueDates[i]);
if (i > 0 && dueDates[i] <= dueDates[i - 1]) {
throw new RangeError(`dueDates must be in strictly ascending order: ${dueDates[i]} follows ${dueDates[i - 1]}`);
}
}
const amounts = allocate(total, shares);
let paid = 0;
return amounts.map((amount, i) => {
paid += amount.minor;
return {
number: i + 1,
dueDate: dueDates[i],
amount,
cumulative: money(paid, total.currency),
remaining: money(total.minor - paid, total.currency),
};
});
}