Functional Weave
Code in Rust

property.service-charge-apportion@1.0.0

impl/typescript.ts

2,647 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 { lcm } from "./math_gcd_lcm.ts";  ← from math.gcd-lcm ^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 } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type ApportionBasis, type ServiceChargeLine, type ServiceChargeUnit } from "./property_service_charge_apportion_types.ts";

/**
 * Share a service charge between units by lease fraction, floor area or fixed
 * percentage. Every share becomes an integer weight, and money.allocate does
 * the one split, so the lines always add up to the total exactly.
 */
export function apportionServiceCharge(
  total: Money,
  units: readonly ServiceChargeUnit[],
  basis: ApportionBasis,
): readonly ServiceChargeLine[] {
  if (basis !== "fraction" && basis !== "floor-area" && basis !== "percentage") {
    throw new RangeError(`unknown basis "${basis}": use fraction, floor-area or percentage`);
  }
  if (units.length === 0) {
    throw new RangeError("units must not be empty");
  }
  for (const u of units) {
    if (!Number.isSafeInteger(u.share) || u.share < 0) {
      throw new RangeError(`unit "${u.unit}": share must be a whole number, zero or more, received ${u.share}`);
    }
    if (basis === "fraction") {
      if (u.of === null || !Number.isSafeInteger(u.of) || u.of <= 0) {
        throw new RangeError(`unit "${u.unit}": a fraction needs a denominator "of" greater than zero`);
      }
    } else if (u.of !== null) {
      throw new RangeError(`unit "${u.unit}": "of" is only for the fraction basis`);
    }
  }
  let weights: number[];
  if (basis === "fraction") {
    let common = 1;
    for (const u of units) common = lcm(common, u.of as number);
    weights = units.map((u) => u.share * (common / (u.of as number)));
    const sum = weights.reduce((a, b) => a + b, 0);
    if (!Number.isSafeInteger(sum)) {
      throw new RangeError("the fractions are too fine to apportion exactly");
    }
    if (sum !== common) {
      throw new RangeError(`the fractions add up to ${sum}/${common}, not exactly 1`);
    }
  } else {
    weights = units.map((u) => u.share);
    const sum = weights.reduce((a, b) => a + b, 0);
    if (basis === "percentage" && sum !== 10000) {
      throw new RangeError(`the percentages add up to ${sum} basis points, not 10000`);
    }
    if (sum === 0) {
      throw new RangeError("the floor areas add up to zero");
    }
  }
  const sum = weights.reduce((a, b) => a + b, 0);
  if (!Number.isSafeInteger(sum) || !Number.isSafeInteger(Math.abs(total.minor) * sum)) {
    throw new RangeError("the fractions are too fine to apportion exactly");
  }
  const parts = allocate(total, weights);
  return units.map((u, i) => ({ unit: u.unit, charge: parts[i] }));
}