1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Performs validation and correction of GTIN-14 codes.
use std::ascii::AsciiExt;
use utils;


/// Errors that make GTIN-14 correction impossible.
#[derive(Debug)]
pub enum FixError {
    /// The provided string contains non-ASCII characters.
    NonAsciiString,
    /// The provided code was too long to be valid.
    TooLong,
    /// The calculated check-digit did not match the code's check-digit.
    CheckDigitIncorrect
}

/// Check that a GTIN-14 code is valid by confirming that it is exactly
/// 14 digits in length and that the check-digit is correct.
///
/// # Examples
/// ```
/// use gtin_validate::gtin14;
///
/// assert_eq!(gtin14::check("14567815983469"), true);  // Valid GTIN-14
/// assert_eq!(gtin14::check("1456781598346"), false);  // too short
/// assert_eq!(gtin14::check("14567815983468"), false); // Bad check digit
/// ```
pub fn check(code: &str) -> bool {
    if code.is_ascii() == false {
        return false;
    }
    if code.len() != 14 {
        return false;
    }

    let bytes = code.as_bytes();
    if !utils::is_number(bytes, 14) {
        return false;
    }

    // Calculate and compare check digit
    let check = utils::compute_check_digit(bytes, 14);
    if check != bytes[13] - 48 {
        return false;
    }

    return true;
}

/// Attempt to fix an invalid GTIN-14 code by stripping whitespace from
/// the left and right sides and zero-padding the code if it is less
/// than 14 digits in length.
///
/// These corrections fix many common errors introduced by manual data
/// entry and software that treats GTINs as integers rather than
/// strings, thus truncating the leading zeros.
///
/// # Examples
/// ```
/// use gtin_validate::gtin14;
///
/// // Add missing zero, fixing length
/// let result1 = gtin14::fix("04527819983417");
/// assert!(result1.is_ok());
/// assert_eq!(result1.unwrap(), "04527819983417");
///
/// // Remove extra whitespace
/// let result2 = gtin14::fix("04527819983417 ");
/// assert!(result2.is_ok());
/// assert_eq!(result2.unwrap(), "04527819983417");
/// ```
///
/// Here is how you catch errors:
///
/// ```
/// # use gtin_validate::gtin14;
/// match gtin14::fix("14507829283411") {
///   Ok(fixed) => {println!("Fixed GTIN-14: {}", fixed);}
///   Err(_) => {println!("Could not fix GTIN-14");}
/// }
/// ```
pub fn fix(code: &str) -> Result<String, FixError> {
    let mut fixed = code.trim_left().trim_right().to_string();

    if !fixed.is_ascii() {
        return Err(FixError::NonAsciiString);
    }
    if fixed.len() > 14 {
        return Err(FixError::TooLong);
    }
    fixed = utils::zero_pad(fixed, 14);
    if !check(&fixed) {
        return Err(FixError::CheckDigitIncorrect);
    }

    return Ok(fixed);
}

#[cfg(test)]
mod tests {
    use super::check;
    use super::fix;

    #[test]
    fn check_valid() {
        assert_eq!(check("00000000000000"), true);
        assert_eq!(check("17342894127884"), true);
        assert_eq!(check("44889977112244"), true);
    }

    #[test]
    fn check_invalid_length() {
        assert_eq!(check("0000000000000"), false);
        assert_eq!(check("1734289412788"), false);
    }

    #[test]
    fn fix_non_ascii() {
        assert!(fix("❤").is_err());
    }

    #[test]
    fn fix_needs_zero_padding() {
        assert!(fix("0").is_ok());
        assert_eq!(fix("0").unwrap(), "00000000000000");
        assert_eq!(fix("8987561651112").unwrap(), "08987561651112");
    }
}