Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
34 views

The Secrets of Array Handling, Part 2

This document discusses how Clipper 5.0 implements arrays and objects as arrays, allowing programmers to model complex data structures. It also explains how the ASCAN and ASORT functions were updated in Clipper 5.0 to allow passing a code block parameter to specify how elements should be compared or sorted, rather than just what elements to operate on. This increased flexibility allows parameterizing both the data and logic of sorting and searching algorithms.

Uploaded by

Jose Cordero
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
34 views

The Secrets of Array Handling, Part 2

This document discusses how Clipper 5.0 implements arrays and objects as arrays, allowing programmers to model complex data structures. It also explains how the ASCAN and ASORT functions were updated in Clipper 5.0 to allow passing a code block parameter to specify how elements should be compared or sorted, rather than just what elements to operate on. This increased flexibility allows parameterizing both the data and logic of sorting and searching algorithms.

Uploaded by

Jose Cordero
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 5

The secrets of array handling, part 2.

Data Based Advisor

April 01, 1991 Spence, Rick

Last month we looked at Clipper 5.0's array handling capabilities. 5.0 arrays are important in that they allow you to model
any data structure. This month I show how Clipper objects are implemented as arrays. We continue with a quick review of
code blocks, discuss some of the built-in array processing functions, and finish with coverage of multi-dimensional and
"nested" arrays.

Clipper's objects are also implemented as arrays. When you create an object by calling its class function, or constructor, the
function returns an array. You can thus use == to determine whether two variables refer to the same object. This allows you
to browse more than one database at any time.

For example, Listing 1 (omitted here because of space but included on this month's source code diskette and on our
CompuServe forum) creates two TBrowse objects tb1 and tb2, and initializes them to browse different databases in different
windows. The key to the routine is the variable "cb," standing for "current browse." The code initializes it to point to tb1;
remember, there’s only one object, but two things now point to it. We'll return to this in a moment.

The main loop is the standard way of implementing a TBrowse. Note how you optimize type-ahead with:

DO WHILE nextkey () = 0 .AND !cb:stabilize ()


ENDDO

Clipper's short circuit evaluation ensures STABILIZED() isn't called until the type-ahead buffer is empty. Also note how you
use a function to return a code block to implement the standard methods. This saves you from repeating the code for every
browse you use. An interesting point is that the returned code block contains a formal parameter that it expects to be a
TBrowse object. This lets you parameterize the standard methods with regard to a TBrowse object (different browse systems
have their own TBrowse objects). You specify the actual TBrowse object when you evaluate the code block.

Now--this is the key--you write the main loop in terms of the variable cb. Initially cb points to tb 1, so when you invoke a
TBrowse method, you're invoking it on tb 1. However, simply by reassigning to cb, you can browse a different database. Next
time you invoke a method on cb, it operates on the newly set TBrowse object. Listing 1 does this in the CASE statement where
it checks:

CASE 1 Key = SWITCH_KEY

If cb is currently set to tb1, it sets it to tb2, so that it's on tb2 the next time we invoke a method. Otherwise, it must be set to
tb2, so that the code will reset it to tb1.

I'll cover TBrowse in more detail in a later article, so if you haven't used it extensively, Listing 1's code may be rather cryptic.
Compile and link it anyway; it does work!

Why Nantucket gave us code blocks

A few months ago I discussed code blocks. I said they're used in two cases:

* To replace repetitive macro expansion


* To parameterize the logic of a routine

The first use is obvious. The macro operator always performs two indivisible steps: code compilation and evaluation. By
compiling the code once and saving it in a variable, you can continually re-evaluate it without recompiling. This gives you a
speed advantage.

The second advantage, the real use of code blocks, is a little more difficult to grasp.

Let's digress for a moment. Why do you write functions or procedures? One reason is to break a problem into smaller parts.
Rather than writing a 1,000-line program, you split it into 20 50-line routines, even if you only call each one once. This aids
your understanding of the problem.

The other reason is to parameterize a sequence of operations with regard to some data. Consider the simple task of centering a
message on the screen. You probably have a routine to do this. You pass it a message and a line number and the routine
displays the centered message on that line. You call this routine from many parts of the application, saving code, and making
the program more reliable.

A routine then is a sequence of operations, parameterized with regard to a set of data. One of the biggest aftermarkets
for Clipper, in fact, is routine libraries. Third-party developers create a number of routines, parameterized with regard to a set
of data, and market them as libraries.

Code blocks allow you to take this parameterization one step further. Consider a function to search an array. There's one built
into Clipper. ASCAN() searches an array for a value you specify, and returns the array element where it found it, or a zero if it
didn't find it. It searches the array by successively comparing elements with the value you specify. It compares according to
the current state of SET EXACT.

