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