Generic Record Replace - ReplAll()

Have you ever needed to copy a record from one database to another with the
same structure?  This article will provide you with a function to do just
that.  I will show you the Clipper Summer '87 version as well as the
Clipper 5.01 version.  Along the way I will introduce several new functions
added to 5.01 that allow the REPLALL() function to perform its magic
without using macros.

Background

The REPLALL() function is useful in situations when there is a need to copy
a record from one database to another with the same structure.  Let's
assume that we have two databases containing client records.  The
CLIENT.DBF database contains the active clients.  The INACTIVE.DBF is a
database with an identical structure but contains the inactive clients.  
Before removing the record from CLIENT.DBF we would want to copy the client
record to the inactive database.  This might look something like the
following:

USE CLIENT
USE INACTIVE NEW
APPEND BLANK
REPLACE INACTIVE->LAST WITH CLIENT->LAST
REPLACE INACTIVE->FIRST WITH CLIENT->FIRST
// Continue with as many fields as are defined in the structure
.
.
.
SELECT CLIENT
DELETE

As you can see, the more fields that are defined, the more code is required
to perform the operation.  Worse yet, if the database structure is modified
in any way, the source code would require modification.  The REPLALL()
function allows a generic replace of all fields without concern for
structure and reduces the code required as well.  Here is the same code
using the REPLALL() syntax:

USE CLIENT
USE INACTIVE NEW
APPEND BLANK
REPLALL("CLIENT", "INACTIVE")
SELECT CLIENT
DELETE

The REPLALL() function takes two parameters.  These parameters represent
the source and destination aliases to be worked on.  The following code
lists the Summer '87 version of this function:

Function ReplAll
Parameters cSource, cDest

Private cFldName, n

For n = 1 to FCount()
   cFldName = FieldName(n)
   Replace &cDest.->&cFldName with &cSource.->&cFldName
Next n

Return .T.

The Summer '87 version provides a generic routine accomplishing the task at
the expense of performance (due to the use of macros).  Let's rewrite this
simple function in 5.01 without relying on macro substitution.

Function ReplAll(cSource, cDest)
LOCAL   n,            ;   // counter
        nFieldCnt         // number of fields in source database

nFieldCnt := (cSource)->(FCount())

For n := 1 to nFieldCnt
   (cDest)->(FIELDPUT(n, (cSource)->(FIELDGET(n))))
Next n
return( NIL )

The 5.01 version looks quite a bit different from the Summer '87 version. 
Notice the lack of the REPLACE commands.  The testing that I did proved
that the version without macros ran consistently about four times faster. 
Let's examine some of the new features of Clipper 5.01 that allow this to
happen.

Summer '87 allowed the use of parentheses to signify an indirect reference
in situations where a macro would otherwise be necessary.  The following
example demonstrates this with the USE command:

cFileName := "ACCOUNTS"
USE &cFileName       // Works fine
Close Databases
USE (cFileName)     // Accomplishes the same as above w/o MACRO

This works the same in 5.01 and can be implemented in many other commands
as well.  However, aliases still required macro expansion to work properly
in Summer '87.  Clipper 5 has increased the instances when parentheses may
be used in place of macro substitution, including when referencing database
aliases.

Statements are now permitted such as the following:

cAlias := Alias()
(cAlias)->NAME := "BOB"

Clipper 5.01 has added three very useful functions FIELDPUT(), FIELDGET()
and FIELDPOS().  These functions provide the capability of referencing
fields in any database without knowledge of its structure.  Further, they
eliminate the dependency on macro substitution.  These functions are new to
5.01 and therefore only documented in the Norton Guides.

FIELDPUT() accepts two parameters.  The first parameter is numeric and
specifies the field number in the work area.  The second parameter
represents the value to be assigned to that field.  FIELDGET() works
similarly but only returns the value of the specified field number
requested.  This version of REPLALL() does not make use of the FIELDPOS()
function.   I will implement it in the final version. These functions
perform their work on the currently selected database.

Alias Power

The REPLALL() function does not concern itself with saving and restoring
work areas.  The function does not explicitly select a database as you may
expect using the SELECT command.  REPLALL() is designed so that it does not
matter which work area is active.  Instead, the function relies on the way
aliases are allowed to work with functions.

You can apply an alias to any expression that performs an operation.  This
is done by specifying the alias of the remote work area and the expression
enclosed in parentheses.  For example, to SEEK a value in an un-selected
work area, you would normally execute a series of statements like the
following:

SELECT LookUp
SEEK "BOB"
IF Found()
   <some processing>
ENDIF
SELECT Main

Using the alias expression form, these statements become:

LookUp->(dbSeek("BOB"))
IF LookUp->(Found())
   <some processing>
ENDIF

Actually, the DBSEEK() function returns .T. if the specified key was found. 
So the above code could be written as follows:

IF LookUp->(dbSeek("BOB"))
   <some processing>
ENDIF

The SEEK command gets replaced by its functional equivalent, DBSEEK(). 
This function is documented in the Norton Guides.  This is necessary to
take advantage of aliased expressions.  The SEEK command gets pre-processed
into this function call anyway.  A quick look at STD.CH will confirm this.

The DBSEEK() function has another advantage over the SEEK command.  The
optional second parameter controls whether a soft seek is to be performed. 
This negates the concern for setting/getting the SET SOFTSEEK status.