ASCAN() is a parameterized, array-searching algorithm. You tell it what array to search and what to search for, and it does it
according to the current state of SET EXACT. The routing is fully parameterized then, except with regard to how the actual
comparison is done. What if you wanted to search an array of character variables ignoring case? ASCAN() in the form we've
described it, won't do it. What if you have a sorted array and you want the search to stop at the next highest element if you
don't find what you're looking for (a soft search)? ASCAN() won't do that either.

What you need is a way of telling ASCAN() how to do the comparison. In clipper Summer '87 you could write your own version,
in which you passed an extra parameter indicating how to do the comparison, as in:

FUNCTION my_ascan
PARAM ar, search_val, search _method
PRIVATE elem, found_yet
found_ yet = .F.
DO WHILE ! found _yet .AND. elem <= len (ar)
DO CASE
CASE search_method = 1 && Compare - method 1
found_yet = . . .
CASE search_method = 2 && Compare - method 2
found _ yet = . . .
ENDCASE
ENDDO
RETURN elem

You've also parameterized the logic to a certain extent, but to use a new searching method, you must change the function. If
you don't have the source, you can't. It would be better if the caller could specify the actual comparison as code, rather than as
a special value that the routine checks with a CASE statement. This gives complete flexibility. You could then parameterize both
the data the routine operates on and how it operates on it. You parameterize both the data and the logic.

In 5.0, code blocks give you this flexibility. You can rewrite MY_ASCAN to take a code block as the third parameter. The caller
specifies how to do the comparison by creating a code block to do it. Since a code block can contain any expression, you can
compare in any way. Fortunately, Clipper's ASCAN() function has been changed in exactly that way. You can pass it a code
block parameter specifying how to do the comparison.

ASCAN() in 5.0

The ASCAN() function in 5.0, while compatible with the in Summer '87, is also extended. Rather than passing a simple value to
search for as the second parameter, you can pass it a code block. When you do, ASCAN works a little differently. It evaluates
your code block to do the comparison. It passes the current array element it's looking at as a parameter, and the block returns
a logical, indicating whether this is the element it wants or not. ".T." means it is, ".F." means it isn't. If the block returns a ".T.,"
ASCAN() stops, returning the current element number. If the block returns ".F.," ASCAN() moves to the next element. If it
moves past the end, it returns a "0," indicating that it didn't find what you wanted.

Here's an example searching an array for the string "Spence," ignoring case:

where = ascan (ar, { | el | upper (el) = "SPENCE"})

The code block returns ".T." when the current element, converted to upper case, is equal to the string constant "SPENCE." Let's
see what ASCAN() does if you pass it the array {"Brown", "spence", "Tollefson"}. It evaluates the code block, passing the first
element, "Brown," as a parameter. The code block calls the parameter "el" (it's just a formal parameter, you can call it "Fred" if
you want), so for this invocation of the code block, "el" contains the string "Brown." It converts this upper case, then compares
it with the string constant "SPENCE." The comparison returns ".F.," so ASCAN() moves to the next element. Then it invokes the
block again, passing the second element, "Spence." Inside the block, "el" is now equal to "Spence." It converts this to upper
case, then compares it with the string constant "SPENCE." This time it returns a ".T.," so ASCAN() stops the search and returns
the current elements number, 2.

The important point is that ASCAN() is traversing the array. It passes the current element as a parameter to the block. Inside
the block you call the element "el." This is similar to what happens when you call a function passing a parameter. You call the
function passing different parameters, but inside the routine you always refer to it by the formal parameter name. Here are
some other ways to search an array using ASCAN()'s code block:

// Exact search ascan (ar, { | el | el == "Spence"})


// Exact search ignoring case ascan (ar, { | el | upper (el) == "SPENCE"})
// "Soft" search ascan (ar, { | el | el >= "SPENCE"})

ASORT() in 5.0

In Summer '87, the ASORT() function sorts an array into ascending order. You can sort dates, numerics, or character strings.
In 5.0 Nantucket extended it in a similar way to ASCAN() You can pass a block indicating how to do the sort. You can specify a
descending sort, a sort that ignores case, or a sort according to string length.

Again, the advantage is that you only have one sort routine; but now you can specify what to sort and how to sort them. You
can parameterize both the data and logic. Here's how it works.

You don't care about ASORT() 's internal sorting algorithm. It may use a quick sort, a shell sort, or a bubble sort. You can't
control this, nor do you want to. All sort algorithms, at their lowest level, compare two things and, if they're out of order, swap
them. What you want to control is how it does the comparison. By default, it does a simple comparison. If the lower element is
greater than the higher element, it swaps them. If you pass ASORT() a code block, you can change that.

