use crate::errors::ValidationError;
use crate::traits::ValueObject;
pub type Ean13Input = String;
pub type Ean13Output = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Ean13(String);
fn ean_checksum_valid(digits: &[u8], expected_len: usize) -> bool {
if digits.len() != expected_len {
return false;
}
let n = digits.len();
let sum: u32 = digits
.iter()
.enumerate()
.map(|(i, &d)| {
let weight = if (n - i) % 2 == 0 { 3u32 } else { 1u32 };
weight * d as u32
})
.sum();
sum % 10 == 0
}
impl ValueObject for Ean13 {
type Input = Ean13Input;
type Output = Ean13Output;
type Error = ValidationError;
fn new(value: Self::Input) -> Result<Self, Self::Error> {
let stripped: String = value.chars().filter(|c| c.is_ascii_digit()).collect();
if stripped.len() != 13 {
return Err(ValidationError::invalid("Ean13", value.trim()));
}
let digits: Vec<u8> = stripped.chars().map(|c| c as u8 - b'0').collect();
if !ean_checksum_valid(&digits, 13) {
return Err(ValidationError::invalid("Ean13", &stripped));
}
Ok(Self(stripped))
}
fn value(&self) -> &Self::Output {
&self.0
}
fn into_inner(self) -> Self::Input {
self.0
}
}
impl Ean13 {
pub fn check_digit(&self) -> u8 {
self.0.as_bytes().last().map(|b| b - b'0').unwrap_or(0)
}
}
impl TryFrom<&str> for Ean13 {
type Error = ValidationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value.to_owned())
}
}
impl std::fmt::Display for Ean13 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_valid_ean13() {
let e = Ean13::new("4006381333931".into()).unwrap();
assert_eq!(e.value(), "4006381333931");
}
#[test]
fn strips_spaces_and_hyphens() {
let e = Ean13::new("4006381-333931".into()).unwrap();
assert_eq!(e.value(), "4006381333931");
}
#[test]
fn check_digit_returns_last_digit() {
let e = Ean13::new("4006381333931".into()).unwrap();
assert_eq!(e.check_digit(), 1);
}
#[test]
fn rejects_wrong_length() {
assert!(Ean13::new("12345".into()).is_err());
}
#[test]
fn rejects_invalid_checksum() {
assert!(Ean13::new("4006381333930".into()).is_err());
}
#[test]
fn try_from_str() {
let e: Ean13 = "4006381333931".try_into().unwrap();
assert_eq!(e.value(), "4006381333931");
}
}