Functional Weave
Code in TypeScript

energy.bill-calculate@1.0.0

impl/rust.rs

5,413 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::energy_climate_change_levy::climate_change_levy;  ← from energy.climate-change-levy ^1.0.0 · built alongside by fune
use super::energy_gas_kwh::gas_kwh;  ← from energy.gas-kwh ^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::finance_tax_add_vat::add_vat;  ← from finance.tax.add-vat ^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

// watt-hours x thousandths of a penny per kWh is millionths of a penny.
const DIVISOR: i64 = 1_000_000;
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 thousandths of a penny, 0 or more, received {}",
            what, value
        );
    }
}

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

/// A GB electricity or gas bill from two reads: energy at the unit rate,
/// standing charge by the day, Climate Change Levy unless the supply is for
/// qualifying use, and VAT on the lot at the caller's category.
///
/// # Panics
/// Panics on an unknown fuel, negative rates, reads that do not fit the
/// register or are in the wrong order, gas details missing (or given for
/// electricity), or anything the capabilities it uses refuse.
pub fn calculate_energy_bill(input: &EnergyBillInput, mode: &str) -> EnergyBill {
    if input.fuel != "electricity" && input.fuel != "gas" {
        panic!("unknown fuel \"{}\": expected electricity or gas", input.fuel);
    }
    check_rate("unitRate", input.unit_rate);
    check_rate("standingCharge", input.standing_charge);
    let units = meter_advance(input.opening_read.value, input.closing_read.value, input.meter_digits);
    let days = days_between(&input.opening_read.date, &input.closing_read.date);
    if days < 0 {
        panic!(
            "the closing read must not be dated before the opening read, received {} and {}",
            input.opening_read.date, input.closing_read.date
        );
    }
    let watt_hours = if input.fuel == "gas" {
        match (&input.gas_unit, input.calorific_value) {
            (Some(unit), Some(cv)) => gas_kwh(units, unit, cv).watt_hours,
            _ => panic!("a gas bill needs gasUnit and calorificValue"),
        }
    } else {
        if input.gas_unit.is_some() || input.calorific_value.is_some() {
            panic!("gasUnit and calorificValue are for gas bills only");
        }
        units * 1000
    };
    let energy = round_div(exact(watt_hours as i128 * input.unit_rate as i128), DIVISOR, mode);
    let standing = round_div(exact(days as i128 * input.standing_charge as i128), 1000, mode);
    let ccl = climate_change_levy(&input.fuel, watt_hours, &input.closing_read.date, input.qualifying_use, mode)
        .levy
        .minor;
    let vat = add_vat(
        &money(energy + standing + ccl, "GBP"),
        "GB",
        &input.vat_category,
        &input.closing_read.date,
    );
    EnergyBill {
        units,
        watt_hours,
        days,
        energy_charge: money(energy, "GBP"),
        standing_charge: money(standing, "GBP"),
        climate_change_levy: money(ccl, "GBP"),
        net: vat.net,
        vat: vat.tax,
        vat_basis_points: vat.basis_points,
        total: vat.gross,
    }
}

fn whole(v: &Value, message: &str) -> i64 {
    if let Value::Float(f) = v {
        panic!("{}, received {}", message, f);
    }
    v.as_i64()
}

fn meter_read_from_value(v: &Value) -> MeterRead {
    MeterRead {
        date: v.get("date").as_str().to_string(),
        value: whole(v.get("value"), "reads must be whole numbers"),
    }
}

pub fn energy_bill_input_from_value(v: &Value) -> EnergyBillInput {
    EnergyBillInput {
        fuel: v.get("fuel").as_str().to_string(),
        opening_read: meter_read_from_value(v.get("openingRead")),
        closing_read: meter_read_from_value(v.get("closingRead")),
        meter_digits: v.get("meterDigits").as_i64(),
        gas_unit: if v.get("gasUnit").is_null() { None } else { Some(v.get("gasUnit").as_str().to_string()) },
        calorific_value: if v.get("calorificValue").is_null() {
            None
        } else {
            Some(v.get("calorificValue").as_i64())
        },
        unit_rate: whole(v.get("unitRate"), "unitRate must be a whole number of thousandths of a penny, 0 or more"),
        standing_charge: whole(
            v.get("standingCharge"),
            "standingCharge must be a whole number of thousandths of a penny, 0 or more",
        ),
        qualifying_use: v.get("qualifyingUse").as_bool(),
        vat_category: v.get("vatCategory").as_str().to_string(),
    }
}

pub fn energy_bill_to_value(b: &EnergyBill) -> Value {
    Value::obj(vec![
        ("units", Value::Int(b.units)),
        ("wattHours", Value::Int(b.watt_hours)),
        ("days", Value::Int(b.days)),
        ("energyCharge", money_to_value(&b.energy_charge)),
        ("standingCharge", money_to_value(&b.standing_charge)),
        ("climateChangeLevy", money_to_value(&b.climate_change_levy)),
        ("net", money_to_value(&b.net)),
        ("vat", money_to_value(&b.vat)),
        ("vatBasisPoints", Value::Int(b.vat_basis_points)),
        ("total", money_to_value(&b.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    energy_bill_to_value(&calculate_energy_bill(&energy_bill_input_from_value(&args[0]), args[1].as_str()))
}