Lab 5: Keyed Lists and using procedures

  1. The first step to building a library of commands is to construct the first procedure and test it.

    Create a new Tcl file named keyedListLib.tcl and then add the appendKeyedPair procedure from the lecture.

    Create another Tcl file named keyedListTest.tcl and write code to test that the procedure works.

    A good test would allow the user add elements and see the changes. You can use the -textvariable option to the label command to view the contents of the keyed list with code like this:

    
    set keyList {}
    set w [label .l_keyList -textvariable keyList -width 50]
    grid $w -row 1 -column 1
    

    A button command to add a pair of elements to the list might look like this:

    
    set key 1
    set w [button .b_addPair -text "Add Pair" \
        -command "set keyList [appendKeyedPair $keyList [incr key] $key]"]
    grid $w -row 2 -column 1
    

    This command adds a pair of elements where the key and value are identical, and they increase each time you click the button.

    Put these snippets together into a test application and confirm that it works and that the appendKeyedPair application works.

    The button code is not correct. Think about when the substitution is happening and where when it should be happening. Fix the bug and test it again.

    When it works, it should look like this:

    solution

  2. Make a procedure to perform the action associated with the button. It does not need any arguments. Use this procedure name in the -command option to the button.

    Hardcode the name of the keyed list to modify.

    Notice that you don't need to worry about when substitutions happen with the procedure.

    solution

  3. Add the getValue procedure to keyListLib.tcl and add a test for this to the keyedListTest.tcl application.

    Each time you click the button to test the getValue procedure it should display the key and the value returned, and step to the next key.

    After a few button clicks, it should look like this:

    solution

    The getValue procedure in the lecture has a bug.

    Here is the code. What happens when the first occurance of the searched for key is a value, not a key?

    For instance, consider a keyed list like this:

    
    set keyed {A 1 B 2 C D D 4}
    getValue $keyed D
    

    
    proc getValue {list key} {
      set pos [lsearch $list $key]
      if {($pos >= 0) && (($pos & 1) == 0)} {
          return [lindex $list $pos+1]
      }
      return ""
    }
    

    The lsearch command will find the first occurance of a D, which is the value associated with the key "C".

    the getValue then returns an empty string, rather than searching to see if that key exists later in the keyed list.

    Here is code that uses a for loop with no body to test the results of the lsearch and continue looping if it's not a valid position.

    
    ################################################################
    # proc getValue {list key}--
    #    Retrieve the value associated with a key
    # Arguments
    #   list        The keyed list
    #   key         The key to search for
    #
    # Results
    #   No side effects.
    #
    proc getValue {list key} {
        set start 0
        for {set pos [lsearch $list $key]} \
            {($pos >= 0) && (($pos & 1) == 1)} \
            {set pos [lsearch -start $pos+1 $list $key]} {
        }
        if {$pos >= 0} {
          return [lindex $list $pos+1]
        } else {
          return ""
        }
    }
    

    The while command would make this code simpler. Look up the while command and modify the code to use that instead.

  4. Add the deleteElement procedure to the keyedListLib.tcl file and then add a test similar to the testGetVal procedure to test it. You'll need new global variables.

  5. Add the replaceValue procedure to the keyedListLib.tcl file and then add a test similar to the testGetVal procedure to test it. You'll need new global variables.

    The testReplaceValue procedure can increment through a set of numbers, replacing a the value associated with a key with a value one lower.

    This is an example of a test that checks that a procedure can work, but does not test that the procedure is robust. The bad getValue procedure passes this test. Change the test in this solution to use a value one higher instead of one lower and see if the original getValue procedure still passes the test.

    After clicking Add Pair 4 times, Test Replace twice, and Test Delete once, it should look like this:

    solution

  6. Actions that get repeated frequently are good things to put into a procedure. For instance, this sequence of events happens whenever we need to open a file and process the data line by line:

    
      set if [open $fileName r]
      set d [read $if]
      close $if
      set datalist [split $d \n]
    

    This functionality can be put into a procedure that looks like this:

    
    ################################################################
    # proc readFileAsList {fileName}--
    #    read in a file and convert it to a list delimited by newlines
    # Arguments
    #   fileName	Name of the file to read
    # 
    # Results
    #   no side effects
    # 
    proc readFileAsList {fileName} {
      set if [open $fileName r]
      set d [read $if]
      close $if
      return [split $d \n]
    }
    

    Here's a file of gem colors that starts like this:

    
    Black Obsidian Onyx 
    Blue Aquamarine Blue Lace Agate Iolite Lapis Lazuli Sapphire Tanzanite Topaz Turquoise 
    

    write an application to read in this file and create a keyed list with each gem as a key and the appropriate color as the value.

    You'll need to use nested loops to solve this.

    If you use puts to display the gem color list, it should start like this:

    
    Obsidian Black Onyx Black Aquamarine Blue {Blue Lace} Blue Iolite Blue
    

  7. Here's a file of birthstones. It looks like this:

    
    January Garnet 
    February Amethyst 
    March Aquamarine 
    April Diamond 
    

    Add some code to the previous program to read this file, convert it to a list and then generate a display that looks like this:

    You can use the getValue function from keyedListLib.tcl file and the gem color keyed list to find out the color of each birthstone.

  8. The practice.txt file contains the output from the practice exercise. It looks like this:

    
    16:02:14	7723	2999	192.168.1.2	->	mail.emich.edu 	443
    16:02:19	534	2183	192.168.1.2	->	0.channel41.facebook.com 	80
    16:02:40	20247	1098	192.168.1.249	->	204.176.49.116 	80
    16:03:17	534	2183	192.168.1.2	->	0.channel41.facebook.com 	80
    16:04:15	534	2181	192.168.1.2	->	0.channel41.facebook.com 	80
    16:04:52	460	882	192.168.1.231	->	204.176.49.2 	80
    

    This report shows

    1. the timestamp for start of a conversation
    2. the number of bytes read
    3. the number of bytes written
    4. the local IP address
    5. an arrow
    6. the remote address
    7. the service port

    It would be useful to know how many bytes were transferred by which IP addresses.

    To do that, we could create a keyed list that uses the local IP address as the key.

    An application can have more than one keyed list. For instance, we can have one keyed list of values for the sizes of packets we've read and one for the quantities of bytes written.

    The trick to adding new data to the list is knowing what data is new and what data belongs to an IP address that's already been used.

    The getValue procedure will return an empty string when you request a value for a key that isn't in the list. We can use that behavior to determine whether or not a key has been added.

    These variables are used in this sample code:

    localIP
    The local IP value read from the current line of the data file
    in
    The number of bytes input from the current line of the data file
    inputByLocalIP
    A keyed list where the keys are local IP addresses and the value is the running total of bytes input to that IP address.
    inTot
    A temporary variable that holds the current total number of bytes input for a given IP address.

    
        set inTot [getValue $inputByLocalIP $localIP]
        if {$inTot eq ""} {
            set inTot 0
            set inputByLocalIP [appendKeyedPair $inputByLocalIP $localIP $inTot]
        }
        set inTot [expr {$in + $inTot}]
        set inputByLocalIP [replaceValue $inputByLocalIP $localIP $inTot]
    

    Extend this code to read the file and generate a report of IP addresses, total bytes read by that IP address and total bytes written by that IP address.

    The output should resemble this:

    
     192.168.1.2 8829671 1173032
     192.168.1.249 25348 4973
     192.168.1.231 21367 3130
     192.168.1.250 1590 2688
    

    solution

  9. Modify the previous code to use a set of labels instead of puts. The output should look like this:


Copyright Clif Flynt 2010