upc_a/lib.rs
1//! # upc-a
2//!
3//! A Rust library for parsing, validating, and working with [UPC-A (Universal Product Code)](https://www.gs1us.org/upcs-barcodes-prefixes/guide-to-upcs).
4//!
5//! ## What is UPC-A?
6//!
7//! UPC-A is a 12-digit number used to identify a product. Each UPC-A number consists of 11 digits
8//! plus a check digit. The check digit is calculated using a specific algorithm based on the first
9//! 11 digits.
10//!
11//! ## Features
12//!
13//! - Memory-efficient representation (8 bytes)
14//! - Format-aware serialization/deserialization with [serde]
15//! - Binary serialization support via [bitcode]
16//! - Comprehensive error handling for invalid input
17//! - No-std compatible, zero heap allocation
18//!
19//! [serde]: https://docs.rs/serde
20//! [bitcode]: https://docs.rs/bitcode
21//!
22//! ## Usage
23//!
24//! ```rust
25//! use upc_a::UpcA;
26//! use std::str::FromStr;
27//!
28//! // From numeric UPC-A code
29//! let upc_a = UpcA::from_code(123456789012)?;
30//! # assert_eq!(upc_a.to_code(), 123456789012);
31//!
32//! // From string representation using FromStr trait
33//! let upc_a = UpcA::from_str("123456789012")?;
34//! # assert_eq!(upc_a.to_code(), 123456789012);
35//!
36//! // From a compact binary format
37//! let upc_a = UpcA::from_bytes(b"\x14\x1A\x99\xBE\x1C")?;
38//!
39//! // Get the UPC-A code as a numeric value
40//! assert_eq!(upc_a.to_code(), 123456789012);
41//!
42//! // Display a UPC-A code
43//! assert_eq!(upc_a.to_string(), "123456789012");
44//!
45//! // Convert to a compact binary format
46//! assert_eq!(upc_a.to_bytes(), *b"\x14\x1A\x99\xBE\x1C");
47//! # anyhow::Ok::<()>(())
48//! ```
49//!
50//! ### Serde integration
51//!
52//! ```rust
53//! # #[cfg(feature = "serde")]
54//! # {
55//! use upc_a::UpcA;
56//! use serde::{Deserialize, Serialize};
57//!
58//! // Define a struct with a UPC-A code
59//! #[derive(Serialize, Deserialize)]
60//! struct Product {
61//! name: String,
62//! upc_a: UpcA,
63//! }
64//!
65//! // For human-readable formats like JSON and TOML, ISRCs are serialized as strings
66//! let json = r#"{"name":"Meme cat plush","upc_a":123456789012}"#;
67//! let product: Product = serde_json::from_str(json)?;
68//! # assert_eq!(product.upc_a.to_code(), 123456789012);
69//! # }
70//! # anyhow::Ok::<()>(())
71//! ```
72
73#![no_std]
74#![deny(missing_docs)]
75
76use core::fmt::{self, Display, Formatter};
77use core::num::ParseIntError;
78use core::str::FromStr;
79
80#[cfg(feature = "alloc")]
81extern crate alloc;
82
83#[cfg(feature = "bitcode")]
84use bitcode::{Decode, Encode};
85#[cfg(feature = "serde")]
86use serde::{Deserialize, Serialize};
87
88use thiserror::Error;
89
90/// Universal Product Code version A (UPC-A)
91///
92/// A UPC-A code uniquely identifies a product using a 12-digit number. The last digit is a check
93/// digit.
94///
95/// # Examples
96///
97/// ```
98/// use upc_a::UpcA;
99///
100/// // Parse an ISRC from a string
101/// let upc_a = UpcA::from_code(123456789012)?;
102///
103/// // Retrieve the numeric value
104/// assert_eq!(upc_a.to_code(), 123456789012);
105///
106/// // Display a formatted ISRC
107/// assert_eq!(upc_a.to_string(), "123456789012");
108/// # anyhow::Ok::<()>(())
109/// ```
110///
111/// ###### References
112/// - <https://en.wikipedia.org/wiki/Universal_Product_Code>
113/// - <https://www.gtin.info/upc/>
114#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
115#[cfg_attr(feature = "bitcode", derive(Encode, Decode))]
116pub struct UpcA(
117 /// 12-digit number. The last digit is a error detecting check digit, but interestingly,
118 /// whether we include the last digit or not, 5 bytes are still required, so we include the
119 /// last digit.
120 ///
121 /// - Without error detecting check digit: 37 bits = 5 bytes (2**36 < 10**11 < 2**37)
122 /// - With error detecting check digit: 40 bits = 5 bytes (2**39 < 10**12 < 2**40)
123 u64,
124);
125
126#[test]
127fn test_upc_a_size() {
128 assert_eq!(size_of::<UpcA>(), 8);
129}
130
131/// Error that can occur during parsing a UPC-A code.
132///
133/// This enum represents all the possible errors that can occur when validating or parsing
134/// a UPC-A from various input formats.
135#[derive(Clone, Eq, PartialEq, Debug, Error)]
136pub enum UpcAParseError {
137 /// The input is too large to be a valid UPC-A code.
138 #[error("Input is too large (expected 0 <= input <= 999_999_999_999, found {found})")]
139 InputTooLarge {
140 /// The (invalid) input value that was too large.
141 found: u64,
142 },
143
144 /// The input string is not a valid integer.
145 #[error(transparent)]
146 ParseIntError(#[from] ParseIntError),
147
148 /// The checksum digit is invalid.
149 #[error("Checksum failed (expected 0, found {found})")]
150 ChecksumFailed {
151 /// The (invalid) checksum digit that was found.
152 found: u8,
153 },
154}
155
156impl UpcA {
157 /// Creates an [`UpcA`] from a numeric code.
158 ///
159 /// The input must be a 12-digit number, and the last digit must be a valid checksum digit.
160 ///
161 /// # Examples
162 ///
163 /// ```
164 /// use upc_a::UpcA;
165 ///
166 /// // Valid UPC-A
167 /// let upc_a = UpcA::from_code(123456789012)?;
168 /// assert_eq!(upc_a.to_string(), "123456789012");
169 ///
170 /// // Invalid UPC-A (incorrect checksum)
171 /// assert!(UpcA::from_code(123456789010).is_err());
172 /// # anyhow::Ok::<()>(())
173 /// ```
174 ///
175 /// # Errors
176 ///
177 /// Returns an `UpcAParseError` if:
178 /// - The integer value exceeds the maximum allowed value (999,999,999,999)
179 /// - The checksum digit is invalid
180 pub const fn from_code(n: u64) -> Result<Self, UpcAParseError> {
181 if n > 999_999_999_999 {
182 return Err(UpcAParseError::InputTooLarge { found: n });
183 }
184
185 // Checksum
186 let mut a = n;
187 let mut odd_sum = 0;
188 let mut even_sum = 0;
189 even_sum += a % 10;
190 a /= 10;
191 odd_sum += a % 10;
192 a /= 10;
193 even_sum += a % 10;
194 a /= 10;
195 odd_sum += a % 10;
196 a /= 10;
197 even_sum += a % 10;
198 a /= 10;
199 odd_sum += a % 10;
200 a /= 10;
201 even_sum += a % 10;
202 a /= 10;
203 odd_sum += a % 10;
204 a /= 10;
205 even_sum += a % 10;
206 a /= 10;
207 odd_sum += a % 10;
208 a /= 10;
209 even_sum += a % 10;
210 a /= 10;
211 odd_sum += a % 10;
212
213 let checksum = ((odd_sum * 3 + even_sum) % 10) as u8;
214 if checksum != 0 {
215 return Err(UpcAParseError::ChecksumFailed { found: checksum });
216 }
217
218 Ok(Self(n))
219 }
220
221 /// Returns the decimal integer value of a [`UpcA`].
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// use upc_a::UpcA;
227 /// use std::str::FromStr;
228 ///
229 /// let upc_a = UpcA::from_str("123456789012")?;
230 /// assert_eq!(upc_a.to_code(), 123456789012);
231 /// # anyhow::Ok::<()>(())
232 /// ```
233 pub const fn to_code(self) -> u64 {
234 self.0
235 }
236
237 /// Creates a [`UpcA`] from a 5-byte binary representation.
238 ///
239 /// This method deserializes a UPC-A code from its compact binary representation, which is
240 /// primarily useful for binary serialization formats.
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// use upc_a::UpcA;
246 ///
247 /// let bytes = [0x14, 0x1A, 0x99, 0xBE, 0x1C];
248 /// let upc_a = UpcA::from_bytes(&bytes)?;
249 /// assert_eq!(upc_a.to_code(), 123456789012);
250 /// # anyhow::Ok::<()>(())
251 /// ```
252 ///
253 /// # Errors
254 ///
255 /// Returns an `UpcAParseError` if:
256 /// - The integer value exceeds the maximum allowed value (999,999,999,999)
257 /// - The checksum digit is invalid
258 pub const fn from_bytes(bytes: &[u8; 5]) -> Result<Self, UpcAParseError> {
259 let ret = u64::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], 0, 0, 0]);
260 Self::from_code(ret)
261 }
262
263 /// Converts the [`UpcA`] to its compact 5-byte binary representation.
264 ///
265 /// This method serializes a UPC-A code into a fixed-size array suitable for binary storage or
266 /// transmission. It is the inverse of `from_bytes`.
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// use upc_a::UpcA;
272 /// use std::str::FromStr;
273 ///
274 /// let upc_a = UpcA::from_code(123456789012)?;
275 /// let bytes = upc_a.to_bytes();
276 /// assert_eq!(bytes, *b"\x14\x1A\x99\xBE\x1C");
277 ///
278 /// // Round-trip conversion
279 /// let round_trip = UpcA::from_bytes(&bytes)?;
280 /// assert_eq!(round_trip, upc_a);
281 /// # anyhow::Ok::<()>(())
282 /// ```
283 pub const fn to_bytes(self) -> [u8; 5] {
284 [
285 self.0 as u8,
286 (self.0 >> 8) as u8,
287 (self.0 >> 16) as u8,
288 (self.0 >> 24) as u8,
289 (self.0 >> 32) as u8,
290 ]
291 }
292}
293
294/// Implements the [`FromStr`] trait for [`UpcA`] to allow parsing from strings using the `parse`
295/// method.
296///
297/// This implementation delegates to [`UpcA::from_code`].
298///
299/// # Examples
300///
301/// ```
302/// use upc_a::UpcA;
303/// use std::str::FromStr;
304///
305/// // Parse using FromStr
306/// let upc_a = UpcA::from_str("123456789012")?;
307/// # assert_eq!(upc_a.to_code(), 123456789012);
308///
309/// // Or using the more idiomatic parse method
310/// let upc_a: UpcA = "123456789012".parse()?;
311/// # assert_eq!(upc_a.to_code(), 123456789012);
312/// # anyhow::Ok::<()>(())
313/// ```
314impl FromStr for UpcA {
315 type Err = UpcAParseError;
316
317 fn from_str(s: &str) -> Result<Self, UpcAParseError> {
318 Self::from_code(s.parse()?)
319 }
320}
321
322/// Implements the [`Display`] trait for [`UpcA`] to provide a string representation.
323///
324/// # Examples
325///
326/// ```
327/// use upc_a::UpcA;
328///
329/// let upc_a = UpcA::from_code(123456789012)?;
330/// assert_eq!(upc_a.to_string(), "123456789012");
331/// # anyhow::Ok::<()>(())
332/// ```
333impl Display for UpcA {
334 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
335 write!(f, "{:012}", self.0)
336 }
337}
338
339#[test]
340#[allow(
341 clippy::zero_prefixed_literal,
342 reason = "A UPC is a 12-digit decimal number that can start with a zero."
343)]
344fn test_upc_a() -> Result<(), UpcAParseError> {
345 #[cfg(feature = "alloc")]
346 use alloc::string::ToString;
347
348 let upc = UpcA::from_code(123456789012)?;
349 assert_eq!(upc.0, 123456789012);
350 #[cfg(feature = "alloc")]
351 assert_eq!(upc.to_string(), "123456789012");
352
353 let upc: UpcA = UpcA::from_code(036000291452)?;
354 assert_eq!(upc.0, 036000291452);
355 #[cfg(feature = "alloc")]
356 assert_eq!(upc.to_string(), "036000291452");
357
358 assert_eq!(
359 UpcA::from_code(1_000_000_000_000),
360 Err(UpcAParseError::InputTooLarge {
361 found: 1_000_000_000_000
362 })
363 );
364
365 assert_eq!(
366 UpcA::from_code(999_999_999_999),
367 Err(UpcAParseError::ChecksumFailed { found: 6 })
368 );
369
370 assert_eq!(
371 "036000291453".parse::<UpcA>(),
372 Err(UpcAParseError::ChecksumFailed { found: 1 })
373 );
374
375 Ok(())
376}
377
378/// Implements the [`Serialize`] trait for [`UpcA`] to support serialization with serde.
379///
380/// This implementation provides format-aware serialization:
381/// - For human-readable formats (like JSON, TOML): Uses a numeric representation ([`UpcA::to_code`])
382/// - For binary formats (like bincode): Uses the binary representation ([`UpcA::to_bytes`])
383///
384/// # Examples
385///
386/// ```
387/// use upc_a::UpcA;
388/// use std::str::FromStr;
389///
390/// let upc_a = UpcA::from_code(123456789012)?;
391///
392/// // JSON serialization (human-readable)
393/// let json = serde_json::to_string(&upc_a)?;
394/// assert_eq!(json, r#"123456789012"#);
395///
396/// // Bincode serialization (binary)
397/// let binary = bincode::serialize(&upc_a)?;
398/// # anyhow::Ok::<()>(())
399/// ```
400#[cfg(feature = "serde")]
401impl Serialize for UpcA {
402 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
403 where
404 S: serde::Serializer,
405 {
406 if serializer.is_human_readable() {
407 self.0.serialize(serializer)
408 } else {
409 self.to_bytes().serialize(serializer)
410 }
411 }
412}
413
414#[test]
415#[cfg(feature = "serde")]
416fn test_upc_a_serialize() -> anyhow::Result<()> {
417 let upc = UpcA::from_code(123456789012)?;
418
419 // JSON (human readable)
420 assert_eq!(serde_json::to_string(&upc)?, r#"123456789012"#);
421 // TOML (human readable)
422 #[cfg(feature = "alloc")]
423 {
424 use alloc::collections::BTreeMap;
425 let table: BTreeMap<&str, UpcA> = BTreeMap::from_iter([("upc", upc)]);
426 assert_eq!(
427 toml::to_string(&table)?,
428 r#"upc = 123456789012
429"#
430 );
431 }
432 // Bincode (binary)
433 assert_eq!(
434 bincode::serialize(&upc)?,
435 // little endian representation of 0x1cbe991a14
436 b"\x14\x1A\x99\xBE\x1C"
437 );
438
439 Ok(())
440}
441
442/// Implements the [`Deserialize`] trait for [`UpcA`] to support deserialization with serde.
443///
444/// This implementation provides format-aware deserialization:
445/// - For human-readable formats (like JSON, TOML): Expects a number and uses [`UpcA::from_code`]
446/// - For binary formats (like bincode): Expects an 5-byte array and uses [`UpcA::from_bytes`]
447///
448/// # Examples
449///
450/// ```
451/// use upc_a::UpcA;
452/// use serde::Deserialize;
453///
454/// // JSON deserialization (human-readable)
455/// #[derive(Deserialize)]
456/// struct Product {
457/// code: UpcA,
458/// }
459///
460/// let product: Product = serde_json::from_str(r#"{"code":123456789012}"#)?;
461/// # assert_eq!(product.code.to_code(), 123456789012);
462/// # anyhow::Ok::<()>(())
463/// ```
464#[cfg(feature = "serde")]
465impl<'de> Deserialize<'de> for UpcA {
466 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
467 where
468 D: serde::Deserializer<'de>,
469 {
470 if deserializer.is_human_readable() {
471 UpcA::from_code(u64::deserialize(deserializer)?)
472 } else {
473 UpcA::from_bytes(&<[u8; 5]>::deserialize(deserializer)?)
474 }
475 .map_err(serde::de::Error::custom)
476 }
477}
478
479#[test]
480#[cfg(feature = "serde")]
481fn test_upc_a_deserialize() -> anyhow::Result<()> {
482 #[derive(Deserialize)]
483 struct TestInput {
484 upc: UpcA,
485 }
486
487 // JSON (human readable)
488 let v: TestInput = serde_json::from_str(r#"{"upc": 123456789012}"#)?;
489 assert_eq!(v.upc.0, 123456789012);
490 // TOML (human readable)
491 let v: TestInput = toml::from_str(r#"upc = 123456789012"#)?;
492 assert_eq!(v.upc.0, 123456789012);
493 // Bincode (binary)
494 let v: UpcA = bincode::deserialize(b"\x14\x1A\x99\xBE\x1C")?;
495 assert_eq!(v.0, 123456789012);
496
497 Ok(())
498}