Next / Previous / Contents / Shipman's homepage

32.3. BandNoField.incrementPrefix(): Increment a prefix with rollover

This method first extracts the prefix portion of self. Then it adds one, propagating the carry until it reaches the end of the odometer portion, or until it hits a non-digit character, whichever comes first. The class attribute TRAILING_DIGITS is a compiled regular expression that matches trailing digits at the end of a string, but it must match at least one.

baseclasses.py
# - - -   B a n d N o F i e l d . i n c r e m e n t P r e f i x   - - -

    TRAILING_DIGITS = re.compile (
        r'\d+'           # Matches one or more digits
        r'$')            # End-of-string anchor

    def incrementPrefix(self):
        '''Return self's prefix, incremented with rollover.
        '''

First we extract the part of the prefix that will not be affected by rollover, and the part that may be affected.

baseclasses.py
        #-- 1 --
        # [ fixed      :=  self's fixed part
        #   odoPrefix  :=  odometer portion of self's prefix ]
        fixed     = self.value [ : L_BAND_NO_FIXED ]
        odoPrefix = self.value [ L_BAND_NO_FIXED : L_BAND_NO_PREFIX ]

Next we separate odoPrefix into a “good part,” containing all trailing digits, and a “junk part,” consisting of all the characters to the left of the good part. If the good part is empty, there's nothing we can do but return the whole prefix as it stands. Note that we use the .search() method on the regular expression, so it will find matches anywhere, not the .match() method that matches only at the start of the string.

baseclasses.py
        #-- 2 --
        # [ if odoPrefix has any trailing digits ->
        #     goodPart  :=  those digits
        #     junkPart  :=  characters from odoPrefix before
        #                   those digits
        #   else ->
        #     return self's prefix ]
        m = self.TRAILING_DIGITS.search(odoPrefix)
        if  m is None:
          return self.prefix()
        else:
          goodPart = m.group()
          junkPart = odoPrefix[:-len(goodPart)]

At this point, goodPart contains only digits, so we can convert it to integer, add one, and convert it back to a string (with left zeroes). There are several subtle Python features here. Formatting with a "%*d" format takes two values, a field size and a field value, so for example "%*d" % (3, 13) would produce the value " 13". Adding a 0 just after the % specifies left zero fill, so for example "%0*d" % (5, 13) would produce the value "00013".

baseclasses.py
        #-- 3 --
        # [ goodPart is a string containing only digits ->
        #     goodLen   :=  len(goodPart)
        #     goodPlus  :=  int(goodPart)+1, converted to an integer
        #         with left zero fill to size L_BAND_NO_ODO_PREFIX ]
        goodLen  = len(goodPart)
        goodPlus = ("%0*d" %
                      (goodLen, int(goodPart) + 1))

There is one annoying pathological case. Suppose that the odoPrefix part is "999". If we add one to that, we get "1000". In that case, we really should discard the carry.

baseclasses.py
        #-- 4 --
        if  len(goodPlus) > goodLen:
            goodPlus = goodPlus[1:]

All that remains is to reassemble the three different parts of the prefix: the non-odometer part, any “junk” part, and the incremented odometer part.

baseclasses.py
        #-- 5 --
        return "%s%s%s" % (fixed, junkPart, goodPlus)

None of this would have been necessary if the Bird Banding Lab had stipulated that a string of bands is number 00–99 instead of 01–100. Obviously there were no programmers involved in this decision!