Functional Weave
Code in TypeScript

property.council-tax-band@1.0.0

impl/typescript.ts

2,467 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 { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^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 { BAND_RATIOS, BAND_RATIOS_HISTORY, type BandRatio } from "./property_council_tax_band_data.ts";  ← this capability’s own data, compiled from data/band-ratios.json into the same file by fune build
import { type CouncilTaxBandCharge } from "./property_council_tax_band_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
const NATIONS = ["england", "wales", "scotland"];
// Keeps Band D × the largest proportion exact as a JavaScript number.
const MAX_MINOR = 1_000_000_000_000;

/**
 * The council tax for a band: Band D × the band's statutory proportion ÷ Band
 * D's, rounded half-up to the penny once. The proportions are dated rows, so
 * a Scottish band E before April 2017 uses 440/360 and after it 473/360.
 */
export function councilTaxBand(bandDCharge: Money, band: string, nation: string, onDate: string): CouncilTaxBandCharge {
  if (!ISO_DATE.test(onDate)) {
    throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  }
  if (!NATIONS.includes(nation)) {
    throw new RangeError(`unknown nation "${nation}": use england, wales or scotland`);
  }
  if (bandDCharge.currency !== "GBP") {
    throw new RangeError(`bandDCharge must be in GBP, received ${bandDCharge.currency}`);
  }
  if (!Number.isInteger(bandDCharge.minor) || bandDCharge.minor < 0 || bandDCharge.minor > MAX_MINOR) {
    throw new RangeError(`bandDCharge must be between 0 and ${MAX_MINOR} minor units, received ${bandDCharge.minor}`);
  }
  let found: BandRatio | null = null;
  let earliest: string | null = null;
  for (const row of BAND_RATIOS) {
    if (row.nation !== nation) continue;
    if (earliest === null || row.validFrom < earliest) earliest = row.validFrom;
    if (row.band !== band || onDate < row.validFrom) continue;
    if (row.validTo !== null && onDate > row.validTo) continue;
    found = row;
  }
  if (found === null) {
    if (BAND_RATIOS_HISTORY !== "full" && earliest !== null && onDate < earliest) {
      throw new RangeError(
        `no council tax band ${band} for ${nation} on ${onDate}: this build was installed with history=${BAND_RATIOS_HISTORY}, so it only carries rules from ${earliest}`,
      );
    }
    throw new RangeError(`no council tax band ${band} for ${nation} on ${onDate}`);
  }
  return {
    band,
    ratioNumerator: found.numerator,
    ratioDenominator: found.denominator,
    charge: money(roundDiv(bandDCharge.minor * found.numerator, found.denominator, "half-up"), "GBP"),
  };
}