Functional Weave
Code in Rust

energy.climate-change-levy

Climate Change Levy on a business supply of electricity, gas, LPG or solid fuel, at the main rate in force on the date.

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

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

Not professional advice. This capability calculates tax figures from published rules. It is a software component for developers, not tax advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a tax adviser review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

The Climate Change Levy (CCL) a supplier adds to a business energy bill:

levy = quantity x main rate for the commodity on the date of supply

For example

  • climate_change_levy(electricity, 10,000,000, 2026-05-01, false, half-up) → commodity electricity, unit kWh, rate 801, excluded false, levy £80.10, valid from 2026-04-01, valid to 2027-03-31 10,000 kWh of business electricity in May 2026 at £0.00801 is £80.10
  • climate_change_levy(gas, 5,000,000, 2024-01-15, false, half-up) → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31 5,000 kWh of gas in January 2024, still at the 2023 gas rate of £0.00672
  • climate_change_levy(gas, 5,000,000, 2024-03-31, false, half-up) → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31 gas on 31 March 2024, the last day of the lower rate

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 climate_change_levy(commodity: &str, quantity: i64, on_date: &str, qualifying_use: bool, mode: &str) -> ClimateChangeLevy
commodityCclCommodityelectricity and gas are charged per kWh, lpg and other per kg
quantityintin thousandths of the unit: watt-hours, or grams
on_datedatethe date of supply the rate applies on
qualifying_usebooltrue for domestic use, charity non-business use or a de minimis supply: the levy does not apply
modeRoundingModehow the levy rounds to a whole penny
returnsClimateChangeLevy

The types it declares, generated into your project

// CclCommodity is a string in Rust, one of: "electricity", "gas", "lpg", "other".
// Parameters take it as &str and results hold it as String.

/// The levy, the rate it was charged at, and whether the supply was outside it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClimateChangeLevy {
    pub commodity: String,
    /// kWh or kg
    pub unit: String,
    /// main rate in thousandths of a penny per unit: £0.00775/kWh is 775
    pub rate: i64,
    /// true when qualifying use kept the supply out of the levy
    pub excluded: bool,
    /// GBP, zero when excluded
    pub levy: Money,
    /// when the rate came into force
    pub valid_from: String,
    /// when it stops; null while it is the latest rate published
    pub valid_to: Option<String>,
}

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

fune!(energy.climate-change-levy@^1);  // then call climate_change_levy(…)
impl/rust.rs · 130 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::energy_climate_change_levy_data::{CCL_RATES, CCL_RATES_HISTORY};  ← this capability’s own data, compiled from data/ccl-rates.json into the same file by fune build
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

// quantity (thousandths of a unit) x rate (thousandths of a penny) is in
// millionths of a penny.
const DIVISOR: i64 = 1_000_000;
const MAX_SAFE: i128 = 9_007_199_254_740_991;

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes.iter().enumerate().all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

/// The Climate Change Levy on one supply, at the main rate in force on
/// `on_date`. A qualifying-use supply (domestic, charity non-business or
/// de minimis) is outside the levy and comes back excluded, with a zero levy.
///
/// # Panics
/// Panics on a malformed date, a negative quantity, an unknown commodity, a
/// date no rate covers, or a levy too large to be exact in every language.
pub fn climate_change_levy(
    commodity: &str,
    quantity: i64,
    on_date: &str,
    qualifying_use: bool,
    mode: &str,
) -> ClimateChangeLevy {
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    if quantity < 0 {
        panic!(
            "quantity must be a whole number of thousandths of a unit, 0 or more, received {}",
            quantity
        );
    }
    if !CCL_RATES.iter().any(|r| r.commodity == commodity) {
        panic!("unknown commodity \"{}\": expected electricity, gas, lpg or other", commodity);
    }
    let mut best = None;
    for r in CCL_RATES {
        if r.commodity != commodity || on_date < r.valid_from {
            continue;
        }
        if let Some(to) = r.valid_to {
            if on_date > to {
                continue;
            }
        }
        if best.map_or(true, |b: &super::energy_climate_change_levy_data::CclRate| r.valid_from > b.valid_from) {
            best = Some(r);
        }
    }
    let best = match best {
        Some(r) => r,
        None => {
            // A pruned build must not answer an old date with a newer rate.
            if CCL_RATES_HISTORY != "full" {
                let mut earliest: Option<&'static str> = None;
                for r in CCL_RATES {
                    if r.commodity == commodity && earliest.map_or(true, |e| r.valid_from < e) {
                        earliest = Some(r.valid_from);
                    }
                }
                if let Some(earliest) = earliest {
                    if on_date < earliest {
                        panic!(
                            "no Climate Change Levy rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older supplies.",
                            commodity, on_date, CCL_RATES_HISTORY, earliest
                        );
                    }
                }
            }
            panic!("no Climate Change Levy rate for {} on {}", commodity, on_date)
        }
    };
    let mut pence = 0;
    if !qualifying_use {
        let product = quantity as i128 * best.rate as i128;
        if product > MAX_SAFE {
            panic!("levy too large to calculate exactly");
        }
        pence = round_div(product as i64, DIVISOR, mode);
    }
    ClimateChangeLevy {
        commodity: commodity.to_string(),
        unit: best.unit.to_string(),
        rate: best.rate,
        excluded: qualifying_use,
        levy: money(pence, "GBP"),
        valid_from: best.valid_from.to_string(),
        valid_to: best.valid_to.map(str::to_string),
    }
}

