File size: 4,035 Bytes
b0414d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""Validation helpers for barcodes, SKU values and normalized numeric fields.

The challenge scoring is very sensitive to false positives: a wrong barcode or
SKU is worse than an empty value.  This module centralizes the strict validators
used by OCR, QR parsing, multi-frame voting and catalog lookup.
"""

from __future__ import annotations

import re
from typing import Any

SKU_RE = re.compile(r"^2\d{11}$")


def normalize_digits(raw: Any) -> str:
    """Return only digits from ``raw`` while handling pandas float strings."""
    if raw is None:
        return ""
    try:
        if raw != raw:  # NaN
            return ""
    except Exception:
        pass
    text = str(raw).strip().replace("\u00a0", " ")
    if not text or text.lower() in {"nan", "none", "нет"}:
        return ""
    if re.fullmatch(r"\d+\.0", text):
        text = text[:-2]
    return re.sub(r"\D", "", text)


def ean13_checksum_valid(code: str) -> bool:
    """Return True only for a valid 13-digit EAN-13 code."""
    if len(code) != 13 or not code.isdigit():
        return False
    digits = [int(c) for c in code]
    total = sum(d * (3 if idx % 2 else 1) for idx, d in enumerate(digits[:12]))
    return (10 - total % 10) % 10 == digits[12]


def ean13_check_digit(prefix12: str) -> str:
    """Compute the EAN-13 check digit for a 12-digit prefix."""
    if len(prefix12) != 12 or not prefix12.isdigit():
        raise ValueError("EAN-13 prefix must contain exactly 12 digits")
    digits = [int(c) for c in prefix12]
    total = sum(d * (3 if idx % 2 else 1) for idx, d in enumerate(digits))
    return str((10 - total % 10) % 10)


def repair_ean13(
    raw: Any,
    *,
    allow_append_12: bool = True,
    allow_drop_14: bool = True,
    allow_one_digit_repair: bool = False,
) -> str:
    """Conservatively repair a trusted barcode candidate into valid EAN-13.

    OCR text is not a trusted source for 12-digit values because Lenta SKU values
    have exactly the same length.  Callers should set ``allow_append_12`` only for
    QR/pyzbar/barcode-ROI sources, never for arbitrary OCR text.
    """
    digits = normalize_digits(raw)
    if ean13_checksum_valid(digits):
        return digits
    if len(digits) == 12 and allow_append_12:
        candidate = digits + ean13_check_digit(digits)
        return candidate if ean13_checksum_valid(candidate) else ""
    if len(digits) == 14 and allow_drop_14:
        for candidate in (digits[:-1], digits[1:]):
            if ean13_checksum_valid(candidate):
                return candidate
    if len(digits) == 13 and allow_one_digit_repair:
        for idx, current in enumerate(digits):
            for repl in "0123456789":
                if repl == current:
                    continue
                candidate = digits[:idx] + repl + digits[idx + 1 :]
                if ean13_checksum_valid(candidate):
                    return candidate
    return ""


def normalize_ean13(
    raw: Any,
    *,
    allow_repair: bool = False,
    allow_append_12: bool = False,
    allow_drop_14: bool = True,
    allow_one_digit_repair: bool = False,
) -> str:
    """Normalize and validate an EAN-13 candidate.

    By default this function is strict: invalid 13-digit values and 12-digit SKU
    lookalikes are rejected.  Repair is opt-in and should be enabled only for
    trusted scanner/ROI sources.
    """
    digits = normalize_digits(raw)
    if ean13_checksum_valid(digits):
        return digits
    if not allow_repair:
        return ""
    return repair_ean13(
        digits,
        allow_append_12=allow_append_12,
        allow_drop_14=allow_drop_14,
        allow_one_digit_repair=allow_one_digit_repair,
    )


def is_valid_sku(raw: Any) -> bool:
    """Return True for Lenta SKU values: exactly 12 digits starting with 2."""
    return bool(SKU_RE.fullmatch(normalize_digits(raw)))


def normalize_sku(raw: Any) -> str:
    """Return a strict SKU string or an empty value."""
    digits = normalize_digits(raw)
    return digits if SKU_RE.fullmatch(digits) else ""