When you pass a block, instead of ASORT() doing a simple comparison, your block does it. Whenever ASORT() needs to
compare two elements to determine whether they're in order, it evaluates your block, passing the two elements it's comparing.
The block must return a ".T." if they're in order, ".F." otherwise. This is similar to the way ASCAN() works, except now the block
receives two parameters. Remember, the block doesn't know nor care which elements it receives; that's up to ASORT()'s
sorting algorithm. It knows when the array's in order and therefore when to stop the search. Here are some sample calls to
ASORT():
// Sort into descending order asort (ar,,,{|el1, el2| el1 > el2})
// ascending ignoring case asort (ar,,, {|el1, el2| upper (el1) < upper (el2)})
// ascending string lenghth - shorter string at start asort (ar,,,{|el1, el2| len (el1) < len (el2)})

Multi-dimensional arrays

5.0 lets you create arrays of more than one dimension. You can do this in several ways. When you declare an array, you can
specify the size of each dimension inside square brackets, as in:

LOCAL screen [25] [80]

This is known as the "Pascal style." An alternative, equivalent method, the "C style," specifies each dimension inside square
brackets, as in:

LOCAL screen [25] [80]

These both create a two-dimensional array with 25 rows of 80 columns. You access a particular element by specifying two
indexes, as in:

screen [1, 1] = "R"

Each element can be a different type, as with single-dimensional arrays:

LOCAL names_sales [3, 2]


names_sals [1, 1] = "Brown"
names_sals [1, 2] = 30000
names_sals [2, 1] = "Jones"
names_sals [2, 2] = 25000

If you ask the length of a multi-dimensional array, it returns the number of rows, as in:

? len (screen) // 25

As you'll see, in multi-dimensional arrays, each element is another array.

Two functions return multi-dimensional arrays, DBSTRUCT() and DIRECTORY(). DBSTRUCT() returns a two-
dimensional array containing information about the currently selected database. Its format is similar to the database created
by the COPY STRUCTURE EXTENDED command. The number of rows in the returned array is the number of fields in the
database. There are always four columns. The first contains the field names, the second the field types, the third the field
lengths, the fourth the number of decimal places.

The Nantucket-supplied header file, DBSTRUCT.CH, contains definitions for symbolic constants corresponding to each column of
the array:

// subscripts for field structure array


#define DBS_NAME 1
#define DBS_TYPE 2
#define DBS_LEN 3
#define DBS_DEC 4

You can write a simple routine to list the current database's structure as:

#include "dbstruct.ch"
file_stru = dbstruct ()
FOR i = 1 TO len (file_stru)
? file_stru[ i, DBS_NAME], ;
file_stru[ i, DBS_TYPE],;
file_stru[ i, DBS_LEN ],;
file_stru[ i, DBS_DEC]
NEXT

Elements of multi-dimensional arrays

Each element of a multi-dimensional array is an array itself, and can be treated as such, as in the following:

ar = dbstruct ()
? valtype(ar[ 1 ]) // "A"
? len(ar[1]) // 4

You ask the type of ar[1]. As you know, DBSTRUCT() returns a two-dimensional array, an array of arrays. The type, therefore,
of ar[1] is "A," and its length is 4.

Things get interesting when you pass a row of a two-dimensional array to a subroutine, as in:

#include "dbstruct.ch"
ar = dbstruct ()
FOR i = 1 TO len(ar)
list_fld( ar[ I ] )
NEXT
FUNCTION list_fld( field_stru) // valtype (field _ stru) "A"
// len (field_stru) 4
? field_stru[ DBS_NAME ], field_stru[ DBS_TYPE],;
field_stru[ DBS_LEN ], field_stru[ DBS_DEC]
RETURN NIL
Inside the routine, you only use one index to access an actual element. As you may recall, when you pass arrays as
parameters, you're passing a pointer. Inside LIST_FLD, FIELD_STRU and AR[i] point to the same array. In effect, you're
freezing one of the dimensions. The same applies when you pass arrays to code blocks. As an example, consider the following
call to AEVAL():

#include "dbstruct.ch"

aeval (dbstruct(), ;
{ |field_stru| ;
quot( field_stru [DBS _ NAME],;
field_stru [DBS_TYPE],;
field_stru [DBS_LEN],;
field_stru [DBS_ DEC])})

AEVAL() loops and array, passing each element to your block. In this case, the array is the two-dimensional array returned
from DBSTRUCT(). Each element of this array is another array of four elements, so inside the block, the type of FIELD_STRU is
"A," array, and its length is 4. You can thus use one subscript with FIELD_STRU to refer to an actual elements. The example
lists the structure of the currently selected database.

The DIRECTORY() function returns an array containing information about files. You pass it a file spec, and it returns a two-
dimensional array where each row corresponds to one file. The length of the array is the number of files that match the file
specification, and there are five columns. The first column contains the file name, the second the file size, the third the file's
date, fourth the file's time, and the fifth the file's attributes. These constants are defined in the DIRECTORY.CH header file:

