def _parse_upc_e(cls, value: str) -> Upc: length = len(value) assert length in (6, 7, 8) if length == 6: # Implicit number system 0, no check digit. number_system_digit = 0 payload = f"{number_system_digit}{value}" upc_a_payload = _upc_e_to_upc_a_expansion(f"{payload}0")[:-1] check_digit = numeric_check_digit(upc_a_payload) elif length == 7: # Explicit number system, no check digit. number_system_digit = int(value[0]) payload = value upc_a_payload = _upc_e_to_upc_a_expansion(f"{payload}0")[:-1] check_digit = numeric_check_digit(upc_a_payload) elif length == 8: # Explicit number system and check digit. number_system_digit = int(value[0]) payload = value[:-1] check_digit = int(value[-1]) else: raise Exception( # pragma: no cover "Unhandled UPC-E length. This is a bug." ) # Control that the number system digit is correct. if number_system_digit not in (0, 1): raise ParseError( f"Invalid UPC-E number system for {value!r}: " f"Expected 0 or 1, got {number_system_digit!r}." ) # Control that check digit is correct. upc_a_payload = _upc_e_to_upc_a_expansion(f"{payload}{check_digit}")[:-1] calculated_check_digit = numeric_check_digit(upc_a_payload) if check_digit != calculated_check_digit: raise ParseError( f"Invalid UPC-E check digit for {value!r}: " f"Expected {calculated_check_digit!r}, got {check_digit!r}." ) return cls( value=value, format=UpcFormat.UPC_E, payload=payload, number_system_digit=number_system_digit, check_digit=check_digit, )
def without_variable_measure(self: Rcn) -> Gtin: """Create a new RCN where the variable measure is zeroed out. This provides us with a number which still includes the item reference, but does not vary with weight/price, and can thus be used to lookup the relevant trade item in a database or similar. This has no effect on RCNs intended for use within a company, as the semantics of those numbers vary from company to company. Returns: A new RCN instance with zeros in the variable measure places. Raises: EncodeError: If the rules for variable measures in the region are unknown. """ if self.usage == RcnUsage.COMPANY: return self if self.region in ( RcnRegion.BALTICS, RcnRegion.NORWAY, RcnRegion.SWEDEN, ): measure = "0000" payload = f"{self.value[:-5]}{measure}" check_digit = checksums.numeric_check_digit(payload) value = f"{payload}{check_digit}" return Gtin.parse(value, rcn_region=self.region) elif self.region in (RcnRegion.GREAT_BRITAIN, ): measure = "0000" price_check_digit = checksums.price_check_digit(measure) payload = f"{self.value[:-6]}{price_check_digit}{measure}" check_digit = checksums.numeric_check_digit(payload) value = f"{payload}{check_digit}" return Gtin.parse(value, rcn_region=self.region) else: raise EncodeError( f"Cannot zero out the variable measure part of {self.value!r} as the " f"RCN rules for the geographical region {self.region!r} are unknown." )
def parse(cls: Type[Sscc], value: str) -> Sscc: """Parse the given value into a :class:`Sscc` object. Args: value: The value to parse. Returns: SSCC data structure with the successfully extracted data. The checksum is guaranteed to be valid if an SSCC object is returned. Raises: ParseError: If the parsing fails. """ value = value.strip() if len(value) != 18: raise ParseError(f"Failed to parse {value!r} as SSCC: " f"Expected 18 digits, got {len(value)}.") if not value.isnumeric(): raise ParseError( f"Failed to parse {value!r} as SSCC: Expected a numerical value." ) value_without_extension_digit = value[1:] prefix = GS1Prefix.extract(value_without_extension_digit) extension_digit = int(value[0]) payload = value[:-1] check_digit = int(value[-1]) calculated_check_digit = numeric_check_digit(payload) if check_digit != calculated_check_digit: raise ParseError( f"Invalid SSCC check digit for {value!r}: " f"Expected {calculated_check_digit!r}, got {check_digit!r}.") return cls( value=value, prefix=prefix, extension_digit=extension_digit, payload=payload, check_digit=check_digit, )
def without_variable_measure(self, rcn: Rcn) -> Gtin: # Zero out the variable measure part of the payload, and recalculate both # the GTIN check digit and the variable measure's check digit digit, if any. rcn_13 = rcn.as_gtin_13() zeroed_value = "0" * len(rcn_13[self.value_slice]) digits = list(self.pattern) digits[self.prefix_slice] = list(rcn_13[self.prefix_slice]) digits[self.value_slice] = list(zeroed_value) if self.check_digit_slice is not None: digits[self.check_digit_slice] = [ str(checksums.price_check_digit(zeroed_value)) ] gtin_payload = "".join(digits) gtin_check_digit = checksums.numeric_check_digit(gtin_payload) gtin = f"{gtin_payload}{gtin_check_digit}" return Gtin.parse(gtin, rcn_region=rcn.region)
def _parse_upc_a(cls, value: str) -> Upc: assert len(value) == 12 payload = value[:-1] number_system_digit = int(value[0]) check_digit = int(value[-1]) calculated_check_digit = numeric_check_digit(payload) if check_digit != calculated_check_digit: raise ParseError( f"Invalid UPC-A check digit for {value!r}: " f"Expected {calculated_check_digit!r}, got {check_digit!r}." ) return cls( value=value, format=UpcFormat.UPC_A, payload=payload, number_system_digit=number_system_digit, check_digit=check_digit, )
def test_numeric_check_digit_with_nonnumeric_value(value: str) -> None: with pytest.raises(ValueError) as exc_info: numeric_check_digit(value) assert str(exc_info.value) == f"Expected numeric value, got {value!r}."
def test_numeric_check_digit(value: str, expected: int) -> None: assert numeric_check_digit(value) == expected
def parse( cls: Type[Gtin], value: str, *, rcn_region: Optional[RcnRegion] = None ) -> Gtin: """Parse the given value into a :class:`Gtin` object. Both GTIN-8, GTIN-12, GTIN-13, and GTIN-14 are supported. Args: value: The value to parse. rcn_region: The geographical region whose rules should be used to interpret Restricted Circulation Numbers (RCN). Needed to extract e.g. variable weight/price from GTIN. Returns: GTIN data structure with the successfully extracted data. The checksum is guaranteed to be valid if a GTIN object is returned. Raises: ParseError: If the parsing fails. """ from biip.gtin import Rcn value = value.strip() if len(value) not in (8, 12, 13, 14): raise ParseError( f"Failed to parse {value!r} as GTIN: " f"Expected 8, 12, 13, or 14 digits, got {len(value)}." ) if not value.isnumeric(): raise ParseError( f"Failed to parse {value!r} as GTIN: Expected a numerical value." ) stripped_value = _strip_leading_zeros(value) assert len(stripped_value) in (8, 12, 13, 14) num_significant_digits = len(stripped_value) gtin_format = GtinFormat(num_significant_digits) payload = stripped_value[:-1] check_digit = int(stripped_value[-1]) packaging_level: Optional[int] = None if gtin_format == GtinFormat.GTIN_14: packaging_level = int(stripped_value[0]) value_without_packaging_level = stripped_value[1:] prefix = GS1Prefix.extract(value_without_packaging_level) elif gtin_format == GtinFormat.GTIN_12: # Add a zero to convert U.P.C. Company Prefix to GS1 Company Prefix prefix = GS1Prefix.extract(stripped_value.zfill(13)) elif gtin_format == GtinFormat.GTIN_8: prefix = GS1Prefix.extract(stripped_value.zfill(12)) else: prefix = GS1Prefix.extract(stripped_value) calculated_check_digit = numeric_check_digit(payload) if check_digit != calculated_check_digit: raise ParseError( f"Invalid GTIN check digit for {value!r}: " f"Expected {calculated_check_digit!r}, got {check_digit!r}." ) gtin_type: Type[Union[Gtin, Rcn]] if "Restricted Circulation Number" in prefix.usage: gtin_type = Rcn else: gtin_type = Gtin gtin = gtin_type( value=value, format=gtin_format, prefix=prefix, payload=payload, check_digit=check_digit, packaging_level=packaging_level, ) if isinstance(gtin, Rcn) and rcn_region is not None: gtin._parse_with_regional_rules(rcn_region) return gtin