use crate::FiscalError;
pub fn is_valid_gtin(gtin: &str) -> Result<bool, FiscalError> {
if gtin.is_empty() || gtin == "SEM GTIN" {
return Ok(true);
}
if gtin.chars().any(|c| !c.is_ascii_digit()) {
return Err(FiscalError::InvalidGtin(format!(
"GTIN must contain only digits: \"{gtin}\" is not valid."
)));
}
let len = gtin.len();
if len != 8 && len != 12 && len != 13 && len != 14 {
return Err(FiscalError::InvalidGtin(format!(
"GTIN must be 8, 12, 13, or 14 digits. Got {len} digits."
)));
}
let expected_dv = calculate_check_digit(gtin)?;
let actual_dv = gtin.as_bytes()[len - 1] - b'0';
if actual_dv != expected_dv {
return Err(FiscalError::InvalidGtin(format!(
"GTIN \"{gtin}\" has an invalid check digit."
)));
}
Ok(true)
}
pub fn calculate_check_digit(gtin: &str) -> Result<u8, FiscalError> {
let len = gtin.len();
if len < 2 {
return Err(FiscalError::InvalidGtin(
"GTIN must have at least 2 digits".to_string(),
));
}
let without_check = >in[..len - 1];
let padded = format!("{:0>15}", without_check);
let mut total: u32 = 0;
for (pos, ch) in padded.bytes().enumerate() {
let val = (ch - b'0') as u32;
let multiplier = ((pos + 1) % 2) * 2 + 1;
total += multiplier as u32 * val;
}
let dv = (10 - (total % 10)) % 10;
Ok(dv as u8)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calculate_check_digit_too_short() {
let err = calculate_check_digit("1").unwrap_err();
assert!(matches!(err, FiscalError::InvalidGtin(_)));
}
#[test]
fn calculate_check_digit_single_digit() {
let err = calculate_check_digit("0").unwrap_err();
assert!(matches!(err, FiscalError::InvalidGtin(_)));
}
#[test]
fn calculate_check_digit_valid_gtin13() {
let dv = calculate_check_digit("7891000315507").unwrap();
assert_eq!(dv, 7);
}
}