use super::funejson::Value; use super::money_amount::{money, money_to_value}; 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 { 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().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(); 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 = 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())) }