Functional Weave
Code in Rust

fleet.company-car-bik Unreviewed

UK company car benefit in kind: appropriate percentage by CO2 and electric range, car and fuel benefit for a tax year.

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

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

Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll or benefits-in-kind specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

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 payroll or benefits-in-kind specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Status: needs review by a tax professional before it is published.

The taxable benefit in kind of a company car for one UK tax year, and of free fuel for it: what goes on a P11D or is payrolled. It follows the method in ITEPA 2003 s121:

For example

  • company_car_bik(£30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, false) → tax year 2025/26, days in year 365, days available 365, price £30,000.00, appropriate basis points 30%, car benefit £9,000.00, fuel benefit £0.00, total £9,000.00 petrol car at 120 g/km in 2025-26 is 30% of 30,000.00
  • company_car_bik(£30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, true) → tax year 2025/26, days in year 365, days available 365, price £30,000.00, appropriate basis points 30%, car benefit £9,000.00, fuel benefit £8,460.00, total £17,460.00 free fuel adds 30% of the 28,200 multiplier
  • company_car_bik(£40,000.00, £0.00, 0, 250, false, 2024-01-10, 2025-09-01, 0, £0.00, true) → tax year 2025/26, days in year 365, days available 365, price £40,000.00, appropriate basis points 3%, car benefit £1,200.00, fuel benefit £0.00, total £1,200.00 electric car at 3% in 2025-26, and electricity is not fuel so no fuel benefit

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 company_car_bik(list_price: &Money, capital_contributions: &Money, co2: i64, electric_range_miles: i64, diesel_supplement: bool, first_registered: &str, on_date: &str, unavailable_days: i64, private_use_contributions: &Money, free_fuel: bool) -> CompanyCarBik
list_priceMoneylist price of the car plus taxable accessories, in GBP
capital_contributionsMoneywhat the employee paid towards the car or accessories; only the first 5,000.00 counts
co2intthe car's CO2 emissions figure in g/km, whole number (0 for zero emission)
electric_range_milesintelectric range in miles, rounded up to a whole number; 0 when the car has none
diesel_supplementbooltrue for a car propelled solely by diesel that does not meet Euro 6d (RDE2)
first_registereddatedate of first registration; before 6 April 2020 the NEDC table applies in 2021-22
on_datedateany date in the tax year to calculate
unavailable_daysintdays in the tax year the car was not available (30 or more consecutive days only)
private_use_contributionsMoneywhat the employee was required to pay, and paid, for private use in the year
free_fuelbooltrue when the employer provides fuel for private use
returnsCompanyCarBik

The type it declares, generated into your project

/// The benefit for one car and one tax year, with the figures it was worked from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompanyCarBik {
    /// HMRC's label, e.g. 2025/26
    pub tax_year: String,
    /// 365 or 366
    pub days_in_year: i64,
    /// days in the year less unavailableDays
    pub days_available: i64,
    /// list price less the capital contributions that count
    pub price: Money,
    /// the appropriate percentage, 2500 = 25%, after any diesel supplement
    pub appropriate_basis_points: i64,
    /// cash equivalent of the car, after unavailability and private use payments
    pub car_benefit: Money,
    /// cash equivalent of free fuel, zero without it
    pub fuel_benefit: Money,
    /// car benefit plus fuel benefit
    pub total: Money,
}

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

fune!(fleet.company-car-bik@^1);  // then call company_car_bik(…)
impl/rust.rs · 261 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::dates_uk_tax_year::uk_tax_year;  ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
use super::fleet_company_car_bik_data::{
    BIK_BANDS, BIK_BANDS_HISTORY, BIK_LIMITS, BIK_LIMITS_HISTORY, FUEL_MULTIPLIERS, FUEL_MULTIPLIERS_HISTORY,
};
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9007199254740991;
// Cars first registered before this date keep their NEDC figures, and in
// 2021-22 their own (one point higher) column of the table.
const WLTP_FROM: &str = "2020-04-06";

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())
}

fn in_force(valid_from: &str, valid_to: Option<&str>, on_date: &str) -> bool {
    valid_from <= on_date && valid_to.map_or(true, |to| on_date <= to)
}

