Functional Weave
Code in TypeScript

energy.water-bill@1.0.0

impl/typescript.ts

2,871 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 { meterAdvance } from "./energy_meter_advance.ts";  ← from energy.meter-advance ^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 MeterRead, type WaterBill, type WaterTariff } from "./energy_water_bill_types.ts";

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

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

/**
 * A metered water and sewerage bill: volumetric water, sewerage on the share
 * returned to the sewer, and standing and surface water charges apportioned
 * by days / 365, each line rounded once.
 */
export function waterBill(
  openingRead: MeterRead,
  closingRead: MeterRead,
  meterDigits: number,
  tariff: WaterTariff,
  surfaceWaterDrains: boolean,
  currency: string,
  mode: RoundingMode
): WaterBill {
  const cubicMetres = meterAdvance(openingRead.value, closingRead.value, meterDigits);
  const days = daysBetween(openingRead.date, closingRead.date);
  if (days < 0) {
    throw new RangeError(`the closing read must not be dated before the opening read, received ${openingRead.date} and ${closingRead.date}`);
  }
  checkRate("waterVolumeRate", tariff.waterVolumeRate);
  checkRate("sewerageVolumeRate", tariff.sewerageVolumeRate);
  checkRate("waterStandingCharge", tariff.waterStandingCharge);
  checkRate("sewerageStandingCharge", tariff.sewerageStandingCharge);
  checkRate("surfaceWaterCharge", tariff.surfaceWaterCharge);
  if (!Number.isInteger(tariff.returnToSewer) || tariff.returnToSewer < 0 || tariff.returnToSewer > 10000) {
    throw new RangeError(`returnToSewer must be basis points from 0 to 10000, received ${tariff.returnToSewer}`);
  }
  const water = roundDiv(exact(cubicMetres * tariff.waterVolumeRate), 1000, mode);
  const sewerage = roundDiv(exact(cubicMetres * tariff.sewerageVolumeRate * tariff.returnToSewer), 10_000_000, mode);
  const waterStanding = roundDiv(exact(tariff.waterStandingCharge * days), 365, mode);
  const sewerageStanding = roundDiv(exact(tariff.sewerageStandingCharge * days), 365, mode);
  const surface = surfaceWaterDrains ? roundDiv(exact(tariff.surfaceWaterCharge * days), 365, mode) : 0;
  return {
    cubicMetres,
    days,
    waterVolumeCharge: money(water, currency),
    sewerageVolumeCharge: money(sewerage, currency),
    waterStandingCharge: money(waterStanding, currency),
    sewerageStandingCharge: money(sewerageStanding, currency),
    surfaceWaterCharge: money(surface, currency),
    total: money(water + sewerage + waterStanding + sewerageStanding + surface, currency),
  };
}