Functional Weave
Code in Rust

property.council-tax-band@1.0.2

impl/rust.rs

3,690 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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::property_council_tax_band_data::{BandRatio, BAND_RATIOS, BAND_RATIOS_HISTORY};  ← this capability’s own data, compiled from data/band-ratios.json into the same file by fune build

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