// A history=current build has dropped old rows; say so rather than answering
// an old tax year with today's figures.
fn missing(what: &str, label: &str, earliest: Option<&str>, history: &str, on_date: &str) -> ! {
    if history != "full" {
        if let Some(earliest) = earliest {
            if on_date < earliest {
                panic!(
                    "no {} for tax year {}: this build was installed with history={}, so it only carries figures from {}. Reinstall with history=full for earlier years.",
                    what, label, history, earliest
                );
            }
        }
    }
    panic!("no {} for tax year {}", what, label)
}

fn check_gbp(name: &str, amount: &Money) {
    if amount.currency != "GBP" {
        panic!("company car benefit is worked in GBP; {} is in {}", name, amount.currency);
    }
    if amount.minor < 0 {
        panic!("{} must not be negative, received {}", name, amount.minor);
    }
}

fn check_whole(name: &str, value: i64, unit: &str) {
    if value < 0 {
        panic!("{} must be a whole number of {}, not negative, received {}", name, unit, value);
    }
}

fn check_date(name: &str, value: &str) {
    if !is_iso_date(value) {
        panic!("{} must be an ISO date (YYYY-MM-DD), received \"{}\"", name, value);
    }
}

// price x basis points x days available / (10000 x days in year), rounded down
// to the penny: one rounding, and never more benefit than the law charges.
fn proportion(minor: i64, basis_points: i64, days_available: i64, days_in_year: i64) -> i64 {
    let product = minor as i128 * basis_points as i128 * days_available as i128;
    // Rust would carry on in i128, but TypeScript cannot, and all three must agree.
    if product > MAX_SAFE {
        panic!("the benefit calculation exceeds 2^53 - 1; the list price is too large");
    }
    round_div(product as i64, 10000 * days_in_year, "down")
}

/// The taxable benefit of a company car for one UK tax year (ITEPA 2003 s121).
///
/// Price less capital contributions (at most 5,000.00), times the appropriate
/// percentage for the car's CO2 figure and electric range in that year, cut
/// pro rata for days the car was unavailable, less what the employee paid for
/// private use. Free fuel adds the year's fuel benefit multiplier times the
/// same percentage, cut for the same days.
///
/// # Panics
/// Panics on a currency other than GBP, a negative amount or count, a malformed
/// date, a diesel supplement on a zero-emission car, a registration after the
/// tax year, too many unavailable days, or a tax year the tables do not cover.
#[allow(clippy::too_many_arguments)]
pub fn company_car_bik(
    list_price: &Money,
    capital_contributions: &Money,
    co2: i64,
    electric_range_miles: i64,
    diesel_supplement: bool,
    first_registered: &str,
    on_date: &str,
    unavailable_days: i64,
    private_use_contributions: &Money,
    free_fuel: bool,
) -> CompanyCarBik {
    check_gbp("listPrice", list_price);
    check_gbp("capitalContributions", capital_contributions);
    check_gbp("privateUseContributions", private_use_contributions);
    check_whole("co2", co2, "g/km");
    check_whole("electricRangeMiles", electric_range_miles, "miles");
    check_date("firstRegistered", first_registered);
    check_date("onDate", on_date);
    if diesel_supplement && co2 == 0 {
        panic!("a zero-emission car cannot take the diesel supplement");
    }

    let year = uk_tax_year(on_date);
    if first_registered > year.end.as_str() {
        panic!(
            "firstRegistered ({}) is after the end of tax year {}",
            first_registered, year.label
        );
    }
    let days_in_year = days_between(&year.start, &year.end) + 1;
    if unavailable_days < 0 || unavailable_days > days_in_year {
        panic!(
            "unavailableDays must be a whole number from 0 to {}, received {}",
            days_in_year, unavailable_days
        );
    }
    let days_available = days_in_year - unavailable_days;
    let registration = if first_registered < WLTP_FROM { "before-2020-04-06" } else { "from-2020-04-06" };
    let applies = |row_registration: &str| row_registration == "any" || row_registration == registration;

    let limit = match BIK_LIMITS
        .iter()
        .find(|row| applies(row.registration) && in_force(row.valid_from, row.valid_to, on_date))
    {
        Some(limit) => limit,
        None => missing(
            "company car benefit rates",
            &year.label,
            BIK_LIMITS.iter().map(|row| row.valid_from).min(),
            BIK_LIMITS_HISTORY,
            on_date,
        ),
    };

    let mut basis_points = if co2 >= 75 {
        // s139(3): one point per whole 5 g/km above 75, up to the year's maximum.
        (limit.from75_basis_points + 100 * ((co2 - 75) / 5)).min(limit.maximum_basis_points)
    } else {
        match BIK_BANDS.iter().find(|row| {
            applies(row.registration)
                && in_force(row.valid_from, row.valid_to, on_date)
                && row.co2_from <= co2
                && co2 <= row.co2_to
                && row.range_from <= electric_range_miles
                && row.range_to.map_or(true, |to| electric_range_miles <= to)
        }) {
            Some(band) => band.basis_points,
            None => missing(
                "company car benefit band",
                &year.label,
                BIK_BANDS.iter().map(|row| row.valid_from).min(),
                BIK_BANDS_HISTORY,
                on_date,
            ),
        }
    };
    if diesel_supplement {
        basis_points = (basis_points + limit.diesel_supplement_basis_points).min(limit.maximum_basis_points);
    }

    let counted = capital_contributions.minor.min(limit.capital_contribution_cap_minor);
    let price = money((list_price.minor - counted).max(0), "GBP");
    let provisional = proportion(price.minor, basis_points, days_available, days_in_year);
    let car_benefit = money((provisional - private_use_contributions.minor).max(0), "GBP");

    let mut fuel_benefit = money(0, "GBP");
    // s149(4): electricity, and energy for a car that cannot emit CO2, is not fuel.
    if free_fuel && co2 > 0 {
        let multiplier = match FUEL_MULTIPLIERS
            .iter()
            .find(|row| in_force(row.valid_from, row.valid_to, on_date))
        {
            Some(row) => row,
            None => missing(
                "car fuel benefit multiplier",
                &year.label,
                FUEL_MULTIPLIERS.iter().map(|row| row.valid_from).min(),
                FUEL_MULTIPLIERS_HISTORY,
                on_date,
            ),
        };
        fuel_benefit = money(
            proportion(multiplier.pounds * 100, basis_points, days_available, days_in_year),
            "GBP",
        );
    }

    let total = add_money(&car_benefit, &fuel_benefit);
    CompanyCarBik {
        tax_year: year.label.clone(),
        days_in_year,
        days_available,
        price,
        appropriate_basis_points: basis_points,
        car_benefit,
        fuel_benefit,
        total,
    }
}

