use super::funejson::Value; fn invalid(reason: &str) -> GtinCheck { GtinCheck { valid: false, normalised: None, kind: None, gtin14: None, reason: Some(reason.to_string()), } } /// Check an EAN-8, UPC-A, EAN-13 or GTIN-14 against the GS1 mod-10 check digit. /// /// A pass means the number was typed correctly, not that GS1 issued it. /// Validators answer rather than panic: a malformed value is the answer "no". pub fn validate_gtin(value: &str) -> GtinCheck { // Barcodes are printed in groups, so spaces and hyphens are ignored anywhere. let chars: Vec = value.chars().filter(|ch| *ch != ' ' && *ch != '-').collect(); if chars.is_empty() { return invalid("empty"); } if chars.iter().any(|ch| !ch.is_ascii_digit()) { return invalid("bad-character"); } let kind = match chars.len() { 8 => "ean-8", 12 => "upc-a", 13 => "ean-13", 14 => "gtin-14", _ => return invalid("bad-length"), }; // Zero everywhere satisfies the arithmetic and has never been allocated. if chars.iter().all(|ch| *ch == '0') { return invalid("all-zero"); } // Weights run 3, 1, 3, 1 ... leftwards from the digit next to the check // digit. Counting from the right is what makes one rule serve all four // lengths: EAN-13's first digit gets 1, EAN-8's and UPC-A's get 3. let mut total: u32 = 0; let mut weight: u32 = 3; for ch in chars[..chars.len() - 1].iter().rev() { total += (*ch as u32 - 48) * weight; weight = if weight == 3 { 1 } else { 3 }; } let check = (10 - total % 10) % 10; if check != chars[chars.len() - 1] as u32 - 48 { return invalid("bad-check-digit"); } let digits: String = chars.iter().collect(); GtinCheck { valid: true, gtin14: Some(format!("{:0>14}", digits)), normalised: Some(digits), kind: Some(kind.to_string()), reason: None, } } /// Object keys are camelCase to match the shared vectors. pub fn gtin_check_to_value(result: &GtinCheck) -> 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)), ("kind", text(&result.kind)), ("gtin14", text(&result.gtin14)), ("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. gtin_check_to_value(&validate_gtin(args[0].as_str())) }