Functional Weave
Code in Rust

payroll.tax-code-parse@1.0.2

impl/rust.rs

4,541 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::money_amount::{money, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

/// The whitespace every language trims alike: Rust's trim() also takes \x85,
/// which JavaScript's trim() keeps, and that takes \u{feff}, which this keeps.
fn is_space(c: char) -> bool {
    matches!(
        c,
        ' ' | '\t' | '\n' | '\r' | '\u{0b}' | '\u{0c}' | '\u{a0}' | '\u{1680}' | '\u{2000}'..='\u{200a}' | '\u{2028}' | '\u{2029}' | '\u{202f}' | '\u{205f}' | '\u{3000}'
    )
}

fn all_digits(s: &str) -> bool {
    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}

/// A 1-6 digit number with no leading zero (or "0" itself when `zero_ok`).
fn code_number(s: &str, zero_ok: bool) -> Option<i64> {
    if !all_digits(s) || s.len() > 6 {
        return None;
    }
    if s.len() > 1 && s.starts_with('0') {
        return None;
    }
    let n: i64 = s.parse().ok()?;
    if n == 0 && !zero_ok {
        return None;
    }
    Some(n)
}

/// Take a UK PAYE tax code apart into its region, kind, allowance and basis.
///
/// The annual allowance is number x £10 + £9, not number x £10: a code covers
/// a £10 band of allowances and HMRC's routines give the employee the top of
/// it, so 1257L is worth £12,579 a year to the payroll.
///
/// # Panics
/// Panics on a code HMRC does not issue.
pub fn parse_tax_code(code: &str) -> TaxCode {
    let original = code;
    let upper = code.trim_matches(is_space).to_uppercase();
    if upper.is_empty() {
        panic!("unrecognised tax code \"{}\"", original);
    }

    // The basis marker may be written with or without a space.
    let mut cumulative = true;
    let mut text: &str = &upper;
    for marker in ["W1", "M1", "X"] {
        if let Some(rest) = upper.strip_suffix(marker) {
            let rest = rest.trim_end_matches(is_space);
            if !rest.is_empty() {
                text = rest;
                cumulative = false;
            }
            break;
        }
    }

    let (region, body) = if let Some(rest) = text.strip_prefix('S') {
        ("scotland", rest)
    } else if let Some(rest) = text.strip_prefix('C') {
        ("wales", rest)
    } else {
        ("rest-of-uk", text)
    };

    let kind: &str;
    let mut number = 0i64;
    let mut suffix: Option<String> = None;
    let mut allowance_pounds = 0i64;

    let last = body.chars().last();
    if body == "NT" {
        if region != "rest-of-uk" {
            panic!("there is no Scottish or Welsh NT code: \"{}\"", original);
        }
        kind = "no-tax";
    } else if body == "BR" {
        kind = "basic-rate";
    } else if body.len() == 2 && body.starts_with('D') && all_digits(&body[1..]) {
        kind = "d-rate";
        number = body[1..].parse().unwrap();
    } else if let Some(n) = body.strip_prefix('K').and_then(|rest| code_number(rest, false)) {
        kind = "negative-allowance";
        number = n;
        allowance_pounds = -(n * 10 + 9);
    } else if let Some(n) = match last {
        Some('L') | Some('M') | Some('N') | Some('T') => code_number(&body[..body.len() - 1], true),
        _ => None,
    } {
        let letter = last.unwrap();
        if n == 0 && letter != 'T' {
            panic!(
                "unrecognised tax code \"{}\": a zero allowance is only issued as 0T",
                original
            );
        }
        kind = "allowance";
        number = n;
        suffix = Some(letter.to_string());
        allowance_pounds = if n == 0 { 0 } else { n * 10 + 9 };
    } else {
        panic!("unrecognised tax code \"{}\"", original);
    }

    let prefix = match region {
        "scotland" => "S",
        "wales" => "C",
        _ => "",
    };
    TaxCode {
        code: format!("{}{}", prefix, body),
        region: region.to_string(),
        kind: kind.to_string(),
        number,
        suffix,
        annual_allowance: money(allowance_pounds * 100, "GBP"),
        cumulative,
    }
}

pub fn tax_code_to_value(t: &TaxCode) -> Value {
    Value::obj(vec![
        ("code", Value::str(&t.code)),
        ("region", Value::str(&t.region)),
        ("kind", Value::str(&t.kind)),
        ("number", Value::Int(t.number)),
        (
            "suffix",
            match &t.suffix {
                Some(s) => Value::str(s),
                None => Value::Null,
            },
        ),
        ("annualAllowance", money_to_value(&t.annual_allowance)),
        ("cumulative", Value::Bool(t.cumulative)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    tax_code_to_value(&parse_tax_code(args[0].as_str()))
}