Functional Weave
Code in Rust

fleet.company-car-bik@1.0.1

impl/rust.rs

9,926 bytes · the Rust 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.

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