barcodes 0.2.0

Universal Bar/QR codes library
Documentation
//! UPC-E barcode encoder.
//!
//! UPC-E is a compressed form of UPC-A that suppresses zeros to fit small
//! packages.  It encodes 6 data digits with an implied number system digit
//! (0 or 1) and check digit.
//!
//! # Structure
//!
//! ```text
//! [quiet] start-guard(101)  6 data digits (L/G mix)  end-guard(010101)  [quiet]
//! ```
//!
//! The parity pattern of the 6 encoded digits is determined by the check digit.
#![forbid(unsafe_code)]

use super::ean13::{GUARD_NORMAL, L_CODE, check_digit};
use crate::common::{
    buffer::SliceWriter, errors::EncodeError, traits::BarcodeEncoder, types::Encoded,
};

// ---- Encoding tables -------------------------------------------------------

/// G-code (even parity) patterns for UPC-E — same as EAN-13 G-code.
const G_CODE: [[bool; 7]; 10] = [
    [false, true, false, false, true, true, true],   // 0
    [false, true, true, false, false, true, true],   // 1
    [false, false, true, true, false, true, true],   // 2
    [false, true, false, false, false, false, true], // 3
    [false, false, true, true, true, false, true],   // 4
    [false, true, true, true, false, false, true],   // 5
    [false, false, false, false, true, false, true], // 6
    [false, false, true, false, false, false, true], // 7
    [false, false, false, true, false, false, true], // 8
    [false, false, true, false, true, true, true],   // 9
];

/// UPC-E end guard: 010101
const GUARD_END: [bool; 6] = [false, true, false, true, false, true];

/// Parity pattern for UPC-E indexed by check digit (0–9).
/// `false` = L-code, `true` = G-code for positions 0..6.
const UPCE_PARITY: [[bool; 6]; 10] = [
    [true, true, true, false, false, false], // 0  EEEOOO
    [true, true, false, true, false, false], // 1  EEOEOO
    [true, true, false, false, true, false], // 2  EEOOEO
    [true, true, false, false, false, true], // 3  EEOOOE
    [true, false, true, true, false, false], // 4  EOEEOO
    [true, false, false, true, true, false], // 5  EOOEEO
    [true, false, false, false, true, true], // 6  EOOOEE
    [true, false, true, false, true, false], // 7  EOEOEO
    [true, false, true, false, false, true], // 8  EOEOOE
    [true, false, false, true, false, true], // 9  EOOEOE
];

// ---- Public encoder --------------------------------------------------------

/// UPC-E barcode encoder.
///
/// Accepts 6-digit (data only), 7-digit (with check digit), or 8-digit input
/// (with number system 0/1 prefix and check digit).
///
/// The number system digit must be 0 or 1 when provided.
///
/// # Example
///
/// ```rust
/// use barcodes::common::traits::BarcodeEncoder;
/// use barcodes::common::types::Encoded;
/// use barcodes::ean_upc::upce::UpcE;
///
/// let mut buf = [false; 128];
/// // 8 digits: number system + 6 data + check digit
/// let Encoded::Linear { len, .. } = UpcE::encode_into("01234505", &mut buf).unwrap()
/// else { unreachable!() };
/// assert_eq!(len, 51);
/// ```
pub struct UpcE;

impl BarcodeEncoder for UpcE {
    type Input = str;

    fn encode_into(input: &str, buf: &mut [bool]) -> Result<Encoded, EncodeError> {
        let (number_system, six_digits, check) = parse_and_validate(input)?;
        let len = encode_bars(number_system, &six_digits, check, buf)?;
        Ok(Encoded::Linear { len, height: 69 })
    }

    fn symbology_name() -> &'static str {
        "UPC-E"
    }
}

// ---- Helpers ---------------------------------------------------------------

