Functional Weave
Code in Rust

energy.water-bill

Metered water and sewerage bill from two reads: volumetric charges, return to sewer, standing and surface water charges.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 14 tests, run in TypeScript, Python and Rust.

What it does

A metered water and sewerage bill for England and Wales, from the reads at the start and end of the period and the water company's charges:

| line | how | |---|---| | water volume | m³ used x water rate per m³ | | sewerage volume | m³ used x return to sewer % x sewerage rate per m³ | | water standing charge | annual charge x days / 365 | | sewerage standing charge | annual charge x days / 365 | | surface water drainage | annual charge x days / 365, unless the property's surface water does not drain to the public sewer |

For example

  • water_bill(date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000…) → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £292.13, water standing charge £14.96, sewerage standing charge £19.95, surface water charge £27.42… six months, 123 m³: sewerage on 95% is exactly 29212.5p and rounds up
  • water_bill(date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000…) → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £292.13, water standing charge £14.96, sewerage standing charge £19.95, surface water charge £0.00,… surface water that does not reach the sewer is not charged
  • water_bill(date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000…) → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £292.12, water standing charge £14.95, sewerage standing charge £19.94, surface water charge £27.42… rounding down every line

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn water_bill(opening_read: &MeterRead, closing_read: &MeterRead, meter_digits: i64, tariff: &WaterTariff, surface_water_drains: bool, currency: &str, mode: &str) -> WaterBill
opening_readMeterReadthe read at the start of the period, whole cubic metres
closing_readMeterReadthe read at the end; lower means the meter rolled over
meter_digitsintwhole-m³ digits on the register, 1 to 15
tariffWaterTariffthe water company's charges for the period
surface_water_drainsboolfalse when rainwater from the property does not reach the public sewer: no surface water charge
currencystringthe currency of the tariff
modeRoundingModehow each charge rounds to a whole minor unit
returnsWaterBill

The types it declares, generated into your project

/// One meter read.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MeterRead {
    pub date: String,
    /// the register, whole cubic metres
    pub value: i64,
}

/// A metered water and sewerage tariff, as a water company's charges scheme lists it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WaterTariff {
    /// water supply per m³, in thousandths of a minor unit: £2.0736 is 207360
    pub water_volume_rate: i64,
    /// sewerage per m³ returned to the sewer, in thousandths of a minor unit
    pub sewerage_volume_rate: i64,
    /// water supply standing charge per year, in minor units
    pub water_standing_charge: i64,
    /// sewerage standing charge per year, in minor units
    pub sewerage_standing_charge: i64,
    /// surface water drainage charge per year, in minor units; 0 if not separately charged
    pub surface_water_charge: i64,
    /// share of water charged as returned to the sewer, basis points: 95% is 9500
    pub return_to_sewer: i64,
}

/// The bill, one charge per line.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WaterBill {
    /// water used
    pub cubic_metres: i64,
    /// days between the reads
    pub days: i64,
    pub water_volume_charge: Money,
    /// on cubicMetres x returnToSewer
    pub sewerage_volume_charge: Money,
    /// annual charge x days / 365
    pub water_standing_charge: Money,
    /// annual charge x days / 365
    pub sewerage_standing_charge: Money,
    /// annual charge x days / 365, or zero when surface water does not drain to the sewer
    pub surface_water_charge: Money,
    /// the sum of the lines
    pub total: Money,
}

Your code names it in one line, in the file that uses it

fune!(energy.water-bill@^1);  // then call water_bill(…)
impl/rust.rs · 133 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::energy_meter_advance::meter_advance;  ← from energy.meter-advance ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

fn check_rate(what: &str, value: i64) {
    if value < 0 {
        panic!("{} must be a whole number of 0 or more, received {}", what, value);
    }
}

fn exact(product: i128) -> i64 {
    if product > MAX_SAFE {
        panic!("water charge too large to calculate exactly");
    }
    product as i64
}

