use super::funejson::Value; use super::validation_uk_company_number_data::COMPANY_PREFIXES; /// Every company number is eight characters: eight digits, or a two-character /// prefix and six digits. const LENGTH: usize = 8; fn invalid(reason: &str) -> UkCompanyNumber { UkCompanyNumber { valid: false, normalised: None, prefix: None, jurisdiction: None, company_type: None, reason: Some(reason.to_string()), } } /// Check a Companies House company number's shape and prefix. /// /// There is no check digit, so a pass means the number is shaped like one /// Companies House issues under a prefix it uses, not that the company exists: /// that is a lookup on the register. Checks run in a fixed order (characters, /// shape, length, prefix) so every language gives the same first reason. pub fn uk_company_number(value: &str) -> UkCompanyNumber { let mut compact = String::new(); for ch in value.chars() { if ch == ' ' { continue; } // ASCII-only upper-casing, so every language folds identically. let ch = ch.to_ascii_uppercase(); if !ch.is_ascii_uppercase() && !ch.is_ascii_digit() { return invalid("bad-character"); } compact.push(ch); } if compact.is_empty() { return invalid("empty"); } // Everything is ASCII by now, so byte indexing is character indexing. let bytes = compact.as_bytes(); // R0 (pre-partition Northern Ireland) is the one prefix containing a digit. let prefix_len = if compact.starts_with("R0") || (bytes.len() >= 2 && bytes[0].is_ascii_uppercase() && bytes[1].is_ascii_uppercase()) { 2 } else if bytes[0].is_ascii_uppercase() { return invalid("bad-format"); } else { 0 }; let prefix = &compact[..prefix_len]; let digits = &compact[prefix_len..]; if !digits.bytes().all(|b| b.is_ascii_digit()) { return invalid("bad-format"); } let width = LENGTH - prefix_len; if digits.is_empty() || digits.len() > width { return invalid("bad-length"); } // Leading zeros are part of the number and are often dropped when typed, // so they are put back; a number of nothing but zeros is no company. if digits.bytes().all(|b| b == b'0') { return invalid("bad-format"); } match COMPANY_PREFIXES.iter().find(|row| row.prefix == prefix) { Some(row) => UkCompanyNumber { valid: true, normalised: Some(format!("{}{}{}", prefix, "0".repeat(width - digits.len()), digits)), prefix: Some(prefix.to_string()), jurisdiction: Some(row.jurisdiction.to_string()), company_type: Some(row.company_type.to_string()), reason: None, }, None => invalid("unknown-prefix"), } } /// Object keys are camelCase to match the shared vectors, and so that a /// capability building on this one can reuse the same shape. pub fn uk_company_number_to_value(result: &UkCompanyNumber) -> Value { let text = |field: &Option| match field { Some(s) => Value::str(s), None => Value::Null, }; Value::obj(vec![ ("valid", Value::Bool(result.valid)), ("normalised", text(&result.normalised)), ("prefix", text(&result.prefix)), ("jurisdiction", text(&result.jurisdiction)), ("companyType", text(&result.company_type)), ("reason", text(&result.reason)), ]) } pub fn fune_vector(args: &[Value]) -> Value { // A non-string argument arrives here as an empty string, which is exactly // the answer TypeScript and Python give for a non-string: empty. uk_company_number_to_value(&uk_company_number(args[0].as_str())) }