Functional Weave
Code in Rust

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),
    };
  });
}