ring.py

#! /usr/bin/python3
# ==================================================================
# Ring Data Structure  (a ring of beads)
#
# User objects are held in a ring that has no begining or end. There
# is a "pointer" that "points" to one of the objects. The "pointer"
# can be move around the ring.
#
# Notes:
# 1. I have no idea what this data structure could be use for.
#    I did it because it was fun.
#
# ==================================================================
#
# In this code the class Bead is external to the class Ring.
# Bead could be an internal class of the Ring class. Making
# the class Bead internal  means the user only deals with
# their objects and the Ring class. This could make things
# simpler for the user? 
#
# ==================================================================
# To create a PyDoc html file:
#
#    pydoc -w ring
#
# To run a web server (port 2001) for the documentation
# for the modules in the current directory:
#
#    python3 -m pydoc -p 2001
#
# ==================================================================

class Bead():
    '''
    Ring bead class definition

    Bead is a helper class for the ring class. It is generally
    not used by the users.

    Beads hold the objects the user inserts into the ring. The
    ring can contain a mixture of objects because the ring does
    not have knowledge of object internals. Objects are the
    responsibility of the user.

    Attributes:
      obj               user defined object
      forward           next bead in the forward direction
      backward          next bead in the backward direction

    Bead methods:

    __init__            create a bead
    '''

    # ---- init ring bead ------------------------------------------

    def __init__(self,obj):
        '''
        Create a bead.
        '''
        self.obj      = obj
        self.forward  = self
        self.backward = self

class Ring():
    '''
    Ring class definition

    Beads hold the objects the user inserts into the ring. The
    ring can contain a mixture of objects because the ring does
    not have knowledge of object internals. Objects are the
    responsibility of the user.

    A ring always has a "pointer" pointing to one of the ring beads
    or "None" if there are no ring beads. This bead is called the
    ring's current bead.

    The ring's beads are not maintained in any particular order.
    They are in the order the user inserts them.

    Why have beads?

    Beads allow the ring to know nothing about the objects
    it contains and therefore imposes no requirements on the
    objects in the ring.

    Attributes:
      bead              ring's current bead    
      count             number of beads in the ring

    Ring methods:

    __init__            create a ring
    insert              insert a bead into the ring
    forward             move forward one bead
    backward            move backward one bead
    fetch               return the object in the ring's current bead
    locate              locate a specific object in the ring
                        if found, make it the ring's current bead
    delete              delete the ring's current bead
    display_ring        display (print) the ring
    '''

    # ---- init ring -----------------------------------------------

    def __init__(self):
        '''
        Create a ring.

        Attributes:
          bead = none
          count = 0
        '''
        self.bead  = None            # ring's current bead 
        self.count = 0               # not required but nice to know

    # ---- insert new bead -----------------------------------------
    # ---- before or after the current ring bead -------------------

    def insert(self,obj,before = True):
        '''
        Insert a new bead into the ring.

        If the 'before' flag is True, the new bead is inserted
        before (forward) the ring's current bead. If the
        'before' flag is False, the new bead is inserted behind
        (backward) the ring's current bead.
        '''
        if self.bead == None:
            self.bead = Bead(obj)
        else:
            b  = Bead(obj)            # new bead
            cb = self.bead            # ring's current bead
            fb = self.bead.forward    # ring's current bead.forward
            bb = self.bead.backward   # ring's current bead.backward
            if before:
                b.forward   = fb
                b.backward  = cb
                fb.backward = b
                cb.forward  = b
            else:
                b.backward  = bb
                b.forward   = cb
                bb.forward  = b
                cb.backward = b
        self.count += 1

    # ---- move forward one bead -----------------------------------
    # ---- the bead becomes the ring's current bead ----------------

    def forward(self):
        '''
        Move forward one bead.

        Return False if there is no current bead, else
        return True.
        '''
        if self.bead == None:
            return False
        else:
            self.bead = self.bead.forward
            return True

    # ---- move backward one bead ----------------------------------
    # ---- the bead becomes the ring's current bead ----------------

    def backward(self):
        '''
        Move backward one bead.

        Return False if there is no current bead, else
        return True.
        '''
        if self.bead == None:
            return False
        else:
            self.bead = self.bead.backward
            return True

    # ---- return current bead's obj -------------------------------

    def fetch(self):
        '''
        Return the object contained in the ring's current bead.
        If there is no currernt bead return None.
        '''
        if self.bead == None:
            return None
        else:
            return self.bead.obj

    # ---- locate object -------------------------------------------

    def locate(self,val,f):
        '''
        Locate a specific object in the ring.

        If found, make the found bead the ring's current bead
        and return True, else return False.

        Because the ring knows nothing about the objects stored
        in it, the user must pass in a function to do the
        matching test.

        Parameters:
          val      value to be locate
          f        function (user supplied) to compare a user
                   object with the val passed to this method.
        '''
        if self.bead == None:
            return False 
        b = self.bead                # current bead
        sb = b                       # starting bead
        while True:
            if f(b.obj,val):
                self.bead = b
                return True
            if b.forward is sb:
                break
            b = b.forward
        return False

    # ---- delete bead ---------------------------------------------

    def delete(self):
        '''
        Delete the ring's current bead.

        If there is no current bead return False, else return True.
        '''
        if self.bead == None:
            return False
        b  = self.bead
        fb = b.forward
        bb = b.backward
        # ---- only one bead in the ring?
        if self.count < 2:
            self.bead  = None
            b.forward  = None
            b.backward = None
            self.count = 0
            return True
        bb.forward  = fb
        fb.backward = bb
        b.forward   = None
        b.backward  = None
        self.bead   = fb
        self.count -= 1
        return True

    # ---- display ring --------------------------------------------
    # ---- Because the ring knows nothing about the object's it ----
    # ---- contains, you must pass in a function that knows --------
    # ---- how to display information about the object's. ----------

    def display_ring(self,f):
        '''
        Print all of the beads in the ring.
        
        Because the ring knows nothing about the objects it
        contains, the user must pass in a function to return
        an object's information as a string. (The size of the
        display string should be limited. See the code.)

        Parameters:

          f    user defined function that returns a
               user defined data value for a object.
        '''
        print('Ring bead count is {}\n'.format(self.count))
        if self.bead == None: return
        b = self.bead                # current bead
        sb = b                       # starting bead
        while True:
            print('------------------------------')
            print('bead     obj = {}'.format(f(b.obj)))
            print('forward  obj = {}'.format(f(b.forward.obj)))
            print('backward obj = {}'.format(f(b.backward.obj)))
            if b.forward == sb:
                break
            b = b.forward
        return

