Functional Weave
Code in Rust

energy.bill-calculate@1.0.0

impl/typescript.ts

3,053 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 { daysBetween } from "./dates_days_between.ts";  ← from dates.days-between ^1.0.0 · built alongside by fune
import { climateChangeLevy } from "./energy_climate_change_levy.ts";  ← from energy.climate-change-levy ^1.0.0 · built alongside by fune
import { gasKwh } from "./energy_gas_kwh.ts";  ← from energy.gas-kwh ^1.0.0 · built alongside by fune
import { meterAdvance } from "./energy_meter_advance.ts";  ← from energy.meter-advance ^1.0.0 · built alongside by fune
import { addVat } from "./finance_tax_add_vat.ts";  ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
import { type RoundingMode, roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type EnergyBill, type EnergyBillInput } from "./energy_bill_calculate_types.ts";

// watt-hours x thousandths of a penny per kWh is millionths of a penny.
const DIVISOR = 1_000_000;

function checkRate(what: string, value: number): void {
  if (!Number.isInteger(value) || value < 0) {
    throw new RangeError(`${what} must be a whole number of thousandths of a penny, 0 or more, received ${value}`);
  }
}

function exact(product: number): number {
  if (product > Number.MAX_SAFE_INTEGER) {
    throw new RangeError("energy charge too large to calculate exactly");
  }
  return product;
}

/**
 * A GB electricity or gas bill from two reads: energy at the unit rate,
 * standing charge by the day, Climate Change Levy unless the supply is for
 * qualifying use, and VAT on the lot at the caller's category.
 */
export function calculateEnergyBill(input: EnergyBillInput, mode: RoundingMode): EnergyBill {
  if (input.fuel !== "electricity" && input.fuel !== "gas") {
    throw new RangeError(`unknown fuel "${input.fuel}": expected electricity or gas`);
  }
  checkRate("unitRate", input.unitRate);
  checkRate("standingCharge", input.standingCharge);
  const units = meterAdvance(input.openingRead.value, input.closingRead.value, input.meterDigits);
  const days = daysBetween(input.openingRead.date, input.closingRead.date);
  if (days < 0) {
    throw new RangeError(
      `the closing read must not be dated before the opening read, received ${input.openingRead.date} and ${input.closingRead.date}`
    );
  }
  let wattHours: number;
  if (input.fuel === "gas") {
    if (input.gasUnit === null || input.calorificValue === null) {
      throw new RangeError("a gas bill needs gasUnit and calorificValue");
    }
    wattHours = gasKwh(units, input.gasUnit, input.calorificValue).wattHours;
  } else {
    if (input.gasUnit !== null || input.calorificValue !== null) {
      throw new RangeError("gasUnit and calorificValue are for gas bills only");
    }
    wattHours = units * 1000;
  }
  const energy = roundDiv(exact(wattHours * input.unitRate), DIVISOR, mode);
  const standing = roundDiv(exact(days * input.standingCharge), 1000, mode);
  const ccl = climateChangeLevy(input.fuel, wattHours, input.closingRead.date, input.qualifyingUse, mode).levy.minor;
  const vat = addVat(money(energy + standing + ccl, "GBP"), "GB", input.vatCategory, input.closingRead.date);
  return {
    units,
    wattHours,
    days,
    energyCharge: money(energy, "GBP"),
    standingCharge: money(standing, "GBP"),
    climateChangeLevy: money(ccl, "GBP"),
    net: vat.net,
    vat: vat.tax,
    vatBasisPoints: vat.basisPoints,
    total: vat.gross,
  };
}