pub fn climate_change_levy_to_value(c: &ClimateChangeLevy) -> Value {
    Value::obj(vec![
        ("commodity", Value::str(&c.commodity)),
        ("unit", Value::str(&c.unit)),
        ("rate", Value::Int(c.rate)),
        ("excluded", Value::Bool(c.excluded)),
        ("levy", money_to_value(&c.levy)),
        ("validFrom", Value::str(&c.valid_from)),
        (
            "validTo",
            match &c.valid_to {
                Some(d) => Value::str(d),
                None => Value::Null,
            },
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[1] {
        panic!("quantity must be a whole number of thousandths of a unit, 0 or more, received {}", f);
    }
    climate_change_levy_to_value(&climate_change_levy(
        args[0].as_str(),
        args[1].as_i64(),
        args[2].as_str(),
        args[3].as_bool(),
        args[4].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 2 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.climate-change-levy
Download for Rust energy.climate-change-levy-1.0.2-rust.fune · 22,753 bytes sha256 6cc07eb193b5a7a587608dcde04ed6d9c63d5d9e77a669798071f236b4b1d1de

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

The whole function, every language, is one file too: energy.climate-change-levy-1.0.2.fune, 28,839 bytes, sha256 18139232039628d0676a0d19756af8beb5c03b60f1811a68eccf403a14f406fc. 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.climate-change-levy

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

// fune: after energy.climate-change-levy

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 math.round-div in energy.climate-change-levy
// fune: replace money.amount in energy.climate-change-levy

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.climate-change-levy --steps.

// fune: step energy.climate-change-levy 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
10,000 kWh of business electricity in May 2026 at £0.00801 is £80.10 electricity, 10,000,000, 2026-05-01, false, half-up → commodity electricity, unit kWh, rate 801, excluded false, levy £80.10, valid from 2026-04-01, valid to 2027-03-31
5,000 kWh of gas in January 2024, still at the 2023 gas rate of £0.00672 gas, 5,000,000, 2024-01-15, false, half-up → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31
gas on 31 March 2024, the last day of the lower rate gas, 5,000,000, 2024-03-31, false, half-up → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31
gas on 1 April 2024 is charged at the equalised £0.00775 gas, 5,000,000, 2024-04-01, false, half-up → commodity gas, unit kWh, rate 775, excluded false, levy £38.75, valid from 2024-04-01, valid to 2025-03-31
a tonne of LPG at £0.02175 per kg lpg, 1,000,000, 2025-06-30, false, half-up → commodity lpg, unit kg, rate 2,175, excluded false, levy £21.75, valid from 2025-04-01, valid to 2026-03-31
a tonne of coal as any other taxable commodity in 2026 other, 1,000,000, 2026-04-01, false, half-up → commodity other, unit kg, rate 6,264, excluded false, levy £62.64, valid from 2026-04-01, valid to 2027-03-31
the rate already announced for April 2027 is the latest, open-ended electricity, 1,000,000, 2027-04-01, false, half-up → commodity electricity, unit kWh, rate 827, excluded false, levy £8.27, valid from 2027-04-01, valid to —
a domestic supply is outside the levy but still reports the rate electricity, 10,000,000, 2026-05-01, true, half-up → commodity electricity, unit kWh, rate 801, excluded true, levy £0.00, valid from 2026-04-01, valid to 2027-03-31
one kWh is 0.775p, rounded half-up to 1p electricity, 1,000, 2025-06-01, false, half-up → commodity electricity, unit kWh, rate 775, excluded false, levy £0.01, valid from 2025-04-01, valid to 2026-03-31
one kWh is 0.775p, rounded down to nothing electricity, 1,000, 2025-06-01, false, down → commodity electricity, unit kWh, rate 775, excluded false, levy £0.00, valid from 2025-04-01, valid to 2026-03-31
Show the other 10 tests
CaseArgumentsExpected
a fraction of a kWh in watt-hours: 1,234.567 kWh of gas at 0.775p gas, 1,234,567, 2025-06-01, false, half-up → commodity gas, unit kWh, rate 775, excluded false, levy £9.57, valid from 2025-04-01, valid to 2026-03-31
60 kg of LPG is exactly 130.5p: half-even keeps the even 130p lpg, 60,000, 2025-06-01, false, half-even → commodity lpg, unit kg, rate 2,175, excluded false, levy £1.30, valid from 2025-04-01, valid to 2026-03-31
no energy is no levy gas, 0, 2026-06-01, false, half-up → commodity gas, unit kWh, rate 801, excluded false, levy £0.00, valid from 2026-04-01, valid to 2027-03-31
a date before the earliest rate carried is an error electricity, 1,000, 2023-03-31, false, half-up → error: no Climate Change Levy rate for electricity on 2023-03-31
an unknown commodity is refused diesel, 1,000, 2025-06-01, false, half-up → error: unknown commodity "diesel"
a negative quantity is refused gas, -1, 2025-06-01, false, half-up → error: quantity must be a whole number of thousandths of a unit
a malformed date is refused gas, 1,000, 01/06/2025, false, half-up → error: onDate must be an ISO date
an unknown rounding mode is refused gas, 1,000, 2025-06-01, false, nearest → error: unknown rounding mode "nearest"
a onDate with a trailing newline is refused gas, 1,000, 2026-09-16 , false, half-up → error: onDate must be an ISO date (YYYY-MM-DD), received "2026-09-16 "
a onDate in Arabic-Indic digits is refused gas, 1,000, ٢٠٢٦-٠٩-١٦, false, half-up → error: onDate must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-١٦"

More from the author

Electricity and gas are charged per kWh; LPG and "any other taxable commodity" (coal, coke, petroleum coke) per kg. The rates are dated data from HMRC, from 1 April 2023 to the rates already announced for 1 April 2027.

## Qualifying use: the domestic and de minimis exclusion

CCL does not apply to supplies for **domestic use** or **charity non-business use**, and supplies small enough to be **de minimis** are excluded too. Those are the same supplies that get VAT at the reduced 5% rate as "qualifying use" (VAT Notice 701/19): for a business, up to an average of 33 kWh a day (1,000 kWh a month) of electricity, or 145 kWh a day (4,397 kWh a month) of piped gas. Pass `qualifyingUse: true` for any of these and the result has `excluded: true` and a zero levy; the rate is still returned, so a bill can show which rate it did not charge. Deciding whether a supply qualifies (including the 60% mixed-use rule) is the caller's, because it depends on the customer, not the kWh alone.

## Units and rounding

- Quantities are integers in **thousandths** of the unit (watt-hours or grams), so part-units are exact. - Rates are integers in **thousandths of a penny** per unit, exactly the five decimal places of pounds HMRC publishes (£0.00775 per kWh is `775`). - The levy is rounded once to a whole penny by `mode` (suppliers usually round half-up per bill). Quantity times rate must stay within 2^53.

## Not covered

- **Reduced rates for climate change agreement holders** (a percentage discount off the main rate: 92% for electricity, 89% for gas from April 2024) — apply the discount to the result if the site holds a CCA. - **Carbon Price Support rates** paid by generators. - Supplies spanning a rate change on 1 April: split the quantity by date and call once per part.

## Dates and pruning

Each rate applies from `validFrom` to `validTo` inclusive; the latest has a null `validTo`. A build installed with `history=current` keeps only rates still in force, and a date before the earliest surviving rate is refused with an explanation rather than charged at a newer rate.

## Sources

- HMRC, "Climate Change Levy rates", https://www.gov.uk/guidance/climate-change-levy-rates (main rates from 1 April 2023, 2024, 2025, 2026 and 2027; last updated 27 November 2025). - HMRC, "VAT on fuel and power (VAT Notice 701/19)", https://www.gov.uk/guidance/vat-on-fuel-and-power-notice-70119 (paragraph 2.8: the levy does not apply to domestic or charity non-business use, and de minimis supplies are excluded; paragraphs 4.2 and 5.2: the de minimis limits).

1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

1.0.2 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE189
README.md3,117
data/ccl-rates.json4,291
impl/python.py3,043
impl/rust.rs4,614
impl/typescript.ts2,824
vectors.json5,231