Spaces:
Sleeping
Sleeping
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 ""
|