PRODUCT_EAN_LENGTH = 7
[docs]def bcd_digits(bcd_bytes):
"""Split a byte sequence into four-bit BCD digits
Used internally by `EAN` to decode BCD.
For example 0x12345 is turned into 1, 2, 3, 4, 5.
`bcd_bytes` must be an iterable of eight bit objects supporting `&` and
`>>`.
Note:
The byteorder is currently assumed to be 'little'.
"""
# byteorder is assumed to be 'little'
for char in bcd_bytes:
# Python 2 doesn't have bytes, only string
if isinstance(char, str):
char = ord(char)
yield char & 0xF
yield char >> 4
[docs]def bcd_int(bcd_bytes):
"""Calls `bcd_digits`, and then `int_from_digits` with the result
Used internally by `EAN` to decode BCD.
"""
return int_from_digits(bcd_digits(bcd_bytes))
[docs]def int_from_digits(digits):
"""Joins a sequence of decimal digits into a decimal number
Used internally by `EAN` to decode BCD.
For example (1, 2, 3, 4, 5) is turned into 12345.
Iterating through `digits` is assumed to only yield integers between 0 and
9, inclusive.
"""
decimal = 0
for pos, digit in enumerate(digits):
decimal += digit * 10**pos
return decimal
[docs]class IllegalEAN(ValueError):
"""Could not parse EAN"""
pass
[docs]class EAN(object):
"""Helper object for dealing with European Article Numbers
Depending on the format the ean is in, `EAN` objects are created in
different ways;
For strings::
EAN.from_string('01-2345-67890-1')
For BCD-coded bytes or bytearrays (str in python 2)::
EAN.from_bcd(b'\x00\x01 ... ')
For "hi-lo" format, i.e. two 32-bit integers containing half the ean each,
both BCD-coded::
EAN.from_hilo([eanHi, eanLo])
Sometimes it is easier to only use the last six digits of the ean, the
product code and check digit. This is supported when working with string
representations; `from_string` supports six-digit (seven-character) input::
EAN.from_string('67890-1')
In that cases, the country and manufacturer code is assumed to be that of
Kvaser AB (73-30130).
A string containing only the product code and check digit can also be retrieved::
ean.product()
Note:
The byteorder is currently always assumed to be 'little'.
"""
fmt = "##-#####-#####-#"
num_digits = len([s for s in fmt if s == '#'])
[docs] @classmethod
def from_bcd(cls, bcd_bytes):
"""Create an EAN object from a bytes-like object"""
return cls(bcd_int(bcd_bytes))
[docs] @classmethod
def from_string(cls, ean_string):
"""Create an EAN object from a specially formatted string"""
if len(ean_string) == PRODUCT_EAN_LENGTH:
ean_string = "73-30130-" + ean_string
if not all(
s.isdecimal() if (f == '#') else (f == s)
for f, s in zip(cls.fmt, ean_string)):
raise IllegalEAN("Unreconized format for EAN string")
decimal = int(''.join(s for f, s in zip(cls.fmt, ean_string) if f == '#'))
return cls(decimal)
[docs] @classmethod
def from_hilo(cls, hilo):
"""Create an EAN object from a pair of 32-bit integers, (eanHi, eanLo)"""
# Python 2 does not have int.to_bytes. And also has longs.
import sys
if sys.version_info < (3, 0):
hi, lo = hilo
conc = hi + (lo << 32)
digits = []
for i in xrange(16):
v = conc & 0xf
digits.append(int(v))
conc = conc >> 4
return cls(int_from_digits(digits))
high, low = hilo
high = tuple(bcd_digits(high.to_bytes(4, byteorder='little')))
low = tuple(bcd_digits(low.to_bytes(4, byteorder='little')))
ean = int_from_digits(high + low)
return cls(ean)
def __init__(self, decimal):
"""Create an EAN object from an integer with the natural number"""
self.decimal = decimal
if __debug__:
if len(str(decimal)) != self.num_digits:
raise IllegalEAN("Wrong length of EAN")
def __str__(self):
num_only = iter(str(self.decimal))
out = ''.join(
next(num_only) if s == '#' else s
for s in self.fmt)
if __debug__:
assert len(tuple(num_only)) == 0 # check that all digits where printed
return out
def __repr__(self):
# would rather have self.__class__.__qualname__, but that does not
# exist in python 2
return "<{cls}: {s}>".format(
cls=self.__class__.__name__, s=str(self))
[docs] def product(self):
"""Return only the product code and check digit of the string representation"""
return str(self)[PRODUCT_EAN_LENGTH:]