use super::funejson::Value; use super::dates_days_between::days_between; use super::energy_climate_change_levy::climate_change_levy; use super::energy_gas_kwh::gas_kwh; use super::energy_meter_advance::meter_advance; use super::finance_tax_add_vat::add_vat; use super::math_round_div::round_div; use super::money_amount::{money, money_to_value}; // 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())) }