use super::funejson::Value; use super::energy_climate_change_levy_data::{CCL_RATES, CCL_RATES_HISTORY}; use super::math_round_div::round_div; use super::money_amount::{money, money_to_value}; // quantity (thousandths of a unit) x rate (thousandths of a penny) is in // millionths of a penny. const DIVISOR: i64 = 1_000_000; const MAX_SAFE: i128 = 9_007_199_254_740_991; 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()) } /// The Climate Change Levy on one supply, at the main rate in force on /// `on_date`. A qualifying-use supply (domestic, charity non-business or /// de minimis) is outside the levy and comes back excluded, with a zero levy. /// /// # Panics /// Panics on a malformed date, a negative quantity, an unknown commodity, a /// date no rate covers, or a levy too large to be exact in every language. pub fn climate_change_levy( commodity: &str, quantity: i64, on_date: &str, qualifying_use: bool, mode: &str, ) -> ClimateChangeLevy { if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } if quantity < 0 { panic!( "quantity must be a whole number of thousandths of a unit, 0 or more, received {}", quantity ); } if !CCL_RATES.iter().any(|r| r.commodity == commodity) { panic!("unknown commodity \"{}\": expected electricity, gas, lpg or other", commodity); } let mut best = None; for r in CCL_RATES { if r.commodity != commodity || on_date < r.valid_from { continue; } if let Some(to) = r.valid_to { if on_date > to { continue; } } if best.map_or(true, |b: &super::energy_climate_change_levy_data::CclRate| r.valid_from > b.valid_from) { best = Some(r); } } let best = match best { Some(r) => r, None => { // A pruned build must not answer an old date with a newer rate. if CCL_RATES_HISTORY != "full" { let mut earliest: Option<&'static str> = None; for r in CCL_RATES { if r.commodity == commodity && earliest.map_or(true, |e| r.valid_from < e) { earliest = Some(r.valid_from); } } if let Some(earliest) = earliest { if on_date < earliest { panic!( "no Climate Change Levy rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older supplies.", commodity, on_date, CCL_RATES_HISTORY, earliest ); } } } panic!("no Climate Change Levy rate for {} on {}", commodity, on_date) } }; let mut pence = 0; if !qualifying_use { let product = quantity as i128 * best.rate as i128; if product > MAX_SAFE { panic!("levy too large to calculate exactly"); } pence = round_div(product as i64, DIVISOR, mode); } ClimateChangeLevy { commodity: commodity.to_string(), unit: best.unit.to_string(), rate: best.rate, excluded: qualifying_use, levy: money(pence, "GBP"), valid_from: best.valid_from.to_string(), valid_to: best.valid_to.map(str::to_string), } } pub fn climate_change_levy_to_value(c: &ClimateChangeLevy) -> Value { Value::obj(vec![ ("commodity", Value::str(&c.commodity)), ("unit", Value::str(&c.unit)), ("rate", Value::Int(c.rate)), ("excluded", Value::Bool(c.excluded)), ("levy", money_to_value(&c.levy)), ("validFrom", Value::str(&c.valid_from)), ( "validTo", match &c.valid_to { Some(d) => Value::str(d), None => Value::Null, }, ), ]) } pub fn fune_vector(args: &[Value]) -> Value { if let Value::Float(f) = &args[1] { panic!("quantity must be a whole number of thousandths of a unit, 0 or more, received {}", f); } climate_change_levy_to_value(&climate_change_levy( args[0].as_str(), args[1].as_i64(), args[2].as_str(), args[3].as_bool(), args[4].as_str(), )) }