Functional Weave
Code in Rust

validation.gtin@1.0.0

impl/python.py

1,991 bytes · the Python implementation · view raw

from typing import List

from .validation_gtin_types import GtinCheck

#: The four GTIN lengths, and what each is called.
_KINDS = {8: "ean-8", 12: "upc-a", 13: "ean-13", 14: "gtin-14"}


def _invalid(reason: str) -> GtinCheck:
    return GtinCheck(valid=False, normalised=None, kind=None, gtin14=None, reason=reason)


def validate_gtin(value: str) -> GtinCheck:
    """Check an EAN-8, UPC-A, EAN-13 or GTIN-14 against the GS1 mod-10 check digit.

    A pass means the number was typed correctly, not that GS1 issued it.
    Validators answer rather than raise: a malformed value is the answer "no".
    """
    if not isinstance(value, str):
        return _invalid("empty")

    # Barcodes are printed in groups, so spaces and hyphens are ignored anywhere.
    chars: List[str] = [ch for ch in value if ch != " " and ch != "-"]
    if not chars:
        return _invalid("empty")
    # Compare against ASCII explicitly: str.isdigit() accepts Arabic-Indic and
    # superscript digits, which the other two languages would reject.
    if any(ch < "0" or ch > "9" for ch in chars):
        return _invalid("bad-character")
    digits = "".join(chars)
    kind = _KINDS.get(len(digits))
    if kind is None:
        return _invalid("bad-length")
    # Zero everywhere satisfies the arithmetic and has never been allocated.
    if all(ch == "0" for ch in digits):
        return _invalid("all-zero")

    # Weights run 3, 1, 3, 1 ... leftwards from the digit next to the check
    # digit. Counting from the right is what makes one rule serve all four
    # lengths: EAN-13's first digit gets 1, EAN-8's and UPC-A's get 3.
    total = 0
    weight = 3
    for ch in reversed(digits[:-1]):
        total += (ord(ch) - 48) * weight
        weight = 1 if weight == 3 else 3
    check = (10 - total % 10) % 10
    if check != ord(digits[-1]) - 48:
        return _invalid("bad-check-digit")

    return GtinCheck(valid=True, normalised=digits, kind=kind, gtin14=digits.rjust(14, "0"), reason=None)