Alias expressions are a powerful method that can be applied in many other
instances.  They also help eliminate program errors caused by incorrect
work areas selected or restored.  A full article could be dedicated just
for that topic alone (Editor's note:  Look for one in an upcoming
newsletter).

Well, getting back to the REPLALL() function...

If it weren't for this feature, the 5.01 code might look something like
this:

Function ReplAll(cSource, cDest)
LOCAL n, nCurrSelect, nFieldCnt, TempVal

nCurrSelect := Select()   // Save current work area number
Select (cSource)          // Select the source database
nFieldCnt := FCount()     // Save number of fields in source database

For n := 1 to nFieldCnt
   Select (cSource)       // Select the source database to retrieve from
   TempVal := FIELDGET(n) // Save field value in temporary variable
   Select (cDest)         // Select destination database
   FIELDPUT(n, TempVal)   // Put value in matching destination field
Next n

Select (nCurrSelect)      // Select work area active prior to function call
return( NIL )

As you can see, this approach requires more code and variables.  However,
even though  more steps are required, this version is easier to understand.

Error Checking

The function as written does not provide for any degree of error checking. 
If any field in the source database is different from the corresponding
destination database field, a run-time error would occur.  Code to test for
this situation could be added without too much difficulty.  All that is
needed is a mechanism that allows for a replace with like fields only.

The DBSTRUCT() function returns a two dimensional array with each element
containing information for one field.  I will use two local variables
containing an array for both the source and the destination databases. 
Then I will compare each elements name, type, length and decimals.  If the
comparison passes, I will proceed with the replace.  The REPLALL() function
with error checking looks like this:

#include "dbstruct.ch"

Function ReplAll(cSource, cDest)
LOCAL   n,            ;   // counter
        aSourceDBS,   ;   // array for source database structure
        aDestDBS,     ;   // array for destination database structure
        nFieldCnt         // number of fields in source database

nFieldCnt  := (cSource)->(FCount())
aSourceDBS := (cSource)->(dbStruct())
aDestDBS   := (cDest)->(dbStruct())

For n := 1 to nFieldCnt
   IF n <= Len(aDestDBS) .and. ;
         aSourceDBS[n, DBS_NAME] == aDestDBS[n, DBS_NAME] .and. ;
         aSourceDBS[n, DBS_TYPE] == aDestDBS[n, DBS_TYPE] .and. ;
         aSourceDBS[n, DBS_LEN]  == aDestDBS[n, DBS_LEN] .and. ;
         aSourceDBS[n, DBS_DEC]  == aDestDBS[n, DBS_DEC]

      (cDest)->(FIELDPUT(n, (cSource)->(FIELDGET(n))))
   ENDIF
Next n
return( NIL )

The REPLALL() function would be best if it also did not require the
position of the fields to match to work properly.  Consider the following
two database structures:

Structure for database : NAME.DBF
Number of data records : 2
Date of last update    : 02/01/92
Field   Field Name   Type        Width      Dec
   1      LNAME      Character      20
   2      FNAME      Character      15
   3      GENDER     Character       1
   4      AGE        Numeric         3        0

Structure for database : INACTIVE.DBF
Number of data records : 2
Date of last update    : 02/01/92
Field   Field Name   Type        Width      Dec
   1      GENDER     Character       1
   2      AGE        Numeric         3        0
   3      LNAME      Character      20
   4      FNAME      Character      15

Although the program would not crash given this situation, the REPLALL()
would not perform any replaces.  This is not acceptable, so some
modification is in order.  Remember the FIELDPOS() function I mentioned
earlier?  I will now put it to good use to resolve this problem. 
FIELDPOS() returns the position of a field name in a work area.  FIELDPOS()
expects a character parameter representing the name of the field in the
work area sought.

REPLALL() with the FIELDPOS() function now looks like the following:

#include "dbstruct.ch"

Function ReplAll(cSource, cDest)
LOCAL   n,               ;   // counter
        aSourceDBS,      ;   // array for source database structure
        aDestDBS,        ;   // array for destination database structure
            nDestSubscr, ;   // field number for destination database
            nFieldCnt        // number of fields in source database

nFieldCnt  := (cSource)->(FCount())
aSourceDBS := (cSource)->(dbStruct())
aDestDBS   := (cDest)->(dbStruct())

For n := 1 to nFieldCnt
   nDestSubscr := (cDest)->(FieldPos(aSourceDBS[n, DBS_NAME]))
   IF nDestSubscr <> 0 .and. ;
         aSourceDBS[n, DBS_TYPE] == aDestDBS[nDestSubscr, DBS_TYPE] .and. ;
         aSourceDBS[n, DBS_LEN]  == aDestDBS[nDestSubscr, DBS_LEN] .and. ;
         aSourceDBS[n, DBS_DEC]  == aDestDBS[nDestSubscr, DBS_DEC]

      (cDest)->(FIELDPUT(nDestSubscr, (cSource)->(FIELDGET(n))))
   ENDIF
Next n
return( NIL )

This is about as far as I think REPLALL() should be taken.  It provides for
a generic field replace with matching field error checking capabilities. 
It is also not concerned with the field order matching between the source
and destination databases.  All this, and without requiring one macro!

About the Author

John Agusta is president of S & A PC Solutions, Inc., a personal computer
consulting firm in Long Island, New York.  He specializes in Clipper
application development.  He also serves as Manager, Systems and
Development for Market Guide Inc., a financial database publisher also in
L.I.