# ------------------------------------------------------------------
# main testing
# ------------------------------------------------------------------

if __name__ == '__main__':

     ###############################################################
     # ---- support functions --------------------------------------

    def get_user_input(prompt):
        '''
        Prompt the user and return their input
        '''
        return input(prompt)

    def pause_program():
        '''
        pause the program until the user is ready
        '''
        get_user_input('\nPress enter to continue: ')

    def display_obj(obj,msg=''):
        '''
        Display object information
        '''
        if msg: print(msg)
        print('obj {}'.format('None' if not obj else obj.data))

    def return_obj_data(obj):
        '''
        Return data from an object
        '''
        print('return_obj_data({}'.
            format('None' if not obj else obj.data))
        return 'None' if not obj else obj.data

    def match_obj(obj,val):
        '''
        Match object data
        '''
        ##print('match_obj({},{})'.format(obj.data,val))
        return True if obj.data == val else False

    ################################################################

    # ---- test object class

    class testobj():

        def __init__(self,data):
            self.data = data

    # ---- display object

    def display_obj(obj,msg=''):
        if msg: print(msg)
        print('obj {}'.format('None' if not obj else obj.data))

    def return_obj_data(obj):
        return 'None' if not obj else obj.data

    #???????????????????????????????????????????????????????????????
    # ---- some special tests --------------------------------------
    #
    #objx = testobj(333)
    #
    #rng = Ring()
    #
    #print('---- empty ring (no beads) ----------------')
    #display_obj(rng.fetch())
    #print('---- delete current bead ------------------')
    #o = rng.delete()
    #print('True' if o else 'False')
    #print('---- display ring -------------------------')
    #rng.display_ring(return_obj_data)
    #
    #print('\n')
    #
    #print('---- ring with one bead -------------------')
    #print('---- insert obj (333) ---------------------')
    #rng.insert(objx)
    #display_obj(rng.fetch())
    #print('---- display ring -------------------------')
    #rng.display_ring(return_obj_data)
    #print('---- delete current bead ------------------')
    #o = rng.delete()
    #print('True' if o else 'False')
    #print('---- display ring -------------------------')
    #rng.display_ring(return_obj_data)
    #
    #quit()
    #???????????????????????????????????????????????????????????????

    # ---- create test objects

    obj1 = testobj(100)
    obj2 = testobj(200)
    obj3 = testobj(300)
    obj4 = testobj(400)
    obj5 = testobj(500)
    obj6 = testobj(150)
    obj7 = testobj(250)

    # ---- create ring

    rng = Ring()

    # --------------------------------------------------------------
    # ---- insert and display some objects

    print('---- insert obj --')
    rng.insert(obj1)
    display_obj(rng.fetch(),'--- cur obj -----')

    print('---- insert obj --')
    rng.insert(obj2)
    print('---- forward -----')
    rng.forward()
    display_obj(rng.fetch(),'--- cur obj -----')

    print('---- backward ----')
    rng.backward()
    display_obj(rng.fetch(),'--- cur obj -----')

    print('---- insert obj --')
    rng.insert(obj3,False)
    print('---- backward ----')
    rng.backward()
    display_obj(rng.fetch(),'--- cur obj -----')

    print('\n------------------------------------------')
    print(' display ring')
    print('------------------------------------------\n')
    rng.display_ring(return_obj_data)

    print('\n------------------------------------------')
    print(' insert more objects and display the ring')
    print(' display the modified ring')
    print('------------------------------------------\n')

    rng.insert(obj4)
    rng.insert(obj5,False)
    rng.display_ring(return_obj_data)

    print('\n------------------------------------------')
    print(" delete the ring's current bead")
    print(" display the modified ring")
    print('------------------------------------------\n')

    rng.delete()
    rng.display_ring(return_obj_data)

    print('\n------------------------------------------')
    print(" search the ring for an object")
    print('------------------------------------------')

    while True:

        val = get_user_input('\nEnter obj value: ')

        sval = val.strip()

        if sval == '': break

        if sval.isdigit() != True:
            print('\nIllegal value entered ({})'.format(sval))
            pause_program()
            continue

        ival = int(sval)

        tf = rng.locate(ival,match_obj)

        if tf:
            print('\nFound the object {} in the ring'
                .format(ival))
            print("It is now the ring's current bead")
            display_obj(rng.fetch(),'\n--- cur obj -----')
        else:
            print('\nDid not find the object {} in the ring'
                .format(ival))
            print("The ring's current bead did not change")
            display_obj(rng.fetch(),'\n--- cur obj -----')