Data is commonly displayed in the form of a table with headings for the columns. The first column can be thought of as an index into the table.
Country | Gross National Product |
Samovia | $13 000 000 |
Xanadu | $3 000 |
Lundy | $42 000 |
Pompey | $13 000 |
Here the names of the countries are used as an index to the table in order to find the gross national products of each.
x | f(x) |
1 | 8.9 |
2 | 4.5 |
3 | 6.7 |
4 | 5.1 |
A lookup table or, for short, a table is a finite set of ordered pairs {(x, f(x))}, that is, it is a function on a finite domain (the first column) to some range (the second column).
Lookup tables are very commonly used in a variety of applications in computing. For instance:
There is no particular reason to limit tables to two columns. They could be n-tuples, and be thought of as functions from a finite domain to an (n-1)-tuple. However, to keep the illustrations in this section simple, attention will be confined to the easiest case. In a fashion similar to the one already in use throughout this chapter, first define the data type to be entabled. Here, example 1 above is used.
DEFINITION MODULE Countries; (* define one data type in a generic way for use in semi-generic structures by R. Sutcliffe last modified 1995 06 07 *) FROM Strings IMPORT CompareResults; CONST nameLength = 30; TYPE Country; KeyType = ARRAY [0..nameLength] OF CHAR; FieldType = CARDINAL; ActionProc = PROCEDURE (Country); PROCEDURE New (VAR c : Country); PROCEDURE Valid (c : Country) : BOOLEAN; (* if new was never called for this country, this call is meaningless; if it was, returns true if variable is ok, false if either new failed or dispose has been called *) PROCEDURE Dispose (VAR c : Country); PROCEDURE Assign (source : Country; VAR dest : Country); (* asignment with value semantics *) PROCEDURE WriteCountryData (c : Country); PROCEDURE Compare (key1, key2: KeyType) : CompareResults; PROCEDURE SetKey (c : Country; name : KeyType); PROCEDURE GetKey (c : Country): KeyType; PROCEDURE SetField (c : Country; gnp : FieldType); PROCEDURE GetField (c : Country) : FieldType; END Countries.
Observe that an Assign procedure has been defined. One could just write
destCountry := sourceCountry;
but that kind of assignment is an assignment of the opaque item itself, pointer to the data, so that destCountry now points to the same location as sourceCountry does. The Assign in the module above is an assignment of the value of source^ to the dereferenced location of the destination, so that the value of dest^ is changed, but not the pointer itself. Which kind of assignment is used in a program depends on whether the programmer wishes to move data or change the pointer.
Assignment by changing the pointer is said to have reference semantics (meaning), and assignment by moving the values pointed to without changing the pointers is said to have value semantics.
These ideas can be examined closely in the implementation below. Observe also the definition of the procedure WriteCountryData to print out one data item. This will be useful when it comes time to print out the entire table, for this procedure will do one line of such a printout.
IMPLEMENTATION MODULE Countries; (* define one data type in a generic way for use in semi-generic structures by R. Sutcliffe last modified 1995 06 07 *) FROM Storage IMPORT ALLOCATE, DEALLOCATE; FROM STextIO IMPORT WriteString, WriteLn, WriteChar; FROM SWholeIO IMPORT WriteCard; IMPORT Strings; FROM Strings IMPORT CompareResults; TYPE Country = POINTER TO CountryDataNode; CountryDataNode = RECORD name : KeyType; gnp : FieldType; END; PROCEDURE New (VAR c : Country); BEGIN NEW (c); c^.name := ""; c^.gnp := 0; END New; PROCEDURE Valid (c : Country) : BOOLEAN; BEGIN RETURN (c # NIL) END Valid; PROCEDURE Dispose (VAR c : Country); BEGIN DISPOSE (c); END Dispose; PROCEDURE Assign (source : Country; VAR dest : Country); (* asignment with value semantics *) BEGIN dest^ := source^; END Assign; PROCEDURE WriteCountryData (c : Country); VAR count : CARDINAL; BEGIN WriteString (c^.name); FOR count := 1 TO 2 + nameLength - LENGTH (c^.name) DO (* pad to next field *) WriteChar (" "); END; WriteCard (c^.gnp, 12); WriteLn; END WriteCountryData; PROCEDURE Compare (key1, key2: KeyType) : CompareResults; BEGIN RETURN Strings.Compare (key1, key2); END Compare; PROCEDURE SetKey (c : Country; name : KeyType); BEGIN c^.name := name; END SetKey; PROCEDURE GetKey (c : Country): KeyType; BEGIN RETURN c^.name; END GetKey; PROCEDURE SetField (c : Country; gnp : FieldType); BEGIN c^.gnp := gnp; END SetField; PROCEDURE GetField (c : Country) : FieldType; BEGIN RETURN c^.gnp END GetField; END Countries.
The next step is to define the table type. Here, a module name of CountryTable is employed, but the same semi-generic style as before is used. If some other data type is to be entabled in this way, one need only define the type as above, then
FROM <myADTs> IMPORT <myADT>, KeyType, DataType;
TYPE DataType = <myADT>;
All references to the entabled data type from that point on are done using the local name, so no other changes need to be made. As usual, the allocation of memory for the data to be entabled is the responsibility of the table module rather than the calling program. Memory deallocation at table destruction time is also done by the table module.
Provision is made in the creation of a table to specify up to two strings to act as a title for the table and/or a set of headers for the columns. When the routine WriteTitle is called, a line of dashes the length of the longest of these two strings is printed underneath them to delimit the table and give the text a more professional look.
DEFINITION MODULE CountryTable; (* semi-generic table type by R. Sutcliffe last modified 1995 06 07 *) FROM Countries IMPORT Country, KeyType, ActionProc; TYPE DataType = Country; (* change this line and import as needed *) TitleString = ARRAY [0..80] OF CHAR; TableState = (allRight, empty, entableFailed, notFound, bad); Table; (* opaque type *) PROCEDURE TableStatus (t : Table) : TableState; (* Pre : t is a valid initialized table Post :The State of the table is returned *) PROCEDURE Create (VAR t : Table; s1, s2 : TitleString); (* Pre : none Post : t is a newly created empty table. s1 and s2 are stored for printing up to two lines of a table title *) PROCEDURE Find (t : Table; searchKey : KeyType; VAR data : DataType; VAR found : BOOLEAN); (* Pre : t is a valid initialized table Post : searchKey has been found and the ADT item returned in data and found is TRUE or the searchKey has not been found and found is FALSE. The State of the table is not affected by this operation. *) PROCEDURE Entable (t : Table; data : DataType); (* Pre : t is a valid initialized table Post :memory for the data is obtained, data has been entabled and the state of the table is allRight or the entabling failed and the state is entableFailed. *) PROCEDURE Remove (t : Table; key : KeyType; VAR data : DataType); (* Pre : t is a valid initialized table Post : data matching key has been removed and returned in data (not disposed of) and the state of the table is allRight or the removal failed and the state is notFound. *) PROCEDURE Destroy (VAR t : Table); (* Pre : t is a valid initialized table Post : the table memory is returned and the variable is invalid and the memory associated with the items in the table is removed by calling the ADT module dispose procedure. *) PROCEDURE WriteTitle (t : Table); (* Pre : t is a valid initialized table Post : any non-empty strings stored for this table are written one to a line followed by a line of n-dashes equal in length to the longest of the supplied strings and another carriage return if this is non-zero. *) PROCEDURE Traverse (t : Table; Proc : ActionProc); (* Pre : t is a valid initialized table Post : the table items are traversed and Proc is performed on each one. *) END CountryTable. IMPLEMENTATION MODULE CountryTable; (* semi-generic table type by R. Sutcliffe last modified 1995 06 07 *) FROM Countries IMPORT KeyType, ActionProc, Assign, Compare, GetKey, New, Valid, Dispose; FROM Storage IMPORT ALLOCATE, DEALLOCATE; FROM STextIO IMPORT WriteString, WriteLn, WriteChar; FROM Strings IMPORT CompareResults; TYPE NodePointer = POINTER TO TableNode; TableNode = RECORD item : DataType; toPoint, fromPoint : NodePointer; END; Table = POINTER TO TableData; TableData = RECORD top : NodePointer; title1, title2 : TitleString; state : TableState; END; PROCEDURE TableStatus (t : Table) : TableState; BEGIN IF t # NIL THEN RETURN t^.state; ELSE RETURN bad; END; END TableStatus; PROCEDURE Create (VAR t : Table; s1, s2 : TitleString); BEGIN NEW (t); t^.top := NIL; t^.title1 := s1; t^.title2 := s2; t^.state := empty; END Create; PROCEDURE Find (t : Table; searchKey : KeyType; VAR data : DataType; VAR found : BOOLEAN); VAR point : NodePointer; BEGIN found := FALSE; point := t^.top; WHILE point # NIL DO IF Compare (searchKey, GetKey (point^.item) ) = equal THEN data := point^.item; found := TRUE; RETURN; ELSE point := point^.toPoint END; END; END Find; PROCEDURE MakeNode () : NodePointer; VAR temp : NodePointer; BEGIN NEW (temp); (* get node memory *) IF temp # NIL THEN New (temp^.item); (* node OK so get data value memory *) IF NOT Valid (temp^.item) THEN (* failed so return NIL *) DISPOSE (temp); END; END; RETURN temp; END MakeNode; PROCEDURE Entable (t : Table; data : DataType); VAR temp : NodePointer; state : TableState; BEGIN state := TableStatus (t); IF (state # bad) AND (state # entableFailed) THEN temp := MakeNode (); IF temp # NIL THEN Assign (data, temp^.item); temp^.toPoint := t^.top; (* point to old top *) temp^.fromPoint := NIL; IF t^.top # NIL THEN t^.top^.fromPoint := temp; END; t^.top := temp; t^.state := allRight; ELSE t^.state := entableFailed END; ELSE t^.state := entableFailed END; END Entable; PROCEDURE Remove (t : Table; key : KeyType; VAR data : DataType); VAR point : NodePointer; BEGIN point := t^.top; WHILE point # NIL DO IF Compare (key, GetKey (point^.item) ) = equal THEN data := point^.item; IF point^.fromPoint # NIL THEN point^.fromPoint^.toPoint := point^.toPoint; ELSE t^.top := point^.toPoint; END; IF point^.toPoint # NIL THEN point^.toPoint^.fromPoint := point^.fromPoint END; t^.state := allRight; RETURN; ELSE point := point^.toPoint END; END; t^.state := notFound; (* get here only if not found *) END Remove; PROCEDURE KillNode (VAR node : NodePointer); (* give back all memory associated with node *) BEGIN IF node # NIL THEN Dispose (node^.item); DISPOSE (node); END; END KillNode; PROCEDURE Destroy (VAR t : Table); VAR point, next : NodePointer; BEGIN IF t = NIL THEN RETURN END; point := t^.top; WHILE point # NIL DO (* kill all nodes *) next := point^.toPoint; KillNode (point); point := next; END; DISPOSE (t); (* and memory for table info *) END Destroy; PROCEDURE WriteTitle (t : Table); VAR len, len2, count : CARDINAL; BEGIN len := LENGTH (t^.title1); len2 := LENGTH (t^.title2); IF len2 > len THEN len := len2 (* take the maximum of the two *) END; IF t^.title1 [0] # "" THEN WriteString (t^.title1); WriteLn; END; IF t^.title2 [0] # "" THEN WriteString (t^.title2); WriteLn; END; IF len # 0 THEN FOR count := 1 TO len DO (* write a bar below the title *) WriteChar ("-"); END; WriteLn; END; END WriteTitle; PROCEDURE Traverse (t : Table; Proc : ActionProc); VAR point : NodePointer; BEGIN point := t^.top; WHILE point # NIL DO Proc ( point^.item); point := point^.toPoint; END; END Traverse; END CountryTable.
In the test harness below, note that various aspects of the initial ADT module Countries are tested, and then the table module is checked extensively to see if the entabling, searching, and removal of items works correctly. A data type such as Table should always be checked in this way, with simple data, so as to ensure that the logic is correct, before employing the library for larger types that are more difficult to test. While the author is not so foolish as to guarantee that the libraries above are error free, it is notable that the testing of this carefully designed pair of library modules turned up only one minor logical error--all other changes made during the testing process were of a purely cosmetic nature.
MODULE TestCountryTable; (* program to test the logic of the table library with countries and their GNP by R. Sutcliffe modified 1995 06 07 *) IMPORT Countries, CountryTable, STextIO, SWholeIO; VAR table : CountryTable.Table; country : Countries.Country; str : Countries.KeyType; num : Countries.FieldType; gotIt : BOOLEAN; PROCEDURE WriteTable; BEGIN CountryTable.WriteTitle (table); CountryTable.Traverse (table, Countries.WriteCountryData); END WriteTable; BEGIN CountryTable.Create (table, " Table of countries and their GNP", "Country Name GNP "); Countries.New (country); (* just do it once *) Countries.SetKey (country, "Samovia"); Countries.SetField (country, 13000000); (* test some stuff in Countries *) str := Countries.GetKey (country); STextIO.WriteString (str); num := Countries.GetField (country); SWholeIO.WriteCard (num,10); STextIO.WriteLn; STextIO.WriteLn; (* now get the table filled up *) CountryTable.Entable (table, country); WriteTable; STextIO.WriteLn; Countries.SetKey (country, "Xanadu"); Countries.SetField (country, 3000); CountryTable.Entable (table, country); WriteTable; STextIO.WriteLn; Countries.SetKey (country, "Lundy"); Countries.SetField (country, 42000); CountryTable.Entable (table, country); WriteTable; STextIO.WriteLn; Countries.SetKey (country, "Pompey"); Countries.SetField (country, 13000); CountryTable.Entable (table, country); WriteTable; STextIO.WriteLn; STextIO.WriteLn; (* test finds *) CountryTable.Find(table, "Pompey", country, gotIt); IF gotIt THEN str := Countries.GetKey (country); STextIO.WriteString ("Got "); STextIO.WriteString (str); ELSE STextIO.WriteString ("no got Pompey"); END; STextIO.WriteLn; STextIO.WriteLn; CountryTable.Find(table, "Canada", country, gotIt); IF gotIt THEN str := Countries.GetKey (country); STextIO.WriteString ("Got "); STextIO.WriteString (str); ELSE STextIO.WriteString ("no got Canada"); END; STextIO.WriteLn; STextIO.WriteLn; (* test removes *) CountryTable.Remove(table, "Pompey", country); IF CountryTable.TableStatus (table) = CountryTable.allRight THEN STextIO.WriteString ("removed Pompey"); ELSE STextIO.WriteString ("could not remove Pompey"); END; STextIO.WriteLn; STextIO.WriteLn; (* now check to ensure its gone *) CountryTable.Find(table, "Pompey", country, gotIt); IF gotIt THEN str := Countries.GetKey (country); STextIO.WriteString ("Got "); STextIO.WriteString (str); ELSE STextIO.WriteString ("no got Pompey"); END; STextIO.WriteLn; STextIO.WriteLn; WriteTable; STextIO.WriteLn; STextIO.WriteLn; (* now try to remove something not there *) CountryTable.Remove(table, "Canada", country); IF CountryTable.TableStatus (table) = CountryTable.allRight THEN STextIO.WriteString ("removed Canada"); ELSE STextIO.WriteString ("could not remove Canada"); END; STextIO.WriteLn; STextIO.WriteLn; (* now remove one not at the start *) CountryTable.Remove(table, "Xanadu", country); IF CountryTable.TableStatus (table) = CountryTable.allRight THEN STextIO.WriteString ("removed Xanadu"); ELSE STextIO.WriteString ("could not remove Xanadu"); END; STextIO.WriteLn; STextIO.WriteLn; WriteTable; STextIO.WriteLn; STextIO.WriteLn; (* now see if destroy seems to work *) CountryTable.Destroy (table); IF CountryTable.TableStatus (table) = CountryTable.bad THEN STextIO.WriteString ("table deleted"); ELSE STextIO.WriteString ("could not destroy"); END; STextIO.WriteLn; STextIO.WriteLn; END TestCountryTable.
When this test was run, the following results were obtained:
** Run log starts here ** Samovia 13000000 Table of countries and their GNP Country Name GNP --------------------------------------------- Samovia 13000000 Table of countries and their GNP Country Name GNP --------------------------------------------- Xanadu 3000 Samovia 13000000 Table of countries and their GNP Country Name GNP --------------------------------------------- Lundy 42000 Xanadu 3000 Samovia 13000000 Table of countries and their GNP Country Name GNP --------------------------------------------- Pompey 13000 Lundy 42000 Xanadu 3000 Samovia 13000000 Got Pompey no got Canada removed Pompey no got Pompey Table of countries and their GNP Country Name GNP --------------------------------------------- Lundy 42000 Xanadu 3000 Samovia 13000000 could not remove Canada removed Xanadu Table of countries and their GNP Country Name GNP --------------------------------------------- Lundy 42000 Samovia 13000000 table deleted
As the output from this test illustrates, this implementation of a table had the semantics of set membership--a data item either was in the table or it was not. No sorting was done. If sorted tables are desired, the entabling routine would have to be written accordingly. This is left as an exercise for the reader.