/// Parse and validate input, returning (number_system, [6 digits], check digit).
fn parse_and_validate(input: &str) -> Result<(u8, [u8; 6], u8), EncodeError> {
    let trimmed = input.trim();
    if !trimmed.chars().all(|c| c.is_ascii_digit()) {
        return Err(EncodeError::InvalidInput(
            "UPC-E input must contain digits only",
        ));
    }

    match trimmed.len() {
        6 => {
            // Just the 6 data digits; assume number system 0
            let mut six = [0u8; 6];
            for (i, c) in trimmed.chars().enumerate() {
                six[i] = c as u8 - b'0';
            }
            let upca = expand_to_upca(0, &six);
            let check = check_digit(&upca[..11]);
            Ok((0, six, check))
        }
        7 => {
            // 6 data digits + check digit
            let mut buf = [0u8; 7];
            for (i, c) in trimmed.chars().enumerate() {
                buf[i] = c as u8 - b'0';
            }
            let six = [buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]];
            let provided_check = buf[6];
            let upca = expand_to_upca(0, &six);
            let expected = check_digit(&upca[..11]);
            if provided_check != expected {
                return Err(EncodeError::InvalidInput("UPC-E check digit mismatch"));
            }
            Ok((0, six, expected))
        }
        8 => {
            // Number system + 6 data digits + check digit
            let mut buf = [0u8; 8];
            for (i, c) in trimmed.chars().enumerate() {
                buf[i] = c as u8 - b'0';
            }
            let ns = buf[0];
            if ns > 1 {
                return Err(EncodeError::InvalidInput(
                    "UPC-E number system must be 0 or 1",
                ));
            }
            let six = [buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]];
            let provided_check = buf[7];
            let upca = expand_to_upca(ns, &six);
            let expected = check_digit(&upca[..11]);
            if provided_check != expected {
                return Err(EncodeError::InvalidInput("UPC-E check digit mismatch"));
            }
            Ok((ns, six, expected))
        }
        _ => Err(EncodeError::InvalidInput(
            "UPC-E input must be 6, 7, or 8 digits",
        )),
    }
}

/// Expand a UPC-E number to the equivalent 12-digit UPC-A number.
///
/// The expansion rules depend on the last digit of the 6-digit compressed form:
/// - 0, 1, 2: manufacturer = d[0..2] + last digit + "0000", product = d[2..5]
/// - 3: manufacturer = d[0..3] + "00000", product = d[3..5]  
/// - 4: manufacturer = d[0..4] + "00000", product = d[4..5] (padded)
/// - 5-9: manufacturer = d[0..5] + "00000", product = "0000" + d[5]
pub fn expand_to_upca(number_system: u8, six: &[u8; 6]) -> [u8; 12] {
    let mut upca = [0u8; 12];
    upca[0] = number_system;

    match six[5] {
        0..=2 => {
            // Manufacturer: d[0], d[1], six[5], 0, 0
            upca[1] = six[0];
            upca[2] = six[1];
            upca[3] = six[5];
            upca[4] = 0;
            upca[5] = 0;
            // Product: 0, 0, d[2], d[3], d[4]
            upca[6] = 0;
            upca[7] = 0;
            upca[8] = six[2];
            upca[9] = six[3];
            upca[10] = six[4];
        }
        3 => {
            // Manufacturer: d[0], d[1], d[2], 0, 0
            upca[1] = six[0];
            upca[2] = six[1];
            upca[3] = six[2];
            upca[4] = 0;
            upca[5] = 0;
            // Product: 0, 0, 0, d[3], d[4]
            upca[6] = 0;
            upca[7] = 0;
            upca[8] = 0;
            upca[9] = six[3];
            upca[10] = six[4];
        }
        4 => {
            // Manufacturer: d[0], d[1], d[2], d[3], 0
            upca[1] = six[0];
            upca[2] = six[1];
            upca[3] = six[2];
            upca[4] = six[3];
            upca[5] = 0;
            // Product: 0, 0, 0, 0, d[4]
            upca[6] = 0;
            upca[7] = 0;
            upca[8] = 0;
            upca[9] = 0;
            upca[10] = six[4];
        }
        d => {
            // 5-9: Manufacturer: d[0], d[1], d[2], d[3], d[4]
            upca[1] = six[0];
            upca[2] = six[1];
            upca[3] = six[2];
            upca[4] = six[3];
            upca[5] = six[4];
            // Product: 0, 0, 0, 0, d[5]
            upca[6] = 0;
            upca[7] = 0;
            upca[8] = 0;
            upca[9] = 0;
            upca[10] = d;
        }
    }

    upca
}

