Skip to main content

gtin_validate/gtin13/
mod.rs

1//! Performs validation and correction of GTIN-13 and EAN-13 codes.
2
3use utils;
4
5/// Errors that make GTIN-13 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-13 code is valid by checking the length (should be
17/// exactly 13 digits) and that the check-digit is correct.
18///
19/// # Examples
20/// ```
21/// use gtin_validate::gtin13;
22///
23/// assert_eq!(gtin13::check("1498279802125"), true);  // Valid GTIN-13
24/// assert_eq!(gtin13::check("468712378699"), false);  // Too short
25/// assert_eq!(gtin13::check("1498279802124"), false); // Bad check digit
26/// ```
27pub fn check(code: &str) -> bool {
28    if code.len() != 13 {
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[12] - 48 {
39        return false;
40    }
41
42    true
43}
44
45/// Attempt to fix an invalid GTIN-13 code by stripping whitespace from
46/// the let and right sides and zero-padding the code if it is less than
47/// 13 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 strings,
51/// thus truncating the leading zeros.
52///
53/// # Examples
54/// ```
55/// use gtin_validate::gtin13;
56///
57/// // Add missing zero, fixing length
58/// let result1 = gtin13::fix("495205944325");
59/// assert!(result1.is_ok());
60/// assert_eq!(result1.unwrap(), "0495205944325");
61///
62/// // Remove extra whitespace
63/// let result2 = gtin13::fix("4823011492925 ");
64/// assert!(result2.is_ok());
65/// assert_eq!(result2.unwrap(), "4823011492925");
66/// ```
67///
68/// Here is how you catch errors:
69///
70/// ```
71/// # use gtin_validate::gtin13;
72/// match gtin13::fix("04567432178913") {
73///   Ok(fixed) => {println!("{} is OK!", fixed);}
74///   Err(_) => {println!("UPC is invalid");}
75/// }
76/// ```
77
78pub fn fix(code: &str) -> Result<String, FixError> {
79    let mut fixed = code.trim().to_string();
80
81    if !fixed.is_ascii() {
82        return Err(FixError::NonAsciiString);
83    }
84    if fixed.len() > 13 {
85        return Err(FixError::TooLong);
86    }
87    fixed = utils::zero_pad(fixed, 13);
88    if !check(&fixed) {
89        return Err(FixError::CheckDigitIncorrect);
90    }
91
92    Ok(fixed)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::check;
98    use super::fix;
99    use super::FixError;
100
101    #[test]
102    fn check_valid() {
103        assert_eq!(check("0000000000000"), true);
104        assert_eq!(check("8845791354268"), true);
105        assert_eq!(check("0334873614126"), true);
106    }
107
108    #[test]
109    fn check_invalid_length() {
110        assert_eq!(check("000"), false);
111        assert_eq!(check("00000000000000"), false);
112    }
113
114    #[test]
115    fn check_non_ascii() {
116        assert_eq!(check("❤"), false);
117    }
118
119    #[test]
120    fn check_non_numeric() {
121        assert_eq!(check("a"), false);
122        assert_eq!(check("abcdabcdabcda"), false); // length 13
123        assert_eq!(check("000000000000a"), false); // invalid check digit
124    }
125
126    #[test]
127    fn check_invalid_check_digit() {
128        assert_eq!(check("0000000000001"), false);
129        assert_eq!(check("0000000000002"), false);
130        assert_eq!(check("0000000000003"), false);
131        assert_eq!(check("0000000000004"), false);
132        assert_eq!(check("0000000000005"), false);
133        assert_eq!(check("0000000000006"), false);
134        assert_eq!(check("0000000000007"), false);
135        assert_eq!(check("0000000000008"), false);
136        assert_eq!(check("0000000000009"), false);
137    }
138
139    #[test]
140    fn check_static_data() {
141        assert_eq!(check("0000000000000"), true);
142        assert_eq!(check("0123456789012"), true);
143        assert_eq!(check("0123456789013"), false);
144        assert_eq!(check("0999999999993"), true);
145        assert_eq!(check("0999999999999"), false);
146        assert_eq!(check("4459121265748"), true);
147        assert_eq!(check("4459121265747"), false);
148    }
149
150    #[test]
151    fn fix_non_ascii() {
152        assert!(fix("❤").is_err());
153    }
154
155
156    #[test]
157    fn fix_too_long() {
158        assert_eq!(fix("00000000000000"), Err(FixError::TooLong));
159    }
160
161    #[test]
162    fn fix_incorrect_check_digit() {
163        assert_eq!(fix("8845791354262"), Err(FixError::CheckDigitIncorrect));
164    }
165
166    #[test]
167    fn fix_needs_zero_padding() {
168        assert!(fix("0").is_ok());
169        assert_eq!(fix("0").unwrap(), "0000000000000");
170        assert_eq!(fix("123012301238").unwrap(), "0123012301238");
171    }
172
173    proptest! {
174        #[test]
175        fn doesnt_crash(ref s in ".*") {
176            check(s);
177        }
178    }
179}