#define F_NAME 1
#define F_SIZE 2
#define F_DATE 3
#define F_TIME 4
#define F_ATTR 5

The following example gets the names of all the files in the current directory, then lists them in order of file name, file size, and
file date and time:

#include "directory.ch"
#define LIST_FILES(files) ;
aeval (files, ;
{|file| qout( file[ F_NAME ],;
file[ F_SIZE ],;
file[ F_DATE ],;
file[ F_TIME ],;
file[ F_ATTR ] )})

files = directory ()

// Sort according to ascending name


asort( files,,, ;
{ | file1, file2 | file1[ F_NAME ] < file2[ F_NAME ] } )

LIST_FILES(files)

// Sort according to size


asort(files ,,, ; {|file1, file2| ;
file1[F_SIZE] < file2[F_SIZE]})

LIST _ FILES(files)

// Sort according to date, time


asort(files,,, ; {|file1, file2| ;
dtos (file1[F_DATE]) + file1[F_TIME] < ;
dtos (file2[F_DATE]) + file2[F_TIME]})

LIST _ FILES(files)

Nested arrays

As I've mentioned, each element in a multi-demensional array is another array, and each is the same size. Actually, this is
just a special case of a more general array implementation. In Summer '87, each array element can be a different type,
except another array. 5.0 has removed this restriction. You can create an array where the first element is a date, the second a
numeric, and the third an array of characters. You can create such an array with:

LOCAL ar [3]
ar[1] = ctod("12/19/58")
ar[2] = 32
ar[3] = array(3)
ar[3, 1] = "..."
ar[3, 2] = "..."
ar[3, 3] = "..."

or with:

LOCAL ar[3]
ar[1] = ctod("12/19/58")
ar[2] = 32
ar[3, 3] = {"...", "...", "..."}
or simply with:

LOCAL ar := { ;
ctod ("12/19/58"), ;
32, ;
{"...", "...", "..."} ;
}

In the last example you'll notice that I split the definition over several lines, I suggest you use one line per row in the array.
You don't have to do this, but it does make the definition easy to read.

The third row is an array of three elements, so you create it directly using a nested {...} definition. If that array contained
another array, you could simply nest another inside the second.

My point is that you can create any data structure you need with Clipper's array. You can use them to simulate other
language's aggregate types, such as C's STRUCT and Pascal's RECORD. The following example shows a structure containing
pull-down menus:

LOCAL p_down1 := { ;
{ "Receivables", { || rec() } },;
{ "Payables", { || pay() } };
}

LOCAL p_down2 := { ;
{ "Report 1", { || rep1() } },;
{ "Report 2", { || rep2() } };
}

LOCAL p_down3 := { ;
{ "Remove deleted recs:, {|| packit() },;
{ "Reorcer Database", {|| reindexit()} } ;
}

LOCAL main_menu := { ;
{ "Accounts", p_down1},;
{ "Reports", p_down2},;
{ "Maintenance", p_down3} ; }

The array MAIN_MENU contains three rows, corresponding to the horizontal prompts of a pull-down menu. The prompts are
"Accounts," "Reports," and "Maintenance." The second column of each array contains either another array or a code block. The
idea is that if it's block, it's evaluated to execute a function. Otherwise, the array specifies a pull-down menu. Note that this
structure can be extended. If, in the second pull-down, you wanted to invoke another pull-down rather than execute the code
block, just create another array and replace the code block with a pointer to that array, as in:

LOCAL ordering := { ;
{ "By date", { || rep2_bydate () } },;
{ "By account", { || rep2_byaccnt() } } ;
}

LOCAL p_down2 := { ;
{ "Report 1", { || rep1() } },;
{ "Report 2", ordering } ;
}

Note that there's only one copy of each array; you just have more than one pointer to it.

A summary

The most important thing to note about 5.0's arrays is that you can use them as a general-purpose data structuring tool. I
showed an example of an extendible pull-down menu declaration. The article also showed how you can use code blocks to
process arrays of any structure. Listing 1, on this month's source code disk and our CompuServe forum, shows how with a
small amount of code, you can browse several databases at the same time. It's the result of objects being implemented as
arrays.

A former member of the Nantucket development team, Rick Spence is the author of Clipper Programming Guide, 2nd
Edition, published by Data Based Solutions, Inc. and Slawson Communications. He's also technical editor for Compass
For Clipper from Island Publishing. He's currently giving Clipper 5.0 seminars throughout the U.S. You can contact
Mr. Spence through his company, Software Design Consultants, at (818) 892-3398, on MCI (LSPENCE), or on
CompuServe (71760,632).

http://www.accessmylibrary.com/coms2/summary_0286-9230353_ITM

You might also like