/// 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.
///
/// # Panics
/// Panics on reads that do not fit the register, reads in the wrong order, a
/// negative charge, a return-to-sewer share outside 0-100%, or a charge too
/// large to be exact.
pub fn water_bill(
    opening_read: &MeterRead,
    closing_read: &MeterRead,
    meter_digits: i64,
    tariff: &WaterTariff,
    surface_water_drains: bool,
    currency: &str,
    mode: &str,
) -> WaterBill {
    let cubic_metres = meter_advance(opening_read.value, closing_read.value, meter_digits);
    let days = days_between(&opening_read.date, &closing_read.date);
    if days < 0 {
        panic!(
            "the closing read must not be dated before the opening read, received {} and {}",
            opening_read.date, closing_read.date
        );
    }
    check_rate("waterVolumeRate", tariff.water_volume_rate);
    check_rate("sewerageVolumeRate", tariff.sewerage_volume_rate);
    check_rate("waterStandingCharge", tariff.water_standing_charge);
    check_rate("sewerageStandingCharge", tariff.sewerage_standing_charge);
    check_rate("surfaceWaterCharge", tariff.surface_water_charge);
    if tariff.return_to_sewer < 0 || tariff.return_to_sewer > 10000 {
        panic!("returnToSewer must be basis points from 0 to 10000, received {}", tariff.return_to_sewer);
    }
    let m3 = cubic_metres as i128;
    let d = days as i128;
    let water = round_div(exact(m3 * tariff.water_volume_rate as i128), 1000, mode);
    let sewerage = round_div(
        exact(m3 * tariff.sewerage_volume_rate as i128 * tariff.return_to_sewer as i128),
        10_000_000,
        mode,
    );
    let water_standing = round_div(exact(tariff.water_standing_charge as i128 * d), 365, mode);
    let sewerage_standing = round_div(exact(tariff.sewerage_standing_charge as i128 * d), 365, mode);
    let surface = if surface_water_drains {
        round_div(exact(tariff.surface_water_charge as i128 * d), 365, mode)
    } else {
        0
    };
    WaterBill {
        cubic_metres,
        days,
        water_volume_charge: money(water, currency),
        sewerage_volume_charge: money(sewerage, currency),
        water_standing_charge: money(water_standing, currency),
        sewerage_standing_charge: money(sewerage_standing, currency),
        surface_water_charge: money(surface, currency),
        total: money(water + sewerage + water_standing + sewerage_standing + surface, currency),
    }
}

fn whole(v: &Value, what: &str) -> i64 {
    if let Value::Float(f) = v {
        panic!("{} must be a whole number of 0 or more, received {}", what, f);
    }
    v.as_i64()
}

pub fn meter_read_from_value(v: &Value) -> MeterRead {
    if let Value::Float(f) = v.get("value") {
        panic!("reads must be whole numbers, received {}", f);
    }
    MeterRead {
        date: v.get("date").as_str().to_string(),
        value: v.get("value").as_i64(),
    }
}

pub fn water_tariff_from_value(v: &Value) -> WaterTariff {
    WaterTariff {
        water_volume_rate: whole(v.get("waterVolumeRate"), "waterVolumeRate"),
        sewerage_volume_rate: whole(v.get("sewerageVolumeRate"), "sewerageVolumeRate"),
        water_standing_charge: whole(v.get("waterStandingCharge"), "waterStandingCharge"),
        sewerage_standing_charge: whole(v.get("sewerageStandingCharge"), "sewerageStandingCharge"),
        surface_water_charge: whole(v.get("surfaceWaterCharge"), "surfaceWaterCharge"),
        return_to_sewer: whole(v.get("returnToSewer"), "returnToSewer"),
    }
}

