Skip to main content

gtin_validate/gtin8/
mod.rs

1//! Performs validation and correction of GTIN-8 codes.
2
3use utils;
4
5/// Errors that make GTIN-8 correction impossible.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum FixError {
8    /// The provided string contains non-ASCII characters.
9    NonAsciiString,
10    /// The provided code was too long to be valid.
11    TooLong,
12    /// The calculated check-digit did not match the code's check-digit.
13    CheckDigitIncorrect,
14}
15
16/// Check that a GTIN-8 code is valid by confirming that it is exactly
17/// 8 digits in length and that the check-digit is correct.
18///
19/// # Examples
20/// ```
21/// use gtin_validate::gtin8;
22///
23/// assert_eq!(gtin8::check("14567810"), true);  // Valid GTIN-8
24/// assert_eq!(gtin8::check("1456781"), false);  // too short
25/// assert_eq!(gtin8::check("14567811"), false); // Bad check digit
26/// ```
27pub fn check(code: &str) -> bool {
28    if code.len() != 8 {
29        return false;
30    }
31    if !utils::is_ascii_numeric(code) {
32        return false;
33    }
34
35    // Calculate and compare check digit
36    let bytes = code.as_bytes();
37    let check = utils::compute_check_digit(bytes);
38    if check != bytes[7] - 48 {
39        return false;
40    }
41
42    true
43}
44
45/// Attempt to fix an invalid GTIN-8 code by stripping whitespace from
46/// the left and right sides and zero-padding the code if it is less
47/// than 8 digits in length.
48///
49/// These corrections fix many common errors introduced by manual data
50/// entry and software that treats GTINs as integers rather than
51/// strings, thus truncating the leading zeros.
52///
53/// # Examples
54/// ```
55/// use gtin_validate::gtin8;
56///
57/// // Add missing zero, fixing length
58/// let result1 = gtin8::fix("5766796");
59/// assert!(result1.is_ok());
60/// assert_eq!(result1.unwrap(), "05766796");
61///
62/// // Remove extra whitespace
63/// let result2 = gtin8::fix("05766796 ");
64/// assert!(result2.is_ok());
65/// assert_eq!(result2.unwrap(), "05766796");
66/// ```
67///
68/// Here is how you catch errors:
69///
70/// ```
71/// # use gtin_validate::gtin8;
72/// match gtin8::fix("14567811") {
73///   Ok(fixed) => {println!("Fixed GTIN-14: {}", fixed);}
74///   Err(_) => {println!("Could not fix GTIN-14");}
75/// }
76/// ```
77pub fn fix(code: &str) -> Result<String, FixError> {
78    let mut fixed = code.trim().to_string();
79
80    if !fixed.is_ascii() {
81        return Err(FixError::NonAsciiString);
82    }
83    if fixed.len() > 8 {
84        return Err(FixError::TooLong);
85    }
86    fixed = utils::zero_pad(fixed, 8);
87    if !check(&fixed) {
88        return Err(FixError::CheckDigitIncorrect);
89    }
90
91    Ok(fixed)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::check;
97    use super::fix;
98    use super::FixError;
99
100    #[test]
101    fn check_valid() {
102        assert_eq!(check("00000000"), true);
103        assert_eq!(check("49137712"), true);
104        assert_eq!(check("44196318"), true);
105    }
106
107    #[test]
108    fn check_invalid_length() {
109        assert_eq!(check("0000000"), false); // too short
110        assert_eq!(check("734289412"), false); // too long
111    }
112
113    #[test]
114    fn check_non_ascii() {
115        assert_eq!(check("❤"), false);
116    }
117
118    #[test]
119    fn check_non_numeric() {
120        assert_eq!(check("a"), false);
121        assert_eq!(check("abcdabcd"), false); // length 8
122        assert_eq!(check("0000000a"), false); // invalid check digit
123    }
124
125    #[test]
126    fn check_invalid_check_digit() {
127        assert_eq!(check("00000001"), false);
128        assert_eq!(check("00000002"), false);
129        assert_eq!(check("00000003"), false);
130        assert_eq!(check("00000004"), false);
131        assert_eq!(check("00000005"), false);
132        assert_eq!(check("00000006"), false);
133        assert_eq!(check("00000007"), false);
134        assert_eq!(check("00000008"), false);
135        assert_eq!(check("00000009"), false);
136    }
137
138    #[test]
139    fn check_static_data() {
140        assert_eq!(check("14567810"), true); // Valid GTIN-8
141        assert_eq!(check("1456781"), false); // too short
142        assert_eq!(check("14567811"), false); // Bad check digit
143    }
144
145    #[test]
146    fn fix_non_ascii() {
147        assert!(fix("❤").is_err());
148    }
149
150    #[test]
151    fn fix_too_long() {
152        assert_eq!(fix("000000000"), Err(FixError::TooLong));
153    }
154
155    #[test]
156    fn fix_incorrect_check_digit() {
157        assert_eq!(fix("14567813"), Err(FixError::CheckDigitIncorrect));
158    }
159
160    #[test]
161    fn fix_needs_zero_padding() {
162        assert!(fix("0").is_ok());
163        assert_eq!(fix("0").unwrap(), "00000000");
164        assert_eq!(fix("9944220").unwrap(), "09944220");
165    }
166
167    proptest! {
168        #[test]
169        fn doesnt_crash(ref s in ".*") {
170            check(s);
171        }
172    }
173}