use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::property_council_tax_band_data::{BandRatio, BAND_RATIOS, BAND_RATIOS_HISTORY}; // Keeps Band D × the largest proportion exact as a JavaScript number; the same limit here. const MAX_MINOR: i64 = 1_000_000_000_000; fn is_iso_date(value: &str) -> bool { let bytes = value.as_bytes(); bytes.len() == 10 && bytes.iter().enumerate().all(|(i, b)| if i == 4 || i == 7 { *b == b'-' } else { b.is_ascii_digit() }) } /// The council tax for a band: Band D × the band's statutory proportion ÷ Band /// D's, rounded half-up to the penny once. The proportions are dated rows, so /// a Scottish band E before April 2017 uses 440/360 and after it 473/360. /// /// # Panics /// Panics on a malformed date, an unknown nation, a Band D charge not in GBP /// or out of range, or a band the nation did not have on that date. pub fn council_tax_band(band_d_charge: &Money, band: &str, nation: &str, on_date: &str) -> CouncilTaxBandCharge { if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } if !["england", "wales", "scotland"].contains(&nation) { panic!("unknown nation \"{}\": use england, wales or scotland", nation); } if band_d_charge.currency != "GBP" { panic!("bandDCharge must be in GBP, received {}", band_d_charge.currency); } if band_d_charge.minor < 0 || band_d_charge.minor > MAX_MINOR { panic!("bandDCharge must be between 0 and {} minor units, received {}", MAX_MINOR, band_d_charge.minor); } let mut found: Option<&'static BandRatio> = None; let mut earliest: Option<&'static str> = None; for row in BAND_RATIOS { if row.nation != nation { continue; } if earliest.map_or(true, |e| row.valid_from < e) { earliest = Some(row.valid_from); } if row.band != band || on_date < row.valid_from { continue; } if let Some(to) = row.valid_to { if on_date > to { continue; } } found = Some(row); } let row = match found { Some(row) => row, None => { if BAND_RATIOS_HISTORY != "full" { if let Some(e) = earliest { if on_date < e { panic!( "no council tax band {} for {} on {}: this build was installed with history={}, so it only carries rules from {}", band, nation, on_date, BAND_RATIOS_HISTORY, e ); } } } panic!("no council tax band {} for {} on {}", band, nation, on_date) } }; CouncilTaxBandCharge { band: band.to_string(), ratio_numerator: row.numerator, ratio_denominator: row.denominator, charge: money(round_div(band_d_charge.minor * row.numerator, row.denominator, "half-up"), "GBP"), } } pub fn council_tax_band_charge_to_value(result: &CouncilTaxBandCharge) -> Value { Value::obj(vec![ ("band", Value::str(&result.band)), ("ratioNumerator", Value::Int(result.ratio_numerator)), ("ratioDenominator", Value::Int(result.ratio_denominator)), ("charge", money_to_value(&result.charge)), ]) } pub fn fune_vector(args: &[Value]) -> Value { council_tax_band_charge_to_value(&council_tax_band( &money_from_value(&args[0]), args[1].as_str(), args[2].as_str(), args[3].as_str(), )) }