pub fn water_bill_to_value(b: &WaterBill) -> Value {
    Value::obj(vec![
        ("cubicMetres", Value::Int(b.cubic_metres)),
        ("days", Value::Int(b.days)),
        ("waterVolumeCharge", money_to_value(&b.water_volume_charge)),
        ("sewerageVolumeCharge", money_to_value(&b.sewerage_volume_charge)),
        ("waterStandingCharge", money_to_value(&b.water_standing_charge)),
        ("sewerageStandingCharge", money_to_value(&b.sewerage_standing_charge)),
        ("surfaceWaterCharge", money_to_value(&b.surface_water_charge)),
        ("total", money_to_value(&b.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    water_bill_to_value(&water_bill(
        &meter_read_from_value(&args[0]),
        &meter_read_from_value(&args[1]),
        args[2].as_i64(),
        &water_tariff_from_value(&args[3]),
        args[4].as_bool(),
        args[5].as_str(),
        args[6].as_str(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add energy.water-bill
Download for Rust energy.water-bill-1.0.0-rust.fune · 22,584 bytes sha256 5150ff2e9accb2a50e7f785e702521712c90612a6d9e918229c37626be908cf1

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./energy.water-bill-1.0.0-rust.fune, or fetch it from a terminal with fune pull energy.water-bill@1.0.0:rust.

The whole function, every language, is one file too: energy.water-bill-1.0.0.fune, 28,608 bytes, sha256 79d9f71e0a111957c20dff84b0ca2a486863c3ca44ebdd8b2b69095d390ad594. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before energy.water-bill

after — your function gets the result and the arguments, and returns the final result.

// fune: after energy.water-bill

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.days-between in energy.water-bill
// fune: replace energy.meter-advance in energy.water-bill
// fune: replace math.round-div in energy.water-bill
// fune: replace money.amount in energy.water-bill

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show energy.water-bill --steps.

// fune: step energy.water-bill after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
six months, 123 m³: sewerage on 95% is exactly 29212.5p and rounds up date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £292.13, water standing charge £14.96, sewerage standing charge £19.95, surface water charge £27.42…
surface water that does not reach the sewer is not charged date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £292.13, water standing charge £14.96, sewerage standing charge £19.95, surface water charge £0.00,…
rounding down every line date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £292.12, water standing charge £14.95, sewerage standing charge £19.94, surface water charge £27.42…
a meter rolling over past 99999 date 2026-01-01, value 99,990, date 2026-01-31, value 10, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000, … → cubic metres 20, days 30, water volume charge £41.47, sewerage volume charge £47.50, water standing charge £2.47, sewerage standing charge £3.29, surface water charge £4.52, total…
a leap year of standing charge is 366/365 of the annual charge date 2024-01-01, value 500, date 2025-01-01, value 500, 5, water volume rate 0, sewerage volume rate 0, water standing charge 3,000, sewerage standing charge 0, surface water char… → cubic metres 0, days 366, water volume charge £0.00, sewerage volume charge £0.00, water standing charge £30.08, sewerage standing charge £0.00, surface water charge £0.00, total …
an ordinary year of standing charges is exactly the annual charges date 2025-04-01, value 500, date 2026-04-01, value 500, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000, su… → cubic metres 0, days 365, water volume charge £0.00, sewerage volume charge £0.00, water standing charge £30.00, sewerage standing charge £40.00, surface water charge £55.00, tota…
a business with no water returned to the sewer pays no sewerage volume date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → cubic metres 123, days 182, water volume charge £255.05, sewerage volume charge £0.00, water standing charge £14.96, sewerage standing charge £19.95, surface water charge £27.42, …
reads on the same day with no use cost nothing date 2026-04-01, value 42, date 2026-04-01, value 42, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000, surf… → cubic metres 0, days 0, water volume charge £0.00, sewerage volume charge £0.00, water standing charge £0.00, sewerage standing charge £0.00, surface water charge £0.00, total £0.…
a closing read dated before the opening read is refused date 2026-09-30, value 1,123, date 2026-04-01, value 1,000, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → error: the closing read must not be dated before the opening read
a return to sewer over 100% is refused date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → error: returnToSewer must be basis points from 0 to 10000
Show the other 4 tests
CaseArgumentsExpected
a negative rate is refused date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate -1, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000, sur… → error: waterVolumeRate must be a whole number of 0 or more
a fractional standing charge is refused date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000.5, sewerage standing charge 4,0… → error: waterStandingCharge must be a whole number of 0 or more
a read that does not fit the register is refused date 2026-04-01, value 1,000, date 2026-09-30, value 100,000, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,0… → error: reads must be whole numbers from 0 to 99999
a lowercase currency is refused date 2026-04-01, value 1,000, date 2026-09-30, value 1,123, 5, water volume rate 207,360, sewerage volume rate 250,000, water standing charge 3,000, sewerage standing charge 4,000… → error: is not an uppercase ISO 4217 currency code

More from the author

The rates are the caller's: every company publishes its own charges scheme each April, and they differ by company and by customer type.

## Decisions

- **Return to sewer.** Sewerage is charged on the water assumed to go back to the sewer, usually 95% of what is supplied (90% for some, a site-specific figure for many businesses). It is a tariff field in basis points, and the sewerage volume charge is computed exactly on `m³ x rate x share` with one rounding, not on a rounded volume. - **Surface water drainage allowance.** Where no rainwater from the property drains to the public sewer (it goes to a soakaway, say), the company must not charge for surface water drainage. Pass `surfaceWaterDrains: false` and that line is zero. Companies that fold surface water into the sewerage standing charge publish a reduced sewerage charge for such properties instead; pass that tariff and a zero `surfaceWaterCharge`. - **Days.** The period is the days between the two read dates (`dates.days-between`): reads on 1 April and 30 September are 182 days. Annual charges are apportioned by `days / 365` in every year, including a leap year, which is the usual daily-apportionment rule in charges schemes; a company that apportions by 366 in a leap year will differ by a penny or so. - **Rollover** is handled by `energy.meter-advance`: a lower closing read is a meter that went round once. - **Each line is rounded once** by `mode` and the total is the sum of the lines, as the bill prints them. - **No VAT.** Water and sewerage services to households are zero-rated, and most business supplies are too; if yours are not, apply VAT to the total.

## Sources

- Ofwat, "Surface water and highway drainage", https://www.ofwat.gov.uk/households/your-water-bill/surfacewaterdrainage/ ("If no surface water from your property enters a public sewer then you may qualify for a reduction in your sewerage charge"). - Ofwat, "Metered customers and applying for metered charges", https://www.ofwat.gov.uk/households/your-water-bill/metering/ - The rates, standing charges, return-to-sewer percentage and apportionment rule are each company's own, in its annual charges scheme.

Files

PathBytes
README.md2,758
impl/python.py2,948
impl/rust.rs5,152
impl/typescript.ts2,871
vectors.json8,328