pub fn company_car_bik_to_value(b: &CompanyCarBik) -> Value {
    Value::obj(vec![
        ("taxYear", Value::str(&b.tax_year)),
        ("daysInYear", Value::Int(b.days_in_year)),
        ("daysAvailable", Value::Int(b.days_available)),
        ("price", money_to_value(&b.price)),
        ("appropriateBasisPoints", Value::Int(b.appropriate_basis_points)),
        ("carBenefit", money_to_value(&b.car_benefit)),
        ("fuelBenefit", money_to_value(&b.fuel_benefit)),
        ("total", money_to_value(&b.total)),
    ])
}

fn whole(value: &Value, name: &str, unit: &str) -> i64 {
    match value {
        Value::Int(i) => *i,
        Value::Float(f) if f.fract() == 0.0 => *f as i64,
        Value::Float(f) => panic!("{} must be a whole number of {}, not negative, received {}", name, unit, f),
        _ => panic!("{} must be a whole number of {}, not negative, received a non-number", name, unit),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let co2 = whole(&args[2], "co2", "g/km");
    let range = whole(&args[3], "electricRangeMiles", "miles");
    let unavailable = match &args[7] {
        Value::Int(i) => *i,
        Value::Float(f) if f.fract() == 0.0 => *f as i64,
        other => panic!(
            "unavailableDays must be a whole number from 0 to 366, received {}",
            match other {
                Value::Float(f) => f.to_string(),
                _ => "a non-number".to_string(),
            }
        ),
    };
    company_car_bik_to_value(&company_car_bik(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        co2,
        range,
        args[4].as_bool(),
        args[5].as_str(),
        args[6].as_str(),
        unavailable,
        &money_from_value(&args[8]),
        args[9].as_bool(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 5 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 fleet.company-car-bik
Download for Rust fleet.company-car-bik-1.0.2-rust.fune · 65,717 bytes sha256 d3244063e7e8e6ace5cad878c1c41a9e815e1bc1f63fdb11f68cd99cd8ea7f66

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./fleet.company-car-bik-1.0.2-rust.fune, or fetch it from a terminal with fune pull fleet.company-car-bik@1.0.2:rust.

The whole function, every language, is one file too: fleet.company-car-bik-1.0.2.fune, 80,480 bytes, sha256 7e493f52d22a4498ea6cd4387b67b820b51da25c702c6184ec830b306bda0ff1. 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 fleet.company-car-bik

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

// fune: after fleet.company-car-bik

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 fleet.company-car-bik
// fune: replace dates.uk-tax-year in fleet.company-car-bik
// fune: replace math.round-div in fleet.company-car-bik
// fune: replace money.add in fleet.company-car-bik
// fune: replace money.amount in fleet.company-car-bik

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 fleet.company-car-bik --steps.

// fune: step fleet.company-car-bik 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
petrol car at 120 g/km in 2025-26 is 30% of 30,000.00 £30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, false → tax year 2025/26, days in year 365, days available 365, price £30,000.00, appropriate basis points 30%, car benefit £9,000.00, fuel benefit £0.00, total £9,000.00
free fuel adds 30% of the 28,200 multiplier £30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, true → tax year 2025/26, days in year 365, days available 365, price £30,000.00, appropriate basis points 30%, car benefit £9,000.00, fuel benefit £8,460.00, total £17,460.00
electric car at 3% in 2025-26, and electricity is not fuel so no fuel benefit £40,000.00, £0.00, 0, 250, false, 2024-01-10, 2025-09-01, 0, £0.00, true → tax year 2025/26, days in year 365, days available 365, price £40,000.00, appropriate basis points 3%, car benefit £1,200.00, fuel benefit £0.00, total £1,200.00
plug-in hybrid at 30 g/km with a 45 mile range is 10% in 2026-27 £45,000.00, £0.00, 30, 45, false, 2025-06-01, 2026-06-01, 0, £0.00, false → tax year 2026/27, days in year 365, days available 365, price £45,000.00, appropriate basis points 10%, car benefit £4,500.00, fuel benefit £0.00, total £4,500.00
the same hybrid is 18% in 2028-29, when range stops mattering £45,000.00, £0.00, 30, 45, false, 2025-06-01, 2028-05-01, 0, £0.00, false → tax year 2028/29, days in year 365, days available 365, price £45,000.00, appropriate basis points 18%, car benefit £8,100.00, fuel benefit £0.00, total £8,100.00
2021-22, car first registered before 6 April 2020 uses the NEDC column: 100 g/km is 25% £25,000.00, £0.00, 100, 0, false, 2019-05-01, 2021-10-01, 0, £0.00, false → tax year 2021/22, days in year 365, days available 365, price £25,000.00, appropriate basis points 25%, car benefit £6,250.00, fuel benefit £0.00, total £6,250.00
2021-22, first registered on 6 April 2020 uses the WLTP column: 24% £25,000.00, £0.00, 100, 0, false, 2020-04-06, 2021-10-01, 0, £0.00, false → tax year 2021/22, days in year 365, days available 365, price £25,000.00, appropriate basis points 24%, car benefit £6,000.00, fuel benefit £0.00, total £6,000.00
2021-22, first registered on 5 April 2020 is still NEDC £25,000.00, £0.00, 100, 0, false, 2020-04-05, 2021-10-01, 0, £0.00, false → tax year 2021/22, days in year 365, days available 365, price £25,000.00, appropriate basis points 25%, car benefit £6,250.00, fuel benefit £0.00, total £6,250.00
2023-24 has 366 days: a diesel unavailable 61 days pays for 305/366 of 35% £30,000.00, £0.00, 130, 0, true, 2019-01-01, 2023-11-15, 61, £0.00, false → tax year 2023/24, days in year 366, days available 305, price £30,000.00, appropriate basis points 35%, car benefit £8,750.00, fuel benefit £0.00, total £8,750.00
the diesel supplement stops at the 37% maximum £20,000.00, £0.00, 158, 0, true, 2017-01-01, 2024-12-01, 0, £0.00, false → tax year 2024/25, days in year 365, days available 365, price £20,000.00, appropriate basis points 37%, car benefit £7,400.00, fuel benefit £0.00, total £7,400.00
Show the other 27 tests
CaseArgumentsExpected
capital contributions count only up to 5,000.00; private use payments come off after £30,000.00, £8,000.00, 0, 300, false, 2024-01-01, 2025-09-01, 0, £200.00, false → tax year 2025/26, days in year 365, days available 365, price £25,000.00, appropriate basis points 3%, car benefit £550.00, fuel benefit £0.00, total £550.00
private use payments above the benefit leave it at nil, not negative £20,000.00, £0.00, 0, 300, false, 2024-01-01, 2025-09-01, 0, £1,000.00, false → tax year 2025/26, days in year 365, days available 365, price £20,000.00, appropriate basis points 3%, car benefit £0.00, fuel benefit £0.00, total £0.00
unavailability rounds the car benefit down to the penny; the fuel benefit is 265/365 of 34% of 29,200 £33,333.33, £0.00, 142, 0, false, 2022-01-01, 2026-08-01, 100, £0.00, true → tax year 2026/27, days in year 365, days available 265, price £33,333.33, appropriate basis points 34%, car benefit £8,228.30, fuel benefit £7,208.00, total £15,436.30
2026-27: 77 g/km is still 21%, the same as 70-74 g/km £10,000.00, £0.00, 77, 0, false, 2022-01-01, 2026-08-01, 0, £0.00, false → tax year 2026/27, days in year 365, days available 365, price £10,000.00, appropriate basis points 21%, car benefit £2,100.00, fuel benefit £0.00, total £2,100.00
2026-27: 74 g/km is 21% £10,000.00, £0.00, 74, 0, false, 2022-01-01, 2026-08-01, 0, £0.00, false → tax year 2026/27, days in year 365, days available 365, price £10,000.00, appropriate basis points 21%, car benefit £2,100.00, fuel benefit £0.00, total £2,100.00
2025-26: 50 g/km with a 130 mile range is 3% £10,000.00, £0.00, 50, 130, false, 2024-01-01, 2025-09-01, 0, £0.00, false → tax year 2025/26, days in year 365, days available 365, price £10,000.00, appropriate basis points 3%, car benefit £300.00, fuel benefit £0.00, total £300.00
2025-26: 50 g/km with a 129 mile range is 6% £10,000.00, £0.00, 50, 129, false, 2024-01-01, 2025-09-01, 0, £0.00, false → tax year 2025/26, days in year 365, days available 365, price £10,000.00, appropriate basis points 6%, car benefit £600.00, fuel benefit £0.00, total £600.00
2025-26: 51 g/km is 16% whatever the range £10,000.00, £0.00, 51, 130, false, 2024-01-01, 2025-09-01, 0, £0.00, false → tax year 2025/26, days in year 365, days available 365, price £10,000.00, appropriate basis points 16%, car benefit £1,600.00, fuel benefit £0.00, total £1,600.00
2029-30 maximum is 39% £10,000.00, £0.00, 250, 0, false, 2027-01-01, 2029-09-01, 0, £0.00, false → tax year 2029/30, days in year 365, days available 365, price £10,000.00, appropriate basis points 39%, car benefit £3,900.00, fuel benefit £0.00, total £3,900.00
2029-30 zero emission is 9% £50,000.00, £0.00, 0, 300, false, 2027-01-01, 2029-09-01, 0, £0.00, false → tax year 2029/30, days in year 365, days available 365, price £50,000.00, appropriate basis points 9%, car benefit £4,500.00, fuel benefit £0.00, total £4,500.00
5 April 2026 is still 2025-26: 3% for a zero-emission car £10,000.00, £0.00, 0, 0, false, 2024-01-01, 2026-04-05, 0, £0.00, false → tax year 2025/26, days in year 365, days available 365, price £10,000.00, appropriate basis points 3%, car benefit £300.00, fuel benefit £0.00, total £300.00
6 April 2026 starts 2026-27: 4% £10,000.00, £0.00, 0, 0, false, 2024-01-01, 2026-04-06, 0, £0.00, false → tax year 2026/27, days in year 365, days available 365, price £10,000.00, appropriate basis points 4%, car benefit £400.00, fuel benefit £0.00, total £400.00
a car unavailable all year has no benefit £30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 365, £0.00, true → tax year 2025/26, days in year 365, days available 0, price £30,000.00, appropriate basis points 30%, car benefit £0.00, fuel benefit £0.00, total £0.00
no fuel benefit multiplier is published yet for 2027-28 £30,000.00, £0.00, 120, 0, false, 2023-03-01, 2027-09-01, 0, £0.00, true → error: no car fuel benefit multiplier for tax year 2027/28
2020-21 is before the tables this carries £30,000.00, £0.00, 120, 0, false, 2019-01-01, 2020-09-01, 0, £0.00, false → error: no company car benefit rates for tax year 2020/21
only GBP $30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, false → error: company car benefit is worked in GBP
a negative list price is an error -£0.01, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, false → error: listPrice must not be negative
more unavailable days than the year has £30,000.00, £0.00, 120, 0, false, 2023-03-01, 2025-09-01, 366, £0.00, false → error: unavailableDays must be a whole number from 0 to 365
a fractional CO2 figure must be rounded down by the caller £30,000.00, £0.00, 120.5, 0, false, 2023-03-01, 2025-09-01, 0, £0.00, false → error: co2 must be a whole number of g/km
a negative electric range £30,000.00, £0.00, 120, -1, false, 2023-03-01, 2025-09-01, 0, £0.00, false → error: electricRangeMiles must be a whole number of miles
a zero-emission car has no diesel supplement £30,000.00, £0.00, 0, 0, true, 2023-03-01, 2025-09-01, 0, £0.00, false → error: a zero-emission car cannot take the diesel supplement
a malformed date £30,000.00, £0.00, 120, 0, false, 2023-03-01, 1/9/2025, 0, £0.00, false → error: onDate must be an ISO date (YYYY-MM-DD)
a car first registered after the tax year £30,000.00, £0.00, 120, 0, false, 2026-05-01, 2025-09-01, 0, £0.00, false → error: is after the end of tax year 2025/26
a onDate with a trailing newline is refused £30,000.00, £0.00, 120, 0, false, 2023-03-01, 2026-09-16 , 0, £0.00, false → error: onDate must be an ISO date (YYYY-MM-DD), received "2026-09-16 "
a onDate in Arabic-Indic digits is refused £30,000.00, £0.00, 120, 0, false, 2023-03-01, ٢٠٢٦-٠٩-١٦, 0, £0.00, false → error: onDate must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-١٦"
a firstRegistered with a trailing newline is refused £30,000.00, £0.00, 120, 0, false, 2023-03-01 , 2025-09-01, 0, £0.00, false → error: firstRegistered must be an ISO date (YYYY-MM-DD), received "2023-03-01 "
a firstRegistered in Arabic-Indic digits is refused £30,000.00, £0.00, 120, 0, false, ٢٠٢٦-٠٩-١٦, 2025-09-01, 0, £0.00, false → error: firstRegistered must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-١٦"

More from the author

1. the price: list price plus taxable accessories (`listPrice`), less the employee's capital contributions, of which only the first 5,000.00 counts (s132); 2. times the appropriate percentage for the car's CO2 figure and, for 1-50 g/km, its electric range, in that tax year (s139), plus the 4% diesel supplement where it applies (s141); 3. cut for the days the car was unavailable: price x percentage x (days in year - unavailable days) / days in year (s143); 4. less what the employee was required to pay, and paid, for private use, never below nil (s144).

Free fuel (s149-150) is the year's car fuel benefit multiplier times the same appropriate percentage, cut for the same unavailable days. It is not reduced by private use payments. Electricity is not fuel (s149(4)), so a zero-emission car has no fuel benefit even when `freeFuel` is true.

`taxYear`, `daysInYear` (366 in a year containing 29 February, such as 2023-24 and 2027-28) and the percentage come back with the answer, so a payroll line can show its working.

## What the caller decides

- `co2` is the whole-number g/km on the registration documents: the NEDC figure for a car first registered before 6 April 2020, WLTP otherwise. s139(2) rounds a fractional figure down; do that before calling. - `electricRangeMiles` is rounded up to a whole mile (s139(2)(b)). It only matters for 1-50 g/km up to 2027-28; from 2028-29 every 1-50 g/km car has one percentage. - `dieselSupplement` is true for a car propelled solely by diesel that is not certified to Euro 6d (RDE2). Hybrids are not "solely by diesel". The supplement never takes the percentage past the year's maximum. - `firstRegistered` picks the table column. Only 2021-22 has different percentages for cars first registered before 6 April 2020 (NEDC, one point higher). It must not be after the tax year. - `unavailableDays` are the days before the car was first available, after it was last available, and in any spell of 30 or more consecutive days when it was not available. Shorter gaps do not count; this function trusts the count.

## Rounding

Each benefit is one exact division, rounded down to the penny, so neither is ever more than the statutory figure. The P11D takes whole pounds; converting is left to the caller. The same rounding is used for 2021-22 and 2029-30.

## What it does not do

Cars without a CO2 figure (s140), cars first registered before 1998 (s142), shared cars, a replacement car (s145), classic cars valued at market value, optional remuneration arrangements (salary sacrifice, s121A), fuel withdrawn part way through the year (s152), and the easement for plug-in hybrids approved under Euro 6e-bis from 1 January 2025 (EIM24710). Vans are a different charge.

## Rates

Three tables in `data/`, each with dated rows, looked up on `onDate`:

- `bik-bands.json`: the percentages below 75 g/km, including 0 g/km and the 1-50 g/km electric range bands, by tax year and registration column. - `bik-limits.json`: the percentage at 75 g/km, the maximum, the 4% diesel supplement and the 5,000.00 capital contribution cap. From 75 g/km the percentage rises one point per whole 5 g/km (s139(3)). Note that since 2025-26 the 70-74 and 75-79 g/km bands share a percentage (21% in 2026-27), so "one point per 5 g/km from 55" gets 75-79 wrong. - `fuel-multipliers.json`: the car fuel benefit multiplier, 24,600 (2021-22), 25,300, 27,800, 27,800, 28,200 and 29,200 (2026-27). The multiplier for 2027-28 and later is set each Budget and is not published yet, so free fuel in those years is an error rather than a guess; 2027-28 onward percentages are legislated, so the car benefit itself is answered.

| tax year | 0 g/km | 1-50 g/km by range (130+, 70-129, 40-69, 30-39, <30) | 51-54 | at 75 g/km | max | |---|---|---|---|---|---| | 2021-22 WLTP | 1% | 1, 4, 7, 11, 13% | 14% | 19% | 37% | | 2021-22 NEDC | 1% | 2, 5, 8, 12, 14% | 15% | 20% | 37% | | 2022-23 to 2024-25 | 2% | 2, 5, 8, 12, 14% | 15% | 20% | 37% | | 2025-26 | 3% | 3, 6, 9, 13, 15% | 16% | 21% | 37% | | 2026-27 | 4% | 4, 7, 10, 14, 16% | 17% | 21% | 37% | | 2027-28 | 5% | 5, 8, 11, 15, 17% | 18% | 21% | 37% | | 2028-29 | 7% | 18% | 19% | 22% | 38% | | 2029-30 on | 9% | 19% | 20% | 23% | 39% |

Years before 2021-22 are not carried and are an error. The tables declare effective columns, so a `history=current` build refuses an earlier tax year instead of answering it with today's figures. When a rate changes, a new version of this capability carries it.

## Sources

- HMRC Employment Income Manual, EIM24705, "Car benefit calculation: step 5: appropriate percentage: tax years 2015 to 2016 onwards: ready reckoner for type A cars" (tables 2020-21 to 2026-27, NEDC and WLTP columns, the diesel supplement and Euro 6d): https://www.gov.uk/hmrc-internal-manuals/employment-income-manual/eim24705 - Finance Act 2023 s11 (tables for 2025-26, 2026-27 and 2027-28, and 21% at 75 g/km): https://www.legislation.gov.uk/ukpga/2023/1/section/11/enacted - Finance Act 2025 s5 and s6 (2028-29 and 2029-30, maximum 38% and 39%): https://www.legislation.gov.uk/ukpga/2025/8/section/5/enacted and https://www.legislation.gov.uk/ukpga/2025/8/section/6/enacted - HMRC policy paper, "Taxation of company cars: The appropriate percentage for tax years 2028 to 2029 and 2029 to 2030" (30 October 2024): https://www.gov.uk/government/publications/income-tax-company-car-tax-rates-2028-to-2030 - ITEPA 2003 s132 (capital contributions, 5,000), s139, s141 (diesel), s143 (unavailability), s144 (private use payments), s149 (fuel; electricity is not fuel): https://www.legislation.gov.uk/ukpga/2003/1/part/3/chapter/6 - HMRC, "Travel - mileage and fuel rates and allowances" (car fuel benefit multiplier by tax year): https://www.gov.uk/government/publications/rates-and-allowances-travel-mileage-and-fuel-allowances/travel-mileage-and-fuel-rates-and-allowances

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

## Before you rely on this

**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 above, and have a payroll or benefits-in-kind specialist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll or benefits-in-kind specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

## Notices

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

Legislation: Crown copyright and database right.

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

Files

PathBytes
NOTICE234
README.md7,700
data/bik-bands.json17,562
data/bik-limits.json1,795
data/fuel-multipliers.json999
impl/python.py7,316
impl/rust.rs9,926
impl/typescript.ts6,940
vectors.json16,555