fn encode_bars(
    number_system: u8,
    six: &[u8; 6],
    check: u8,
    buf: &mut [bool],
) -> Result<usize, EncodeError> {
    // Number system 1 uses inverted parity (all G becomes L and vice versa)
    let parity = UPCE_PARITY[check as usize];

    let mut w = SliceWriter::new(buf);

    // Start guard: 101
    w.extend(GUARD_NORMAL.iter().copied())?;

    // 6 data digits using L/G based on parity and number system
    for (pos, &d) in six.iter().enumerate() {
        // For number system 1, invert the parity
        let use_g = if number_system == 0 {
            parity[pos]
        } else {
            !parity[pos]
        };
        let pattern = if use_g {
            &G_CODE[d as usize]
        } else {
            &L_CODE[d as usize]
        };
        w.extend(pattern.iter().copied())?;
    }

    // End guard: 010101
    w.extend(GUARD_END.iter().copied())?;

    Ok(w.len())
}

// ---- Tests -----------------------------------------------------------------

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

    fn encode_len(input: &str) -> usize {
        let mut buf = [false; 128];
        match UpcE::encode_into(input, &mut buf).unwrap() {
            Encoded::Linear { len, .. } => len,
            _ => panic!("expected linear"),
        }
    }

    #[test]
    fn test_encode_8_digits() {
        // Start(3) + 6×7=42 + End(6) = 51
        assert_eq!(encode_len("01234505"), 51);
    }

    #[test]
    fn test_encode_6_digits() {
        assert!(encode_len("123450") > 0);
    }

    #[test]
    fn test_expand_to_upca_last0() {
        // Last digit 0: manufacturer = d[0]d[1]d[5]00, product = 00d[2]d[3]d[4]
        let six = [1u8, 2, 3, 4, 5, 0];
        let upca = expand_to_upca(0, &six);
        assert_eq!(upca[0], 0); // number system
        assert_eq!(upca[1], 1);
        assert_eq!(upca[2], 2);
        assert_eq!(upca[3], 0); // six[5]
        assert_eq!(upca[4], 0);
        assert_eq!(upca[5], 0);
        assert_eq!(upca[6], 0);
        assert_eq!(upca[7], 0);
        assert_eq!(upca[8], 3);
        assert_eq!(upca[9], 4);
        assert_eq!(upca[10], 5);
    }

    #[test]
    fn test_expand_to_upca_last3() {
        let six = [1u8, 2, 3, 4, 5, 3];
        let upca = expand_to_upca(0, &six);
        assert_eq!(upca[1], 1);
        assert_eq!(upca[2], 2);
        assert_eq!(upca[3], 3);
        assert_eq!(upca[4], 0);
        assert_eq!(upca[5], 0);
        assert_eq!(upca[9], 4);
        assert_eq!(upca[10], 5);
    }

    #[test]
    fn test_invalid_number_system() {
        let mut buf = [false; 128];
        assert!(UpcE::encode_into("21234505", &mut buf).is_err());
    }

    #[test]
    fn test_invalid_characters() {
        let mut buf = [false; 128];
        assert!(UpcE::encode_into("0123450X", &mut buf).is_err());
    }

    #[test]
    fn test_wrong_length() {
        let mut buf = [false; 128];
        assert!(UpcE::encode_into("12345", &mut buf).is_err());
    }

    #[test]
    fn test_symbology_name() {
        assert_eq!(UpcE::symbology_name(), "UPC-E");
    }

    #[cfg(feature = "alloc")]
    #[test]
    fn test_svg_output() {
        let svg = UpcE::encode("01234505").unwrap().to_svg_string();
        assert!(svg.starts_with("<svg "));
    }
}