Source code for serialnumbers.sn

'''
:date: Created on 12 mai 2022
:author: Voillat F.
'''
from .common import LASTNUM_RE, RANGE_SEP_CHAR, EXCLUSION_CHAR, SIZE_CHAR, RANGE_CHAR


[docs]class SerialNumber(object): '''Class representing a serial number. Args: sn_or_prefix (str): Serial number or serial number prefix. If *number* and *suffix* are :code:`None`, then this parameter is interpreted as a string describing a serial number. number (int): The number (default = None) suffix (str): Serial number suffix (default = None) padSize (int): Minimum digit group size (default = 1) padChar (str): Filler character for digit group (default = '0') A serial number consists of 3 parts: - The prefix: all the characters located before the last group of digits (number) - The number: the last group of digits, this part can be incremented - The suffix: all the characters located after the last group of digits (number) ====== ====== ====== ====== String Prefix Number Suffix ====== ====== ====== ====== A001X A 1 X 001X None 1 X A001 A 1 None A12X01 A12X 1 None ====== ====== ====== ====== '''
[docs] @classmethod def explode(cls, s): '''Decomposes the string of characters into three constituent parts of a serial number. Args: s (str): String describing the serial number. Returns: tuple: A 3-tuple of containing the prefix, number, and suffix. The 3 elements of a serial number are: - The *prefix*: all the characters located before the last group of digits - The *number*: the last group of digits - The *suffix*: all the characters located after the last group of digits See also: :py:data:`.LASTNUM_RE` See also: The grammar rule for :a4:r:`serial number <serialnumbers.sn>`. >>> SerialNumber.explode('abc123-0045Z') ('abc123-', 45, 'Z', 4) >>> SerialNumber.explode('0045') (None, 45, None, 4) >>> SerialNumber.explode('abc123-0045') ('abc123-', 45, None, 4) >>> SerialNumber.explode('0045Z') (None, 45, 'Z', 4) ''' m = LASTNUM_RE.search(s) try: return (m[1], int(m[2]), m[3], len(m[2])) except: raise 'The string does not correspond to a serial number.'
[docs] @classmethod def fromString(cls, s): '''Creates a `SerialNumber` object from a string. Args: s (str): The serial number as a character string. Returns: SerialNumber: The created serial number. See also the grammar rule for :a4:r:`serial number <serialnumbers.sn>`. >>> str(SerialNumber.fromString('abc123-0045Z')) 'abc123-0045Z' ''' return SerialNumber(*SerialNumber.explode(s))
[docs] @classmethod def listFromString(cls, s, inc=1, sep=RANGE_SEP_CHAR): '''Reads a character string containing the definition of one or more serial number ranges. Args: s (str): the character string containing the definition of the serial number ranges inc (int): The increment to compute numbers, default = 1 sep (str): Serial number ranges separator character (default = :data:`.RANGE_SEP_CHAR`) See also the grammar rules for :a4:r:`lists <serialnumbers.list>` and for :a4:r:`exclusion ranges <serialnumbers.range>` >>> SerialNumber.listFromString('X01:10;X50-X59;-X02:2') [SerialNumber('X01'), SerialNumber('X04'), ..., SerialNumber('X10'), SerialNumber('X50'), ..., SerialNumber('X59')] ''' ret = set() if s is None or s == '': return [] for s0 in s.split(sep): s0 = s0.strip() if s0[0] == EXCLUSION_CHAR: ret -= set(SerialNumberRange.fromString(s0[1:], inc=inc)) else: ret |= set(SerialNumberRange.fromString(s0, inc=inc)) ret = list(ret) ret.sort() return ret
[docs] @classmethod def compare(cls, a, b): '''Compare two serial numbers :param SerialNumber a: The reference serial number :param SerialNumber b: The serial number to compare :return: -1, if `a` < `b` ; 0, if `a` == `b`, 1 otherwise >>> SerialNumber.compare(SerialNumber('X02'), SerialNumber('X01')) 1 ''' return a.cmp(b)
def __init__(self, sn_or_prefix, number=None, suffix=None, padSize=1, padChar='0'): '''Constructor''' self.padChar = padChar if number is None and suffix is None: self.prefix, self.number, self.suffix, self.padSize = self.explode(sn_or_prefix) else: self.prefix = sn_or_prefix self.number = number self.suffix = suffix self.padSize = padSize def __repr__(self): '''Represents the serial number as a string. >>> SerialNumber('abc123-0045Z') SerialNumber('abc123-0045Z') :return: the serial number as a string. ''' try: return '{:s}(\'{:s}\')'.format(self.__class__.__name__, self.toString()) except: return super().__repr__() def __str__(self): '''Converts the serial number as a string. :return: the serial number as a string. ''' try: return self.toString() except: return super().__str__() def __hash__(self): return hash(str(self)) def __lt__(self, other): '''Checks if itself is lower than the other serial number. :param SerialNumber other: the other serial number. :return: `True`, if itself is less than the other serial number ; `False`, otherwise >>> SerialNumber('abc123-0045Z') < SerialNumber('abc123-', 46, 'Z', 4) True ''' assert isinstance(other, SerialNumber), 'The other object isn\'t a SerialNumber!' if self.inSameRange(other): return self.number < other.number else: return self.toString() < other.toString() def __eq__(self, other): '''Checks if itself is equal to the other serial number. :param SerialNumber other: the other serial number. :return: `True`, if itself is equal to the other serial number >>> SerialNumber('abc123-0045Z') == SerialNumber('abc123-', 45, 'Z', 4) True ''' return self.inSameRange(other) and self.number == other.number
[docs] def inSameRange(self, other): '''Checks if itself is in the same range as the other serial number. Two serial numbers are considered to be from the same range if their prefixes and suffixes are the same. :param SerialNumber other: the other serial number. :return: `True`, if itself is in the same range as the other serial number. >>> SerialNumber('abc123-0045Z').inSameRange(SerialNumber('abc123-', 102, 'Z', 4)) True >>> SerialNumber('abc123-0045X').inSameRange(SerialNumber('abc123-', 102, 'Z', 4)) False ''' assert isinstance(other, SerialNumber), 'The other object isn\'t a SerialNumber!' return self.prefix == other.prefix and self.suffix == other.suffix
[docs] def isNext(self, other, inc=1): '''Checks if the other serial number is the next. :param SerialNumber other: the other serial number. :return: `True`, if the other serial number is the next. >>> SerialNumber('abc123-0044Z').isNext(SerialNumber('abc123-', 45, 'Z', 4)) True ''' return self.inSameRange(other) and self.number+inc == other.number
[docs] def toString(self): '''Returns the serial number as a string. :return: the serial number as a string. >>> SerialNumber(None, number=123, suffix='A', padSize=5, padChar='#').toString() '##123A' ''' n = str(self.number) return (self.prefix or '') + self.padChar*(self.padSize-len(n)) + n + (self.suffix or '')
[docs] def next(self, inc=1): '''Returns the next serial number :param int inc: Increment to compute the next serial number :return: the next serial number according increment. :rtype: SerialNumber >>> SerialNumber('abc123-0044Z').next(2) SerialNumber('abc123-0046Z') ''' if inc == -1: return self else: return SerialNumber(self.prefix, self.number+inc, self.suffix, self.padSize, self.padChar);
[docs] def prev(self, inc=1): '''Returns the previous serial number :param int inc: Increment to compute the next serial number :return SerialNumber: the previous serial number according increment. >>> SerialNumber('x0044Z').prev(2) SerialNumber('x0042Z') ''' if self.number==0: raise Exception('No previous serial number!'); return SerialNumber(self.prefix, self.number-inc, self.suffix, self.padSize, self.padChar);
[docs] def countTo(self, other, inc=1): '''Counts the number of SNs in the interval with another SN Both serial numbers must be in the same range (see :py:meth:`~.SerialNumber.inSameRange`). :param SerialNumber other: The other serial number :param int inc: Increment :return: The number of SNs in the interval with the other SN. >>> SerialNumber('x001Z').countTo(SerialNumber('x020Z')) 20 >>> SerialNumber('x001Z').countTo(SerialNumber('y020Z')) Traceback (most recent call last): ... ValueError: The other serial number (y020Z) is not in the same range (x001Z)! ''' assert isinstance(other, SerialNumber), 'The other object isn\'t a SerialNumber!' if not self.inSameRange(other): raise ValueError('The other serial number ({0!s}) is not in the same range ({1!s})!'.format(other, self)) return (1 + other.number - self.number) // inc
[docs] def genCount(self, count, inc=1): '''Returns a generator for a list of `count` serial numbers. >>> print(list(SerialNumber('x001Z').genCount(10))) [SerialNumber('x001Z'), SerialNumber('x002Z'), ..., SerialNumber('x010Z')] ''' current = self for _ in range(int(count)): yield current current = current.next(inc)
[docs] def genEnd(self, end, inc=1): '''Returns a generator for a list of serial numbers according the `end`. >>> print(list(SerialNumber('x001Z').genEnd(SerialNumber('x020Z')))) [SerialNumber('x001Z'), SerialNumber('x002Z'), ..., SerialNumber('x020Z')] ''' current = self yield current while current < end: current = current.next(inc); yield current
[docs] def range(self, a, b=None, c=1): '''Creates a range of serial numbers :param int a : If `b` isn't defined the stop index of the range, then the star index :param int b : If defined, the stop index of the range :param int c : Step (increment) to compute serial number >>> list(SerialNumber('x010Z').range(10)) [SerialNumber('x010Z'), SerialNumber('x011Z'), ..., SerialNumber('x019Z')] >>> list(SerialNumber('x010Z').range(2,5)) [SerialNumber('x012Z'), SerialNumber('x013Z'), ..., SerialNumber('x015Z')] ''' if b is None: return SerialNumberRange(self, a, None, c) else: return SerialNumberRange(self.next(a), None, self.next(b), c)
[docs] def cmp(self, other): '''Compare two serial numbers :param SerialNumber other: the other serial number :return: -1, if `other` is smaller ; 0, if both are equal ; 1 otherwise. >>> SerialNumber('x001Z').cmp(SerialNumber('x001Z')) 0 >>> SerialNumber('x001Z').cmp(SerialNumber('x002Z')) -1 >>> SerialNumber('x001Z').cmp(SerialNumber('a001Z')) 1 ''' if self == other: return 0 elif self < other: return -1 return 1
[docs]class SerialNumberRange(object): '''Class representing a range of serial numbers A serial number range consists of a sequence of consecutive serial numbers Args: first (SerialNumber): The first serial number of range count (int): If defined, the size of range last (SerialNumber): If defined, the last serial number of range inc (int): The increment to compute numbers, default = 1 ''' EXCLUDE = False
[docs] @classmethod def fromStartEnd(cls, first, last, inc=1): return SerialNumberRange(first, None, last, inc)
[docs] @classmethod def fromStartCount(cls, first, count, inc=1): return SerialNumberRange(first, count, None, inc)
[docs] @classmethod def fromString(cls, s, inc=1): '''Creates a range of serial numbers from a string. Args: s (str): The string describing the serial number range inc (int): The increment to compute numbers, default = 1 Returns: SerialNumberRange, SerialNumberXRange: The created serial numbers range or exclusion range A serial number range is defined by giving the starting and ending serial numbers separated by a hyphen (-), or by giving the starting serial number and the number of numbers separated by a colon (:). If the range definition string starts with a hyphen (-), then it is an exclusion range and will result in an object of class :class:`SerialNumberXRange`. See also the grammar rule for :a4:r:`lists <serialnumbers.range>` >>> list(SerialNumberRange.fromString('X001:10')) [SerialNumber('X001'), SerialNumber('X002'), ..., SerialNumber('X010')] >>> list(SerialNumberRange.fromString('X001-X010')) [SerialNumber('X001'), SerialNumber('X002'), ..., SerialNumber('X010')] >>> list(SerialNumberRange.fromString('X001')) [SerialNumber('X001')] >>> SerialNumberRange.fromString('-X05:2') SerialNumberXRange('-X05-X06') ''' if s[0] == EXCLUSION_CHAR: cls = SerialNumberXRange s = s[1:] if SIZE_CHAR in s: start, count = s.split(SIZE_CHAR,2) return cls(SerialNumber(start), int(count), inc=inc) elif RANGE_CHAR in s: start, end = s.split(RANGE_CHAR,2) return cls(SerialNumber(start), None, SerialNumber(end), inc=inc) else: return cls(SerialNumber(s), 1, inc=inc)
def __init__(self, first, count=None, last=None, inc=1): assert isinstance(first, SerialNumber), 'The `first` argument must be a SerialNumber ({!s})!'.format(type(first).__name__) self._first = first self._inc = inc if count is None: assert isinstance(last, SerialNumber), 'If `count` is None, then the `last` argument must be a SerialNumber!' self._last = last self._count = self._first.countTo(self._last, self._inc) elif count == 1: self._count = 1 self._last = self._first else: self._count = count self._last = self._first.next((self._count-1)*self._inc) def __str__(self): '''Converts the serial number range as a string. Returns str: The serial number range as a string. >>> str(SerialNumberRange(SerialNumber('X0123'), 5)) 'X0123-X0127' ''' try: return self.toString() except: return super().__str__() def __repr__(self): '''Represents the serial number range as a string. Returns str: The serial number as a string. >>> SerialNumberRange(SerialNumber('X001'), None, SerialNumber('X010')) SerialNumberRange('X001-X010') ''' try: return '{:s}(\'{:s}\')'.format(self.__class__.__name__, self.toString()) except: return super().__repr__() def __len__(self): return self._count def __getitem__(self, key): ''' >>> SerialNumberRange(SerialNumber('X001'), 10)[5] SerialNumber('X005') >>> SerialNumberRange(SerialNumber('X001'), 10)[2:5] [SerialNumber('X003'), SerialNumber('X004'), SerialNumber('X005')] ''' if isinstance(key, int): if key < 0 or key > self._count: raise IndexError return self._first.next((key-1)*self._inc) else: return list(self)[key] def __iter__(self): ''' >>> list(SerialNumberRange(SerialNumber('X001'), 10)) [SerialNumber('X001'), ..., SerialNumber('X010')] ''' current = self._first for _ in range(self._count): yield current current = current.next(self._inc) def __contains__(self, sn_or_range): '''Determines whether an SN or a range of SNs is contained in a range of SNs. Args: sn_or_range (SerialNumber,SerialNumberRange): Returns: bool: True, if `sn_or_range` is in `self` ; False, otherwise >>> SerialNumber('X05') in SerialNumberRange.fromString('X01:10') True >>> SerialNumber('X12') in SerialNumberRange.fromString('X01:10') False >>> SerialNumber('Y05') in SerialNumberRange.fromString('X01:10') False >>> SerialNumber('X04') in SerialNumberRange.fromString('X00:10', inc=2) True >>> SerialNumber('X05') in SerialNumberRange.fromString('X00:10', inc=2) False >>> SerialNumberRange.fromString('X01:5') in SerialNumberRange.fromString('X01:10') True >>> SerialNumberRange.fromString('X07:5') in SerialNumberRange.fromString('X01:10') False >>> SerialNumberRange.fromString('X01:5') in SerialNumberRange.fromString('X01:10', inc=2) False ''' if isinstance(sn_or_range, SerialNumber): return sn_or_range.inSameRange(self.first) \ and self.first.number <= sn_or_range.number <= self.last.number \ and (sn_or_range.number - self.first.number) % self._inc == 0 elif isinstance(sn_or_range, SerialNumberRange): return sn_or_range.first.inSameRange(self.first) \ and sn_or_range.inc == self._inc \ and self.first.number <= sn_or_range.first.number \ and sn_or_range.last.number <= self.last.number \ and (sn_or_range.first.number - self.first.number) % self._inc == 0 else: raise ValueError('The `sn_or_range` argument must be a SerialNumber or a SerialNumberRange!')
[docs] def toStringFirstLast(self): '''Represents the serial number range in the format: A-B, where A and B are, respectively, the first and last serial number in the range Returns: str : The Representation of the serial number range in the format: A-B >>> SerialNumberRange(SerialNumber('X001'), 10).toStringFirstLast() 'X001-X010' ''' if self.count == 1: return self.first.toString() else: return self.first.toString() + RANGE_CHAR + self.last.toString()
[docs] def toStringFirstCount(self): '''Represents the serial number range in the format: A:C, where A and C are, respectively, the first serial number and the number of serial numbers in the range Returns: str : The Representation of the serial number range in the format: A:C >>> SerialNumberRange(SerialNumber('X001'), 10).toStringFirstCount() 'X001:10' ''' if self.count == 1: return self.first.toString() else: return self.first.toString() + SIZE_CHAR + str(self.count)
[docs] def toString(self): return self.toStringFirstLast()
@property def first(self): return self._first @property def last(self): return self._last @property def count(self): return self._count @property def inc(self): return self._inc
[docs]class SerialNumberXRange(SerialNumberRange): EXCLUDE = True
[docs] def toStringFirstLast(self): ''' >>> str(SerialNumberXRange.fromString('X01:3')) '-X01-X03' ''' return EXCLUSION_CHAR + SerialNumberRange.toStringFirstLast(self)
[docs] def toStringFirstCount(self): return EXCLUSION_CHAR + SerialNumberRange.toStringFirstCount(self)
[docs]class SerialNumberList(object): '''Class for a list of serial number ranges. Args: snlist (str or list): Characters string containing the definition of one or more serial number ranges or list of :class:`SerialNumber` or :class:`SerialNumberRange`. inc (int): The increment to compute numbers, default = 1 sep (str): Serial number ranges separator character (default = :data:`.RANGE_SEP_CHAR`) .. note:: If `snlist` is a list, then all elements must be of the same class, either :class:`SerialNumber` or :class:`SerialNumberRange`. >>> list(SerialNumberList('X01:10;X50-X59;-X02:2').ranges) [SerialNumberRange('X01'), SerialNumberRange('X04-X10'), SerialNumberRange('X50-X59')] '''
[docs] @classmethod def snListFromString(cls, s, inc=1, sep=RANGE_SEP_CHAR): '''Reads a string containing the definition of one or more serial number ranges and convert it into a list of SNs. Args: s (str): Characters string containing the definition of one or more serial number ranges. inc (int): The increment to compute numbers, default = 1 sep (str): Serial number ranges separator character (default = :data:`.RANGE_SEP_CHAR`) Returns: SerialNumberList: The created list of serial numbers ranges ''' temp = set([]) if not(s is None or s == ''): for s0 in s.split(self._sep): s0 = s0.strip() if s0[0] == EXCLUSION_CHAR: temp -= set(SerialNumberRange.fromString(s0[1:], inc=inc)) else: temp |= set(SerialNumberRange.fromString(s0, inc=inc)) temp = list(temp) temp.sort() return temp
[docs] @classmethod def snRangesFromList(cls, snlist, inc=1, nosort=False): '''Convert a list of SN to a list of SN ranges Args: snlist (list,tuple): The list of serial numbers to convert inc (int): The increment to compute numbers, default = 1 Returns: list: The list of SerialNumberRange >>> list(SerialNumberList.snRangesFromList(SerialNumber.listFromString('X01:10;X50-X59;-X02:2'))) [SerialNumberRange('X01'), SerialNumberRange('X04-X10'), SerialNumberRange('X50-X59')] ''' l = len(snlist) if not nosort: snlist.sort() if l == 0: return [] elif l == 1: return [SerialNumberRange(snlist[0], count=1, inc=inc)] ret = [] sn0 = sn1 = snlist[0] for sn2 in snlist[1:]: if not sn1.isNext(sn2, inc=inc): ret.append(SerialNumberRange(sn0, last=sn1, inc=inc)) sn0 = sn2 sn1 = sn2 ret.append( SerialNumberRange(sn0, last=sn1, inc=inc)) return ret
def __init__(self, snlist=None, inc=1, sep=RANGE_SEP_CHAR): '''Constructor''' self._sep = sep self._inc = inc self._ranges = [] self._numbers = None if isinstance(snlist, str) and snlist != '': self._ranges = [ SerialNumberRange.fromString(r.strip(), inc=inc) for r in snlist.split(self._sep) ] elif isinstance(snlist, (list,tuple)) and len(snlist)>0: if isinstance(snlist[0], SerialNumber): self._ranges = self.snRangesFromList(snlist, inc=inc) elif isinstance(snlist[0], SerialNumberRange): self._ranges = snlist else: pass self._update() def _update(self): temp = set([]) e = [] for r in self._ranges: if r.EXCLUDE: e.append(r) else: temp |= set(r) for r in e: temp -= set(r) self._numbers = list(temp) self._numbers.sort() self._ranges = self.snRangesFromList(self._numbers, inc=self._inc, nosort=True) def __iter__(self): for sn in self._numbers: yield sn def __len__(self): return len(self._numbers) def __str__(self): '''Converts the list of serial numbers as a string. Returns: str: A character string containing the definition of one or more serial number ranges. ''' try: return self.toString() except: return super().__str__() def __repr__(self): '''Represents the serial number range as a string. Returns: str: The serial number as a string. ''' try: #return RANGE_SEP_CHAR.join( [repr(r) for r in self] ) return '{:s}(\'{!s}\')'.format(self.__class__.__name__, self.toString()) except: return super().__repr__()
[docs] def getNumbers(self): if self._numbers is None: temp = set([]) e = [] for r in self._ranges: if r.EXCLUDE: e.append(r) else: temp |= set(r) for r in e: temp -= set(r) self._numbers = list(temp) self._numbers.sort() return self._numbers
[docs] def toString(self): return RANGE_SEP_CHAR.join( [r.toString() for r in self._ranges] )
[docs] def addNumber(self, sn): '''Add a serial number into ranges list. Args: sn (SerialNumber): SN to addRange. ''' self.addRange(SerialNumberRange(sn, count=1, inc=self._inc))
[docs] def clear(self): self._numbers = [] self._ranges = []
[docs] def addRange(self, snRange, force=False): '''Add a serial number range into the list. Args: snRange (SerialNumberRange) : The range to addRange force (bool) : If true, forces the addition even if one or more SNs are already in the list. Otherwise the exception ValueError is issued. >>> SerialNumberList('X01:5;X10:5').addRange(SerialNumberRange.fromString('X06:4')).ranges [SerialNumberRange('X01-X14')] ''' assert isinstance(snRange, SerialNumberRange) self._ranges.append(snRange) self._update() return self
@property def inc(self): return self._inc @property def numbers(self): return self.getNumbers() @property def ranges(self): return self._ranges