1  Read Me First

These articles are reprinted from the February '91 edition of
TechNotes/dBASE IV.  Due to the limitations of this media, certain graphic
elements such as screen shots, illustrations and some tables have been
omitted.  Where possible, reference to such items has been deleted.  As a
result, continuity may be compromised.  

TechNotes is a monthly publication from the Ashton-Tate Software Support
Center.  For subscription information, call 800-545-9364.



2  UDF Library

UDF LIBRARY
It's a Setup
Roland Bouchereau and Dan Madoni

 UDFs provide a steady stream of new functionality, enhancing the dBASE
language
and providing an open-end architecture to the software.

Individual Color Attribute Functions
The SET() function received a boost in version 1.1 of dBASE IV.  Objects in
use such as procedure files and format files can now be determined as well
as a few others.  The SET() function now supports the following parameter
values:

ATTRIBUTES
DEFAULT
DIRECTORY
FILTER
PATH
PROCEDURE
VIEW

Using SET("ATTRIBUTES") returns a character string indicating the color
attributes for all the color area groups.  However, all of the colors are
returned in a string together, making it difficult to separate the color
patterns for individual color areas.  The UDFs shown below ("SET UDFs") can
address each individual area.  If you only find yourself dealing with one
or two areas where color change is required, these may come in handy.

Set UDFs
FUNCTION Normal                   && COLOR OF NORMAL
  PRIVATE normal_
  normal_ = SET("ATTRIBUTES")
RETURN LEFT(normal_,AT(",",normal_) - 1)


FUNCTION Highlight                && COLOR OF HIGHLIGHT
  PRIVATE hilite_
  hilite_ = SET("ATTRIBUTES")
  hilite_ = SUBSTR(hilite_,AT(",",hilite_) + 1)
RETURN LEFT(hilite_,AT(",",hilite_) - 1)


FUNCTION Border                   && Border color
  PRIVATE border_
  border_ = SET("ATTRIBUTES")
RETURN SUBSTR(border_,AT(" &",border_) - 3, 1)


FUNCTION Messages                 && COLOR OF MESSAGES
  PRIVATE messages_
  messages_ = SET("ATTRIBUTES")
  messages_ = SUBSTR(messages_,AT("& ",messages_) + 2)
RETURN LEFT(messages_,AT(",",messages_) - 1)


FUNCTION Titles                   && COLOR OF TITLES
  PRIVATE titles_
  titles_ = SET("ATTRIBUTES")
  titles_ = SUBSTR(titles_,AT("& ",titles_))
  titles_ = SUBSTR(titles_,AT(",",titles_) + 1)
RETURN LEFT(titles_,AT(",",titles_) - 1)


FUNCTION Box                      && COLOR OF BOX
  PRIVATE box_, i_
  box_ = SET("ATTRIBUTES")
  i_ = 0
  DO WHILE i_ # 4
    box_ = SUBSTR(box_,AT(",",box_) + 1)
    i_ = i_ + 1
  ENDDO
RETURN LEFT(box_,AT(",",box_) - 1)


FUNCTION Info                     && COLOR OF INFORMATION
  PRIVATE info_, i_
  info_ = SET("ATTRIBUTES")
  i_ = 0
  DO WHILE i_ # 5
    info_ = SUBSTR(info_,AT(",",info_) + 1)
    i_ = i_ + 1
  ENDDO
RETURN LEFT(info_,AT(",",info_) - 1)


FUNCTION Fields                   && COLOR OF FIELDS
  PRIVATE fields_
  fields_ = RIGHT(SET("ATTRIBUTES"),7)
RETURN RIGHT(fields_,7 - AT(",",fields_))

Any Identifying Marks?

To help compensate further for those settings that the SET() function can
not reveal, namely SET CURRENCY, SET CURRENCY TO, SET POINT TO, SET
SEPARATOR, and SET DATE.  The following four UDFs make use of another
powerful built-in dBASE function.  The TRANSFORM() function allows the
programmer to format any datum using a character string that could specify
a template for a PICTURE clause.  For example:

. ? TRANSFORM(-123.45,"@(") 
(       123.45)"

TRANSFORM() can therefore be used to determine the settings for SET
CURRENCY, SET CURRENCY TO, SET POINT, and SET SEPARATOR.  The UDFs
CurrIs(), CurrPos(), PointIs(), and Separator(), respectively, return these
values . 

Transform UDFs
FUNCTION CurrPos
RETURN IIF(LEN(TRANSFORM(9,"@$")) = 10,"LEFT","RIGHT")


FUNCTION Currency
  PRIVATE pos_,currstr_
  pos_ = LEN(TRANSFORM(9,"@$")) = 10
  SET CURRENCY RIGHT
  currstr_ = SUBSTR(TRANSFORM(9,"@$"),11)
  IF pos_
    SET CURRENCY LEFT
  ENDIF
RETURN currstr_


FUNCTION PointIs
RETURN LEFT(TRANSFORM(.9,".9"),1


FUNCTION Separator 
RETURN SUBSTR(TRANSFORM(9999,"9,999"),2,1)


The SET DATE setting is another condition that can not be determined
through the use of the SET() function.  This does not make it easy on the
programmer should support for multiple date formats be necessary.  A good
point, though, is that all the styles conform to three basic styles only
differing by the particular separator used.  

Date orders are either year, month and day (YMD), day, month and year (DMY)
or month, day and year (MDY).  With that in mind, you only really need two
functions to determine the proper appearance for dates.  One to determine
the current SET MARK setting, the other to determine the appropriate
order.  MarkIs() and DateForm() perform these respective functions .

Date UDFs
FUNCTION MarkIs
  PRIVATE cent_, its_
  cent_ = SET("CENTURY") = "ON"
  SET CENTURY OFF
  its_ = SUBSTR(DTOC(DATE()),3,1)
  IF cent_
    SET CENTURY ON
  ENDIF
RETURN its_


FUNCTION DateForm
  PRIVATE today_, temp_, first_
  today_ = DATE()
  IF MONTH(today_) = DAY(today_)  && To make them different
    today_ = today_ + 1
  ENDIF

  temp_ = SET("CENTURY") = "ON"
  SET CENTURY ON
  first_ = INT(VAL(DTOC(today_)))
  IF .NOT. temp_
    SET CENTURY OFF
  ENDIF

  DO CASE
    CASE first_ = YEAR(today_)
      temp_ = "YMD"
    CASE first_ = MONTH(today_)
      temp_ = "MDY"
    CASE first_ = DAY(today_)
      temp_ = "DMY"
  ENDCASE
RETURN temp_
 
A File By Any Other Name.

In its wide variety of useful and practical commands, dBASE IV includes the
ability to store an existing filename into a variable using 

DEFINE POPUP <popup name>PROMPT FILES LIKE

The only catch to this is that you are forced to halt execution of your
program and allow the user to select the desired file.

There are a few instances where this will present a problem.  In one of my
applications, the program must check several databases for the presence of
a certain character string knowing only that the databases it must search
have a .DBF extension.

With a file extension as a parameter, this UDF, AFiles() (see "AFiles"),
returns the number of files that exist with the given extension.  In the
process, AFiles() fills an array with the applicable file names.  

Suppose you have a directory containing the following files:

MAIN.PRG  CUSTOMER.DBF  AFILES.LOD
CUST.PRG  TEST.PRG      SAMPLE.DBF    

Given the following command, the variable fnum would equal 3 because there
are three files in the directory with a .PRG extension.  

fnum = AFiles("PRG")

If you did a DISPLAY MEMORY, you would also notice that the following is
present in memory:

AFILES[1,1] = "MAIN.PRG"
AFILES[2,1] = "CUST.PRG"
AFILES[3,1] = "TEST.PRG"

With this information, your program could now perform a loop operation to
do the necessary processing for each file.  

-----------------------------------------------------
AFiles
To use AFiles(), you must first create a database file called AFILES.LOD
and define the following field therein:

Field Name     Field Type     Field Length     Decimal     Index
FILE           Character      50               0           N



FUNCTION AFiles                  
  PARAMETERS afext 
                
  afext = UPPER(afext)             
  IF .NOT. FILE("AFiles.lod")      
      RETURN 0                      
  ENDIF 
                             
  RUN DIR *.* >AFiles.txt          
  USE AFiles.lod                   
  APPEND FROM AFiles.txt TYPE SDF                           
  IF RECCOUNT() = 0                
    RETURN 0                      
  ENDIF                             

  SORT ON FILE TO AFTemp.$vd     
  ZAP                              
  APPEND FROM AFTemp.$vd         
  ERASE AFTemp.$vd               
  ERASE Afiles.txt 

  GO TOP                           
  DELETE FOR .NOT. SUBSTR(File,10,3) = afext   
  PACK                                                          
  REPLACE ALL FILE WITH RTRIM(SUBSTR(File,1,8)) + "." +
RTRIM(afext)                               

  ANumFils = RECCOUNT()            
  RELEASE AFiles 
                  
  PUBLIC ARRAY AFiles[ANumFils,1]  
  COPY TO ARRAY AFiles             
  ZAP                              
  USE                                                             
  afclrcntr = 0                    

  DO WHILE afclrcntr < ANumFils    
      afclrcntr = afclrcntr + 1     
      afiles[afclrcntr,1] = RTRIM(AFiles[afclrcntr,1])   
  ENDDO                                                            
RETURN ANumFils 


3  Q&A 

Q&A

File Under BUILD

Q:  I am attempting to use BUILD to create RunTime applications.  I am
having difficulty determining how to copy certain .PR2 printer driver files
along with my application.  How is this done?

A:  BUILD does not copy printer driver files.  This file type is not
included under the Options: Include file types submenu.  It is assumed that
in distributing applications, the destination hardware setups would likely
vary to such a degree as to make program references to particular printer
drivers impractical.  

It is anticipated that either the developer will set up the printer
configuration or the developer's installation routine/batch file will allow
for printer definition.  In either case, this will be done at install time
by extracting the needed driver or drivers from the DRIVERS.EXE file.

"I Know You're In There"

Q:  I have a command line in a program I use that searches memos for
certain character strings.  I use the expression:

LOCATE FOR stringexp $ memofld

However, in some cases, it doesn't seem to work.  The program will return a
"Not found" condition, yet I can go into the memo and see that the string
is there.   Why does this chronically occur?

A:  If your memos are more than 254 characters, this method would work
fine.  However, the $ operator, originally designed to search only
character fields, is limited when it comes to a more substantial memo
field.  Try using this expression instead:

LOCATE FOR AT(stringexp,memofld) <> 0


No Need to Rub It In

Q:  I'm using a format file to handle my data entry but, in one case, I am
annoyed with a message that pops up in a field to which I've assigned an
"Accept value when" condition.  The message is "Editing condition not
satisfied (Press SPACE)".  Is there anyway to eliminate this.  I don't want
another message of my own but neither do I want this one.  Call me picky
but I'd really like to know if there's a workaround.

A:  By entering two quotes ("") in the Edit options: Unaccepted message
expression area, you can eliminate all but the Press SPACE portion of that
message.  If that is still unsatisfactory, you can use ON READERROR in your
program or setup that can either sound the bell, initiate a UDF or simply
do nothing.

To sound the bell after an invalid entry,

ON READERROR ?? CHR97)

To activate a UDF:

ON READERROR ?? UDFname()

To do nothing after an invalid entry,

ON READERROR ?? ""

Today At the Touch of a Button

Q:  How do I program a function key to automatically enter the current
system date into a field?

A:  That depends on where you are when you press the function key.  In an
@..GET scenario, where you've actually programmed the input of information
into a variable or field, you can use

SET FUNCTION Fn TO SUBSTR(DTOC(DATE()), 1, 2) + SUBSTR(DTOC(DATE()), 4, 2)
+ SUBSTR(DTOC(DATE()), 7, 2)

where n is a number 1 to 10.

However, in a full screen command such as EDIT or BROWSE, the function keys
are disabled from full user-programmed control.  If this is where you plan
to use your function key, include the following in your setup program:

ON KEY LABEL F7 KEYBOARD SUBSTR(DTOC(DATE()), 1, 2) + SUBSTR(DTOC(DATE()),
4, 2) + SUBSTR(DTOC(DATE()), 7, 2)


Little Sir ECHO

Q:  The Label generator is giving me problems in my 1.1 version of dBASE
IV.  After going through all the motions of creating a label, then typing 

LABEL FORM <Filename> TO PRINT

all I get is a screen full of commands and instructions.  It is as if I had
TYPEd the code (.LBG) file.  

A:  Somehow a dBASE switch that is normally OFF has been set ON.  This
switch, called ECHO, is responsible for the printed instructions you see. 
It's useful for when programmers need to debug their program listings while
the program is running.  It isn't 
very useful for printing labels.

At the dot prompt, type 

SET ECHO OFF

prior to your LABEL FORM command.  To make this change permanent, enter a
line in your CONFIG.DB file that says

ECHO = OFF

To access the CONFIG.DB, enter MODIFY COMMAND CONFIG.DB at the dot prompt. 
After making the change, enter Ctrl-End to save.


PostScript Modification

Q:  I am using your PostScript driver and all was originally okay.  Per the
instructions in Getting Started, page 2-11, I made a small modification to
adjust how the text was centered.  Afterwhich, I could not make anything
print at all with the PostScript driver.  What am I doing wrong?

A:  You may not have done anything.  Depending upon how you made the
change, we have found that some text editors leave an EOF (end of file)
marker at the end of the .DLD file.  This keeps any output following the
POSTSCRI.DLD file from printing.  A way to remove the EOF marker after
mofidying the .DLD file is to do the following trick in DOS:

TYPE POSTSCRI.DLD>TEMP
TYPE TEMP>POSTSCRI.DLD

This little turnabout will eliminate the unwanted marker and allow the
driver to work properly again.


Wide Open Spaces For RunTime

Q:  As I attempted to install RunTime on my hard disk, I noticed that I
received "File not found" errors after inserting Disks #2 and #3.  However,
the installation continued and finished, apparently successfully.  But
RunTime didn't work.  All the files appeared to be there.  Do I have bad
disks?

A:  Probably not.  This situation will typically occur when attempting to
install RunTime without enough free hard disk space.  RunTime needs at
least 2.5 MB to install.  If you'll notice on your present installation,
the RunTime.OVL file (a whopper of a file under normal circumstances) is
unusually small.

The No Match Game

Q:  I'm designing a query and I'd like to be able to find all records in a
parent file that do not have a match in the related child file.  I cannot
quite figure out the proper way of accomplishing this.

A:  There is a way.  First, we'll name our cast.  The parent file we'll
call MASTER and the child file we'll call TRANS.  The linking field will be
refered to as CODE.  
In the CODE field of the MASTER file, enter

EVERY LINK1

In the child file, TRANS, also under the CODE field, enter

LINK1

Here's the trick to make it work.  Add a condition box, and place within it
the expression:

MASTER->CODE <> TRANS->CODE

This will result in a list of records from MASTER which have no match in
TRANS.   



4  Programming Printer Selection

Programming Printer Selection

Steve Koterski

Here is a short, modular routine to allow users of an application 
to set up or change the setup of their printer from within an application. 

 Thanks to the new self-contained file, DRIVERS.EXE, obtaining a printer
driver is a snap.  This enhancement to version 1.1 of dBASE IV  made it
pretty natural to develop a driver installation utility program, presented
in the next few pages.   It uses printer driver information found in the
dBASE IV Language Reference, and the compressed printer driver file,
DRIVERS.EXE.  The default print settings window design is a variation of
that found in the  manual, page 13-25.

The essence of the design of this module is transportability.  I use this
module in a number of applications, just plugging it into an existing menu
system.  It also lends itself well to inclusion in an Applications
Generator program.  Aside from the few dBASE IV specific operations, with a
little modification, it could even be transported to a dBASE III PLUS
application.  

Modularity

The concept of modular programming is important for the developer who needs
boilerplate routines that are common to a range of different applications. 
It also gains importance with the use of the greatly improved Applications
Generator in dBASE IV, where modules can easily be mixed and matched, and
common routines transported to various menu systems.

Modular programming involves program design that incorporates virtually
stand-alone code, independent of any particular application.  The program
design depends only on a parent application to run under.  Useful routines
such as printer configuration, file backup, and screen background design
lend themselves especially well to modular programming.  Programs that are
limited in application such as data entry, ad-hoc queries, and other
application or data specific operations are less adaptable.  A routine that
uses modular design will be one that can be "plugged into" almost any
application, with little to no modification of the code being necessary. 
It will also perform a generic function needed by nearly any application.

A Useful Example

Here now is an example of a program that transports easily into several
applications.  The purpose of this program is to provide an interface
similar to the printer selection menus found in the dBASE installation
program.  

There are four files used with this module: the database and index,
DRIVERS.DBF and DRIVERS.MDX; the memory file, ATTRIB.MEM; and the program
file itself, PRINTERS.PRG.

The data for the printer driver database comes directly from  Language
Reference, Appendix F.  The structure of the database takes into
consideration the long text of some entries in the Manufacturer and Model
columns, and the Driver field includes the printer driver file extension
(.PR2):

Field Type  Width
BRAND   C     30
MODEL   C     40
DRIVER  C     12

Two index tags are used, BRAND and TEMP.  The BRAND TAG orders the database
by manufacturer, suppressing duplicate manufacturer names.  This is
necessary for showing only one occurrence of each manufacturer name in the
Brand popup picklist.  The second tag, TEMP, is a temporary tag, created on
the fly in the module, limiting the printer models displayed in the Model
picklist by matching the Brand field entries with the manufacturer selected
from the Brand picklist.  This is accomplished with the FOR clause:

  INDEX ON brand + model FOR brand = mbrand

The variable mbrand stores the manufacturer name selected from the Brand
picklist.  The TEMP tag is deleted from the MDX file prior to the
termination of the module.

A dBASE memory file, named Attrib.MEM, is also necessary.  In this file are
stored the default print settings for the application the module is used
in.  These settings take the form of memory variables, each corresponding
to a dBASE IV system memory variable.  The memory file is invoked once
during the module to allow the user to modify the default settings.  

The internal structuring of the main routine, Printers.PRG, is designed to
be as modular as possible.  The routine is sectioned off into partitions of
like operations.  This not only helps in design, but also in debugging and
future modification.  As we all know, without this internal structuring,
the program we find so familiar as we write the code can become virtually
unintelligible six months to a year later.

The actual code in Printers.PRG is intended to be as simple and
straight-forward as possible.  The only real exception to this is the
program flow during the activation and deactivation of the popup
picklists.  A consistent, meaningful naming convention for the memory
variables used in the routine facilitates easy control and release.  The
code was broken down into sections of unique operations wherever possible.

The first section establishes the small requirements needed by the module. 
It takes advantage of the dBASE IV function SET() to save original
environment settings that, if left unchanged, could adversely affect the
parent application.  The initialization of the module memory variables is
consolidated here to make identification and tracking of these variables
easier.  The printer driver database is opened here in work area ten. 
Since most applications utilize work areas from the lower numbered ones to
the higher, ten was chosen.  If select area ten is used by a parent
application, this will require modification.

The next section defines and activates the popup picklist Brand.  This
picklist displays the Manufacturer names from the Brand field.  The Brand
field, by necessity, has a duplicate Brand value for each Model made by a
different Manufacturer.  The display of duplicate entries is suppressed for
the popup using the unique index tag BRAND.  The selection of a printer
Brand is stored to the memory variable mbrand.

If there is more than one model for a particular brand, program flow skips
to the Model procedure to define and activate the Model picklist.  The
order is changed to limit the Models displayed with the index tag TEMP.  It
is generated here based on the value stored previously in mbrand.  If there
is only one model for a brand, that model is selected.

At this point the program flow can branch to one of three directions
dependent on user action.  If the user aborts by pressing Escape, control
returns to the Brand picklist, where another abort terminates the routine
or another Brand may be selected.  The second and third directions depend
on whether the user elects to "keep" the Model, or select a new one.  If
the user accepts the choice made, both picklists are deactivated and flow
proceeds to the driver file extraction.  If the user declines the selection
of Model, control returns to the Model picklist.  The content of the Driver
field corresponding to the selected Model is stored to the memory variable
zdriver with the LOOKUP() function.

The Drivers File & DOS Shelling

Once a Model is selected, the routine proceeds to two sequential
operations: the extraction of the printer driver file from the compressed
file DRIVERS.EXE, and the modification of the default print settings.

The DRIVERS.EXE file is a self-extracting depository of all the printer
driver files, compressed into one file.  Ordinarily, DRIVERS.EXE is used
only by the DBSETUP program.  But, it is possible to extract one or more
files with the appropriate DOS command.  The program does this by shelling
out to DOS from dBASE IV with the commands:

RUN DEL *.PR2 > NUL
RUN drivers &zdriver > NUL

The first of these commands deletes any existing driver files.  This
prevents the accumulation of driver files from running this module
frequently.  It is also necessary because the extraction prompts the user
when an existing file is to be overwritten by the extracted file, and it is
not possible to override or suppress this prompt from within dBASE IV.  The
second command executes the actual extraction, naming the file to be
extracted with the zdriver variable.  The combination of setting CONSOLE
off and redirecting the screen output to NUL suppress these operations from
the screen.

The next operation provides, in a window, entry fields to modify the
variables stored in the memory file.  Simple @.SAYs and @.GETs return the
new or unchanged values, and the variable contents are then stored to the
dBASE system variables so the module variables can be released.  The
variables then get resaved to the memory file. 

Closing Environment

The last step in the program flow is the Closing environment section.  Here
the external related settings are restored to the values saved in the
Opening environment section, memory variables specific to the module are
released, and the module terminates.

Program & System Memory Variables

The naming convention used for the memory variables in this module clearly
associates each variable with its destination system variable, and allows
for easy control and release.  The variables fall into two groups: routine
specific and printer specific.  Each of these two groups can be released as
a unit using the common first letter of the variable name  "z" for routine
and "p" for printer variables  and the ALL LIKE clause of the RELEASE
command.

CNTR()

The CNTR() User Defined Function (UDF), is incidental, the likes of which
you've probably encountered before.  Its sole purpose is the centering on
screen of messages of variable length, specifically those containing the
printer driver file name.  It is, itself, a portable module, but can be
limited severely depending on screen design.  It works only with text
displayed with @..SAY commands, those containing @..GET clauses require
literals for positioning.

Considerations

Certain conditions must be taken into consideration when using this module:

1.  The design presumes an application exists in a DOS directory of its own
and not in the dBASE directory.  The routine deletes all printer driver
files in the current directory.

2.  It will be necessary for the parent application to RESTORE the print
settings in the memory file and store the variable contents to the dBASE
system variables for the settings to have any effect in print sessions. 
Unless this is done, the settings will be put into the system variables
only after this module is run.

3.  The routine presumes the parent application uses no memory variables
that begin with "z" or "p".  Such variables are released en masse by the
routine with the ALL LIKE clause of the RELEASE command.

4.  One driver was deliberately left out of the module: the PostScript
driver.  This is because the added considerations associated with this
driver are beyond the scope of this module.

5.  The coordinates of the messages presume STATUS is set off.

6.  No consideration is given in the module to interactive help or printer
status.  It is presumed the parent application will have a help module (if
desired) and an error routine (strongly suggested).

Memory File

Initial contents of the memory file ATTRIB.MEM:


PPDRIVER    pub   C  " "
PPQUALITY   pub   L  .F.
PPPITCH     pub   C  "DEFAULT"
PPWAIT      pub   L  .F.
PPEJECT     pub   C  "NONE  "
PPCOPIES    pub   N           1  (1.000000000000000000)
PPEPAGE     pub   N        9999  (9999.000000000000000)
PPBPAGE     pub   N           1  (1.000000000000000000)
PPSPACING   pub   N           1  (1.000000000000000000)
PPLENGTH    pub   N          66  (66.00000000000000000)
PINDENT     pub   N           0  (0.000000000000000000)
PRMARGIN    pub   N          79  (79.00000000000000000)
PLMARGIN    pub   N           0  (0.000000000000000000)
PLOFFSET    pub   N           0  (0.000000000000000000)
PPORT       pub   C  "LPT1"

-------------------------------------------------------------------
PICKPRNT.PRG
*  Opening environment.
STORE SET("SAFETY") TO zsafety
STORE SET("CURSOR") TO zcursor
SET SAFETY OFF
PUBLIC zans
STORE "N" TO zans
STORE SPACE(4) TO pport
STORE " " TO zbrand,zmodel,zdriver
STORE  pdriver TO molddrvr
SELECT 10
USE drivers ORDER brand

*  Display picklist of printer brands.
DEFINE POPUP brand FROM 5,24 TO 17,56 PROMPT FIELD brand
ON SELECTION POPUP brand DO model
ACTIVATE POPUP brand

*  If new printer is selected, update configuration file.
IF zans = "Y"
  SET CURSOR OFF
  SET CONSOLE OFF
  RUN DEL *.PR2 > NUL
  RUN drivers &zdriver > NUL
  SET CONSOLE ON
  @ 22,0 SAY CNTR("Printer driver '" + zdriver + "' successfully
installed")
  @ 23,0 SAY CNTR("Press a key to continue")
  WAIT ""
  @ 22,0 CLEAR TO 23,79
ENDIF

*  Set printer defaults.
RESTORE FROM attrib ADDITIVE
DEFINE WINDOW printers FROM 5,12 TO 17,69
SET CURSOR ON

IF zans = "Y"
  @ 22,20 SAY "Enter printer port for this driver: " GET pport ;
    PICTURE "@M LPT1, LPT2, LPT3, COM1, COM2, COM3"
  READ
  @ 22,0 CLEAR TO 22,79
ENDIF

ACTIVATE WINDOW printers

@  0,20 SAY "PRINTER DEFAULTS"

@  2, 1 SAY "PAGE SETTINGS"
@  3, 1 SAY REPLICATE("_",20)
@  4, 1 SAY "Offset from left "  GET ploffset  PICTURE "99"
@  5, 1 SAY "Left Margin..... "  GET plmargin  PICTURE "99"
@  6, 1 SAY "Right margin...."   GET prmargin  PICTURE "999"
@  7, 1 SAY "Indentation..... "  GET pindent   PICTURE "99"
@  8, 1 SAY "Page Length....."   GET pplength  PICTURE "999"
@  9, 1 SAY "Spacing.........  " GET ppspacing PICTURE "9"

@  2,22 SAY "PRINT SETTINGS"
@  3,22 SAY REPLICATE("_",27)
@  4,22 SAY "Begin printing on page." GET ppbpage   PICTURE "9999"
@  5,22 SAY "End printing on page..." GET ppepage   PICTURE "9999"
@  6,22 SAY "Number of copies......." GET ppcopies  PICTURE "99"
@  7,22 SAY "Eject page............." GET ppeject  ;
        PICTURE "@M BEFORE, AFTER, BOTH, NONE"
@  8,22 SAY "Wait between pages....." GET ppwait    &&PICTURE "N"
@  9,22 SAY "Text pitch............." GET pppitch ;
        PICTURE "@M DEFAULT, PICA, ELITE, CONDENSED"
@ 10,22 SAY "Quality print.........." GET ppquality &&PICTURE "N"
READ

*  Set system memory variables.
 ploffset = ploffset
 lmargin = plmargin
 rmargin = prmargin
 indent = pindent
 plength = pplength
 pspacing = ppspacing
 pbpage = ppbpage
 pcopies = ppcopies
 peject = ppeject
 pwait = ppwait
 ppitch = pppitch
 pquality = ppquality

IF zans = "Y" .AND. zdriver > " "
   pdriver = zdriver
ENDIF

ppdriver = zdriver
SET PRINTER TO &pport
SAVE ALL LIKE p* TO attrib
DEACTIVATE WINDOW printers

*  Closing environment.
SET CURSOR &zcursor
SET SAFETY &zsafety
CLEAR
RELEASE ALL LIKE p*
RELEASE ALL LIKE z*
RELEASE POPUP brand,model
RELEASE WINDOW printers
RETURN


PROCEDURE Model
  *  Display picklist of printer models from selected brand.

  IF LASTKEY() <> 27
    zbrand = PROMPT()
    INDEX ON brand+model FOR brand=zbrand TAG temp 
    SET ORDER TO temp
    COUNT FOR brand = zbrand TO zcount
    break=1

    IF zcount > 1
      DEFINE POPUP model FROM 5,19 TO 17,61 PROMPT FIELD model
      ON SELECTION POPUP model DO choice
      ACTIVATE POPUP model
      DELETE TAG temp
      SET ORDER TO brand

      IF zans = "Y"
        DEACTIVATE POPUP
      ENDIF

    ELSE
      zbrand = PROMPT()
      zdriver = TRIM(UPPER(LOOKUP(driver,zbrand,brand))) 
      zmodel = TRIM(LOOKUP(model,zbrand,brand))
      zans = " "

      DO WHILE .NOT. zans $ "YN"
        zans = "N"
        @ 22,0 SAY CNTR("You have selected: " + zmodel)
        @ 23,28 SAY "Is this correct? (Y/n)" get zans picture "!"
        READ
        @ 20,0 CLEAR TO 22,79
      ENDDO

      IF zans = "Y"
        DEACTIVATE POPUP
      ELSE
        SET ORDER TO brand
      ENDIF
    ENDIF
  ENDIF
RETURN


PROCEDURE Choice
  *  Store driver name from selected printer to memory variable.

  zmodel = TRIM(PROMPT())
  zdriver = TRIM(UPPER(LOOKUP(driver,zmodel,model))) 
  zans = " "

  DO WHILE .NOT. zans $ "YN"
    zans = "N"
    @ 22,0 SAY CNTR("You have selected: " + zmodel)
    @ 23,28 SAY "Is this correct? (Y/n)" get zans picture "!"
    READ
  ENDDO

  @ 20,0 CLEAR TO 22,79
  DEACTIVATE POPUP
RETURN  


FUNCTION Cntr
*  Center on screen character strings of variable length.

  PARAMETER string
  string2 = SPACE((80 - LEN(string)) / 2) + string
RETURN string2

*   EOF PICKPRNT.PRG




5  Warming Up to Keyboard Macros

Warming Up to Keyboard Macros
Joe Stolz

Most people's lives, and especially their daily computer activities, are a
specific set routine of procedures.  That's what causes our work-day lives
to become a bit stale.  The day in and day out routine leads us to feel
that we are forever stuck in a rut and unproductive.  It's ironic that,
when it comes to our computer work, if we could just do all the required
steps exactly the same way each time, we would get the same results each
time, life would be great.  We'd get more done in less time, look like
techno-whizzes to our superiors and feel better about the work we produce. 


But, depending upon our  mood, the weather,  how much sleep we got, how
much training we were given, the amount of nurturing we  may have received
as children and a roster of other factors, it's sometimes difficult to
remember the exact steps that are required, for example, for a particular
report.  Our Support Technicians are commonly asked to aid someone in
recreating the circumstances required to produce a particular report.  If
only we all had photographic memories, then we could recall all the
requisite steps involved in the billing statement that was due last week.  

The most common way to handle repetitive tasks using dBASE has always been
to write a program.  Programs are swell, but not everyone feels competent
enough to plan out, write and debug a dBASE program.  So a user-friendly
microcosm of the programming concept was built into dBASE IV.  The new way
to handle these tasks is through a keyboard macro!

In the simplest cases, macros record every keystroke that you make during a
recording session.  You can edit the list of keystrokes to modify or delete
any mistakes made during the recording process.  Then you can simply play
the macro when you need to perform your task.  You must save your macros to
a library file stored on disk for use later, and you must load your library
prior to running your macros.

Using the Menu vs. Shift-F10

There are two main ways to record macros; from the Control Center and from
the dot prompt.  From the Control Center, you record macros by accessing
the Tools menu.  The first option in Tools: Macros is Begin recording. 
When you start to record a macro, you must first assign the macro a "hot
key".  This is basically the key that you press to begin playing out the
keystrokes of your macro.  

You will see a table of keystrokes available to be assigned to your macro
once you begin a recording session.  Your macros are easily assigned to a
function key: F1 through F9.  If you begin recording a macro and assign it
to F9, you will need to press Alt-F9 to play that macro.  If you assigned
the macro to the letter A, (you can assign all 26 letters, A through Z) you
play the macro by pressing Alt-F10 A.  Since there are menus available 
through Alt key sequences, it is necessary to prefix an alphabetic keywith
Alt-F10.  It is easy to verify the name of the macro by viewing the list of
available macros in the Tools menu 

 Depending on where you are in dBASE IV, pressing Alt followed by a letter
will either do nothing or pull down a menu.  

Names can be assigned to macros.  By default, the name of each macro is the
name of the key to which it is assigned.  In the previous example, the name
of the A macro is A.  You can give your macro a more descriptive name of up
to 10 characters maximum.

Macros can also be recorded at the dot prompt.  You do this by pressing
Shift-F10.  When you do this a small window pops up and asks whether you
want to begin recording a macro, or finish recording one if in the midst of
a macro recording session.  

This pop-up ability is available anywhere in dBASE IV including the Control
Center.  Actually, you can begin recording your macro in any design screen,
text editor, even in Help screens.  If you use the actual Tools menu
recording system, you must begin all your macros from the Control Center. 
Since you usually are going to open a file and perhaps run a report or some
labels, the Control Center is not such a bad place to begin running your
macro.  Further, only within the Tools menu can you see the table of all
your macros.  If you find it difficult to remember which macro you assigned
to which task, you should definitely start your macro recordings and
playbacks from the Tools menu where you have a full list of the available
macros in the current library.

If you do plan to have your macros play from the Control Center, be sure
that your keystrokes are specific ones.  Pressing Downarrow to move to your
favorite database file will not give the proper results if you create a new
database that takes the first alphabetic position in the data panel. 
Rather, have your macro type the first few unique letters of the file name
since that will make for a macro that will specifically pick out the
database that you want, regardless of its physical position among the other
files in the panel.

Macro Operations

A common error that first time macro users run into is in assuming that the
macro library is loaded automatically when you start up dBASE IV.  It's
quite easy to load a macro library and you must remember to do so every
time you enter dBASE IV or your macros will not be available for use.  You
could also have a startup routine load a predetermined library for you. 
The menu option that loads macro libraries is Tools: Macros: Load library. 
From the dot prompt or from a program a library can be loaded with the
command

RESTORE MACROS FROM <filename>

where filename is substituted by the name of an available macro file on
disk with the extension .KEY .

A second common and unfortunate error is in forgetting to save your newly
recorded macros into a library on the hard disk.  I've done it myself a few
times.  I've recorded a few macros, then inadvertently left dBASE IV to
move to another task.  Whoops!  So remember to save your macros to the disk
or they will disappear into the world of Tron when you quit!  The option
Tools: Macros: Save library is available from the Control Center for you to
save your macros.  If you are a dyed-in-the-wool dot prompt user, you still
may want to venture back to the Control Center for a more visual approach
to saving your macros before you quit.  It can help you a lot to verify the
existence of all your macros prior to saving the library.  You can still
use the SAVE MACROS TO command from the dot prompt if you are satisfied
with what you've recorded. 

Dej Vu 

An interesting feature of macro libraries is that they can be loaded on top
of one another.  This means that if one library is loaded and a second
library is loaded, those  macros that are assigned to the same keystroke
will be overwritten by those contained in the second library.  However,
keys that are not assigned in the second library will not be overwritten
(or blanked out) when the second library is loaded.  With a little stretch
of the imagination you can visualize a system of macros that are based on
an initial set of macros that are fine tuned by a second set of macros. 
The second and subsequent libraries are loaded and available depending on
the context of the application program in which they are available.  

It is common to accidentally add a few unnecessary keystrokes to your
macros during recording.  These can be easily edited out in the macro
editor, available only through the  Tools: Macros: Modify a macro option. 
Using the Menu System, page 14-6 has a complete list of the correct
spelling of each of the special keywords assigned to keystrokes such as
Backspace and PgUp by the dBASE IV macro editor.

Summary

Keyboard macros offer a full range of functions, from creation to
deletion.  You can edit macros, nest macros, insert user breaks (that allow
the user to insert data necessary for the rest of the macro) and so on. 
You can even use macros to record all the keystrokes in a particular
session to allow you to review the steps you took to get that report that
the boss wanted.  Get to know them.  They may come in handy some dreary day
when your concentration has ebbed to the point where you barely remember
your way to the coffee machine much less how to generate the end of month
figures.  


6  Etc.

Etc.
A Matter of Precedence

SQL Commands In UDFs

Page 4-28 of dBASE IV 1.1 Getting Started with dBASE IV Developer's Edition
states, "A UDF can contain only the dBASE IV commands and functions that
are allowed in SQL mode.  A UDF cannot contain any SQL command."

This is not always true.  Some recent testing indicates there are some
limitations.  You can only execute an SQL command in a UDF when it isn't
called by the VALID or WHEN clause of the @...GET command.  Otherwise, an
expression such as this works with no problem:

memvar = NewID()
FUNCTION NewID
  SELECT MAX(Memberid) INTO maxid FROM Members;
  newid=IIF(SQLCNT>0,maxid+1,1)
RETURN newid
APPEND FROM Axiom

Here's a truism that many people miss even though it's been a published
fact for some time:  If you want to append all records that are not marked
for deletion from one file to a new file, follow these simple steps:

SET DELETED ON
USE <fileA>
APPEND FROM <fileB>
 
This brings in all records that are not marked for deletion in <fileB>.

Note the following, however:

1.  SET DELETED must be ON (if it's not, ALL records from <fileB> will be
appended).
2.  You'll only be able to bring in records that aren't marked for
deletion.          

This feature is documented under APPEND FROM in Language Reference.    

Postscript Improvement

A new printer driver for Postscript that allows printing of Avery labels is
available through our support lines and on the BBS.  The new Postscri.DLD
file enhances the capability of the PostScript support in dBASE IV 1.1.  It
adds the capabilities to print to Avery labels, variable point size and
variable number of lines per page.

You will now be able to include the following macro names in the starting
control codes to print to a particular Avery label:

A5160   
A5163   
A5164
A5162
O5162  (for older version of 5162; 7 rows & 9 lines per label)

You will be able to print a font at any point size.  For example,   to
print a 5 point font, put the following macro in the starting control
codes:

5 point

You can take full advantage of the variable point size feature by
specifying the number of lines per page.  For example, to print 100 lines
per page, enter the following macro in the starting control code:   

100 LPP

This macro will adjust the size of the VMI so that 100 lines would fit in
the current paper size.  For proper pagination, set the lines per page
setting on the print menu to 100 lines as well.       

You can combine the macros together.  For example, to print a 6 point
Helvetica font and 90 lines per page, set the starting control codes to the
following:

2font 6 point 90 LPP

To use the enhanced Postscri.DLD, copy the Postscri.dld file supplied with
the disk (available through Software Support) over the existing
Postscri.DLD in your dBASE directory on your hard disk.

Understanding Order of Precedence

dBASE IV uses an order of precedence in which to determine what order
report fields are calculated.  There are six types of calculated or summary
fields possible in a report:

  Named hidden calculated
  Named hidden summary
  Named calculated
  Named summary (SUM, MAX, MIN, etc)
  Unnamed calculated
  Unnamed summary

The order of field calculations are as follows:

1.  Hidden 
2.  Non-hidden named 
3.  Unnamed
 
Hidden named calculated and named summary fields are calculated first. 
They are calculated in the order created.  This can cause potential
problems.  
For instance, creating or modifying a hidden field that references a
non-hidden field.
This results in the value of the hidden field always being  one record
behind and leads to further problems when the hidden field is then
referenced in another hidden or non-hidden field.

Non-hidden named calculated and named summary fields are calculated after
hidden fields.  These fields are calculated in a top-to-bottom,
left-to-right fashion within the band.  Here again, a problem arises when
you create a field that relies on another calculated or summary field that
is to the right or below.  This also results in the value of the field
being always one record behind.  This will lead to further problems if the
field is then referenced in other calculated or summary fields.

Unnamed calculated and summary fields are calculated last, at the time they
are printed out.  The only problems these fields may have is if they
reference a hidden or non-hidden field as discussed above.

Common problems and what to look for

Most often, a calculated or summary field isn't correct or a Group Summary
field is one group behind.  Here's what to look for:

1.  Is the field named or unnamed?
2.  If a calculated field, what exactly is the expression of the
calculation?  (You want to be looking for other calculated or summary
fields contained in the expression)
3.  If a summary field, what field is the operation being done on?  Is this
field a calculated or summary field?

A few solutions

If the field is named and not hidden:

1.  Try unnaming the field.  This will put it last in the order of
precedence.  This solution will only work if the field is not used in
another field's calculation (otherwise the other field will be referencing
a non-existent field).
2.  Move the field to below or to the right of all the fields it depends
on.  It will then be calculated after all the fields it depends on have
been calculated.

If the field is hidden:

1.  If the expression relies on another hidden field, delete the field and
recreate it.  Since hidden fields are calculated in the order they were
created, this field will now be calculated last.
2.  If the expression relies on a non-hidden field:
  a.  Unhide the hidden field
  b.  Place the now visible field after all the fields on which it relies.
  c.  Give it a picture consisting of just spaces.

For example,  suppose you SUM a price field in the Report Summary Band and
then create a calculated field that displays how much tax there will be.
You must put the TAX field after or below the SUM field.

Taking another example, you have a hidden field that relies on a non-hidden
field and the hidden field is being summed in a third non-hidden field. 
The SUM, of course, will come out wrong.

You must un-hide the field and place it after the field it relies on.

What to watch out for:

1.  If you unname a field and that field is used in other calculations,
when the report is run the error "Variable not found" will occur.
2.  If, in a group summary band, you unname a calculated field that uses an
actual database field, the database field used will be from the first
record of the next group instead of  the last record from the current
group.  (This becomes a little trick: If you want to use a value from the
last record's value in a group summary, make sure the calculated field is
named!) 



7  Easy .BINS in C

Easy .BINs in C
Roland Bouchereau


Using other languages to make .BIN files for dBASE IV is getting easier
every day.  But their making is not without a set of special rules.  The
focus of what I will be presenting here shows the ins and outs of
constructing dBASE IV .BIN files written in the C programming language;
specifically, version 2.0 of Borland's Turbo C.  Fair warning:  This is not
for the inexperienced, uninitiated, squeamish, small children or those
wearing pacemakers.  So, with that said, let's jump right in.

The number of third party .BIN file products available to users of dBASE
III Plus were many.  The core of the dBASE Programmer's Utilities, Volume
II was a set of .BIN files written in Lattice C, then a popular C
compiler.  The author of the utilites, Ralph Davis, chose Lattice C because
of two major points.  The Lattice C compiler "provides an option for
creating .COM files, which are closely akin to .BIN files."  Secondly, the
assembler source code for the initialization or start-up routines is
supplied with the compiler.  The approach the author used was to rewrite or
replace the start-up code for use within the dBASE environment.  A very
similar approach is taken in chapter twenty of the book dBASE Power,
written by P. L. Olympia, R. Russell Freeland and Randy Wallin.  The
compiler used is version 1.5 of the Turbo C compiler, however.  The
guidelines and routines in this text reflect use of version 2.0 of the
Turbo C compiler, but much of their discussion relates directly to the
methods employed here.  The authors provide good arguments for using Turbo
C, not least important is the fact that the start-up code is fairly clear
and straightfoward.  This makes it easier to modify it to our needs, if
necessary.

Part of our goal here will be to eliminate the need for the assembly
written start-up code.  This will simplify the creation of .BINs as well as
remove the need for an assembler, a negative handicap for some of us.  A
linker and the DOS EXE2BIN utility will still be necessary, however.  When
replacing the start-up code, we must, as best possible, provide the
compiled program an environment most conducive to its operation.  The
start-up code presented by the authors of dBASE Power takes care of a few
key tasks necessary to prepare just such an environment.  An excerpt of
that code is listed below.  

This start-up code handles four important tasks.  

* Aligns the code segment with DGROUP.  This is done in the group and
assume directives and is necessary if we are to reference any data local to
the .BIN.  
* Asserts the local data segment.  Also necessary if we are to use local
data.  
* Calls the main program main(), and 
* executes a far return back to dBASE control.  The dBASE parameter
referencing is useful but optional and may be handled in other ways as
we'll see later.

Memory Model and Segment Placement

The two factors that most greatly determine how a .BIN file is created are
memory model and segment placement.  These issues are actually one and the
same since the compiler makes different assumptions on segment placement
depending on the memory model.  Other issues are what registers to save, if
any, parameter addressing, whether it is necessary to implement a stack,
what global variables need to be declared, and what library routines may be
used without undermining the rest of the program code.

Let's tackle the issue of memory model. Since only routines within the .BIN
file may be called, only models supporting near code can be used.  EXE2BIN
will not allow us to convert the .EXE file to a .BIN if there exist
references to any far routines called from our code.  That leaves us with
the tiny, small and compact memory models.  Our main routine, however, must
use a far return when returning back to dBASE and thus must itself be a far
routine, regardless of model.

Once the desired model is chosen, some Turbo C extensions allow us to
handle some of the interface in the C program itself as the code below
(Reverse.C) illustrates.

This works like a charm.  The MK_FP macro combined with the ability to
directly access CPU registers are what really get this code working.  The
routine returns to dBASE with a far return since we declared main() to be
far, and our pointer to the dBASE parameter is automatically far since we
are using the compact memory model.  We need little more than this, if our
.BIN files don't have to be any more complex.  But if our .BIN files are to
be more complex or use many library routines, then we must find a means of
properly referencing the data segment within our .BIN.  This involves two
separate and seemingly disparate steps.  The code shown on the next page
(Sample.C) follows suit with the techniques employed so far and employs
techniques for accessing data local to the .BIN file.

Registers, Linkers, and Switches. Oh My!

Equating the ds register to be the same value as the cs register tells the
CPU where to find our data.  This is only half of the trick though.  The
other part involves giving the linker the same kind of instruction, that
is, telling it that the offsets to our data should be relative to code
segment.  Oddly enough this does not involve any linker parameters but a
compiler switch.  The -z directive allows us to change or specify names for
the default segments, groups and classes for the Turbo C compiled code. 
For example, the  start up code example would have to be compiled using the
following command line.

tcc -c -mc -zPDGROUP sample

The -c tells the compiler to compile only, do not link the main program
with the standard Turbo C start-up code.  The -mc switch indicates that we
are compiling to the compact memory model.  The last switch is what
accomplishes the other part of data alignment technique.  It tells the
compiler to align the code segment as a part of DGROUP, the same group used
for data segments.  I cannot stress enough the importance of using this
switch when making .BINs.  It should be used even if you don't include any
data items yourself; the compiler and linker may do so on their own.  The
sample code fragment above may be used quite reliably as a template for the
structure of your C-coded .BIN.  

Now that all that's been said, here's a point-by-point guideline for making
.BINs.

* dBASE IV requires that your code, data and stack (should you decide to
implement one) together not exceed 32K.
* The source program must be compiled using a near code memory model, that
is, either the tiny, small, or compact model.
* All code in the .BIN should be linked in at the lowest memory address. 
This will be handled properly if the -zPDGROUP command line switch is used.
* The need to statically initialize global data is imperative.  This is
because Turbo C puts uninitialized global data in the  BSS data segment. 
This data segment will effectively not exist by the time your C program is
a .BIN, and will cause memory outside your .BIN to be overwritten.
* Always compile with the -c option.  This will prevent the automatic
invocation of the linker, which attempts to link in the normal start-up
code for building .EXE and .COM files.
* The program should be compiled with stack checking off.  The stack
checking routine assumes that you have linked in the normal start-up code. 
Stack checking is off by default.
* The main() routine must be the first function in the source and must be
declared explicitly as far.  This is so that the .BIN returns control back
to dBASE IV using a far return.
* When manipulating dBASE parameters, you are free to change the content of
the parameters, but their lengths must not be altered.
* If a stack must be used, you must manipulate the _SS and _SP variables to
point to a local array.  Be sure to save their original values and restore
them before returning to dBASE IV.
* Unless the compact memory model is used, dBASE parameter strings can not
be accessed without identifying them explicitly as far.  Library functions
only work on data addressed in their specific memory model.  Therefore you
must write functions to copy or manipulate the parameters yourself.
* Long integer arithmetic, either signed or unsigned, can only be used in
the tiny memory model.  This is because long arithmetic operations are not
compiled to inline operations but to a call to a far routine.  The compiler
and linker together perform some low-level sleight of hand to accomplish
this.
* No memory allocation routines may be used.  This is a general no-no when
constructing any dBASE IV .BIN file.
* No floating point arithmetic can be used.  This is because the floating
point library breaks many of the rules we've already laid down and because
it takes up too much code space and because I can't come up with a trick to
make it work anyway!

Even with all these rules to follow, making .BINs for dBASE IV should be
quite easy.  Here is the source code for three .BIN files.  They should
serve as excellent examples for writing your own .BINs and be quite useful
as well.  In addition, there is a library of UDFs at the end of the code
lisitngs (DD.PRG) to help you make better use of the C routines herein. 

--------------------------------------------------------------------------
REVERSE!.C 
/* 
  reverse!.c  Reverses as many parameters as sent to it! 

  Build reverse!.BIN as follows:

  tcc -c -mc -zPDGROUP reverse!
  tlink reverse!,,,cc
  exe2bin reverse!
*/

#include <string.h>
#include <dos.h>

int    argc = 0;
char    ** argv = (char **)0L;

far main()
{
   DS = CS;  argc =  CX;  argv = MK FP( ES, DI);

  while ( argc + 1)
    strrev( argv[ argc]);
}

----------------------------------------------------------------------------
SCROLL.C
/* 
  scroll.c  Makes SCROLL.BIN.  Use it as follows:

      . CALL Scroll WITH 10,10,20,54,-3,"/BG"
*/
#include <dos.h>

#define TRUE      1
#define FALSE     0

/* These are our attribute numeric values */
#define BLACK     0
#define BLUE      1
#define GREEN     2
#define RED       4
#define WHITE     7
#define BRIGHT      8
#define BLINKING    0x80
#define UNDERLINE   1
#define INVERSE   0x70

unsigned char   color(char *dbcolor);

int    argc = 0;
char    ** argv = (char **)0L;

far main()
{
  union REGS  registers;
  int     temp;

   DS = CS;  argc =  CX;  argv = MK FP( ES, DI);
 
  if ( argc == 6) {       /* Have we enough parameters? */
    registers.h.ch = atoi( argv[0]);  /* Upper row  */
    registers.h.cl = atoi( argv[1]);  /* Left column */
    registers.h.dh = atoi( argv[2]);  /* Lower row  */
    registers.h.dl = atoi( argv[3]);  /* Right column*/
    registers.h.ah = 6;   /* Assume scroll up */
    temp = atoi( argv[4]);
    if (temp < 0) {     /* Negative means down */
      registers.h.ah++;
      temp = -temp;
    }
    registers.h.al = temp;
    registers.h.bh = color( argv[5]); /* Parse our color. */
    int86(0x10,&registers,&registers);  /* Call BIOS */
  }
}

unsigned char color(char *dbcolor)
{
  unsigned char   badchar,ground,hue;
  badchar = ground = hue = 0;
  while (*dbcolor == ' ')
    dbcolor++;
  while (*dbcolor && (!badchar)) {
    switch (*dbcolor++) {
      case  'r' :
      case  'R' :
        hue |= (RED << ground);   /* Fore or back"ground". */
        break;
      case  'g' :
      case  'G' :
        hue |= (GREEN << ground);
        break;
      case  'b' :
      case  'B' :
        hue |= (BLUE << ground);
        break;
      case  'w' :
      case  'W' :
        hue |= (WHITE << ground);
        break;
      case  'i' :
      case  'I' :
        hue |= INVERSE;
        break;
      case  'u' :
      case  'U' :
        hue |= UNDERLINE;
        break;
      case  'x' :
      case  'X' :
      case  'n' :
      case  'N' :
        break;
      case  '/' :
        if (!ground)
          ground = 4;   /* Shift to back"ground" */
        else
          badchar = TRUE;
        break;
      case  '+' :
        hue |= BRIGHT;
        break;
      case  '*' :
        hue |= BLINKING;
        break;
      default :
        badchar = TRUE;   /* We've reached a bad character */
    }
  }
  return(hue);
}

----------------------------------------------------------------------
DD.C
/*
  DD.C  A library for disk and directory management

  This is a full blown example of how to create a .BIN
  library.  Each function has a unique number.
*/

#include <dos.h>
#include <dir.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

enum funcnum {
  GETDRIVE=1,
  SETDRIVE,
  ISDRIVE,
  ISNETDRIVE,
  GETDIR,
  CHDIR,
  MKDIR,
  RMDIR,
  ISDIR
};

void dbstrcpy(char *dbstr, char *cstr);
void dbitoa(char *string, int value);

int      argc = 0;
char      ** argv = (char **)0L;
int     errno = 0;  /* Needed by the library routines */
unsigned  version = 0;    /* Needed for a DOS version check. */

char      tempdir[MAXDIR] = "", homedir[MAXDIR] = "";

#define    osmajor    ((unsigned char) version)
#define    osminor    (*((unsigned char *)(& version) + 1))

far main()
{
  int   retval;

   DS =  CS;   argc =  CX;   argv = MK FP( ES, DI);
  if ( argc) {
    if ( argc == 2) {
      switch (atoi( argv[0])) {
        case GETDRIVE   :
          retval = getdrive();
          break;
        case SETDRIVE   :
          retval = setdrive();
          break;
        case ISDRIVE    :
          retval = isdrive();
          break;
        case ISNETDRIVE :
          retval = isnetdrive();
          break;
        case GETDIR   :
          retval = getdir();
          break;
        case CHDIR    :
          retval = cd();
          break;
        case MKDIR    :
          retval = md();
          break;
        case RMDIR    :
          retval = rd();
          break;
        case ISDIR    :
          retval = isdir();
          break;
        default     :
    /* Negative error code from dBASE IV "Invalid function" */
          retval = -87;
      }
    /* Turbo C does not reset this so I do it myself. */
      errno = 0;
    }
    else
      retval = -94;         /* "Wrong number of parameters." */
    dbitoa( argv[0],retval); /* Send the result back to dBASE. */
  }
}

int getdrive()
{
  if (* argv[1])
    argv[1] = (char)getdisk() + 'A';
  return(0);
}

int setdrive()
{
  int   temp;
  if (* argv[1]) {
    temp = (int)(toupper(* argv[1]) - 'A');
    setdisk(temp);
    if (temp == getdisk())
      return(0);
  }
  return(0xf);         /* Invalid drive specification error code. */
}

int isdrive()
{
  int     temp,retval;
  temp = getdisk();
  retval = setdrive();
  setdisk(temp);
  return(retval);
} 

int isnetdrive()
{
  union REGS  registers;

  if (! version) {
     AH = 0x30;
    geninterrupt(0x21);
     version =  AX;
  }
/* Only on DOS 3.10 or above! */
  if (( osmajor >= 3) && ( osminor >= 0xA)) { 
    registers.h.bl = toupper(* argv[1]) - 'A' + 1;
    registers.x.ax = 0x4409;
    intdos(&registers,&registers);
    if (registers.x.cflag)
      return(registers.x.ax);
    else
      return((registers.x.dx & 0x1000) == 0);
  }
  return(0x32);  /* Network request not supported. */
}

int getdir()
{
  char    *temp;

  if (* argv[1]) {
/*  
  If a drive letter was specified, use it.
  Otherwise assume the default drive.
*/
  getcurdir(((* argv[1] != ' ') ? (toupper(* argv[1]) - 'A' + 1) : 0),
tempdir);
    if (!errno) {
      temp =  argv[1];
      *temp++ = '\\'; /* Add a leading backslash. */
      dbstrcpy(temp,tempdir);
      return(0);
    }
    else {
      dbstrcpy( argv[1],"");    /* Blank out the directory name. */
      return(errno);
    }
  }
  return(0);
}

int cd()
{
  if (* argv[1])
    chdir( argv[1]);
  return(errno);
}

int md()
{
  if (* argv[1])
    mkdir( argv[1]);
  return(errno);
}

int rd()
{
  if (* argv[1])
    rmdir( argv[1]);
  return(errno);
}

int isdir()
{
  int homedrive,tempdrive,retval;

  homedrive = getdisk();      /* Save our drive*/
  getcurdir(0,homedir);       /* and our directory. */
  if ( argv[1][1] == ':') {
    tempdrive = (int)(toupper(* argv[1]) - 'A');
    setdisk(tempdrive);
    if (tempdrive != getdisk())
      return(0xf);    /* Invalid Drive Specification */
  }
  getcurdir(0,tempdir);
  retval = ((chdir( argv[1]) == 0) ? 0 : errno);
  chdir(tempdir);
  setdisk(homedrive);
  chdir(homedir);
  return(retval);
}

/* 
  The following functions duties are to copy a string or number
  to a dBase parameter area, padding it out with spaces if
  necessary, so that parameter lengths are unaffected.
*/

void dbstrcpy(char *dbstr, char *cstr)
{
  while (*dbstr && *cstr)
    *dbstr++ = *cstr++;
  while (*dbstr)
    *dbstr++ = ' ';
}

void dbitoa(char *string, int value)
{
  char    negative,len;

  if ((len = strlen(string)) != 0) {
    if (value < 0) {
      negative = TRUE;
      value = -value;
    }
    else
      negative = FALSE;
    while (len && value) {
      string[len] = (value % 10) + '0';
      value /= 10;
    }
    if (value || (!len && negative))
      while (*string)
        *string++ = '*';
    else {
      if (negative)
        string[len] = '-';
      while (len)
        string[len] = ' ';
    }
  }
}

----------------------------------------------------------------------------
DD.PRG
*DD.PRG    This is the dBASE UDF front end to DD.BIN.
*     To make use of both, issue the two following
*     commands.  Negative numbers correspond to 
*     dBASE error numbers.
*
*       . SET PROC TO DD
*       . LOAD DD
*

FUNCTION GetDrive       && Syntax: GetDrive()
  PRIVATE temp 
  temp  = " "
  CALL Dd WITH 1,temp 
RETURN temp         && Returns a character expression.


FUNCTION SetDrive       && Syntax: SetDrive(<expC>)
  PARAMETER drive 
  IF TYPE("drive ") = "C"
    RETURN CALL("Dd",2,drive )
  ENDIF
RETURN -9         && Returns an error number, 0=no error.


FUNCTION IsDrive        && Syntax: IsDrive(<expC>)
  PARAMETER drive 
  IF TYPE("drive ") = "C"
    RETURN CALL("Dd",3,drive ) = 0
  ENDIF
RETURN .F.          && Returns a logical true or false.

FUNCTION IsNetDr        && Syntax: IsNetDr(<expC>)
  PARAMETER drive 
  IF TYPE("drive ") = "C"
    RETURN CALL("Dd",4,drive ) = 0
  ENDIF
RETURN .F.          && Returns a logical true or false.


FUNCTION GetDir       && Syntax: GetDir(<expC>)
  PARAMETER drive 
  PRIVATE temp 
  temp  = IIF("" = drive ," ",LEFT(drive ,1)) + SPACE(62)
  CALL Dd WITH 5,temp 
RETURN TRIM(temp )      && Returns a character expression.


FUNCTION ChDir          && Syntax: ChDir(<expC>)
  PARAMETER dire 
  IF TYPE("dire ") = "C"
    RETURN CALL("Dd",6,dire )
  ENDIF
RETURN -9           && Returns an error number, 0=no error.


FUNCTION MkDir          && Syntax: MkDir(<expC>)
  PARAMETER dire 
  IF TYPE("dire ") = "C"
    RETURN CALL("Dd",7,LTRIM(TRIM(dire )))
  ENDIF
RETURN -9         && Returns an error number, 0=no error.


FUNCTION RmDir          && Syntax: RmDir(<expC>)
  PARAMETER dire 
  IF TYPE("dire ") = "C"
    RETURN CALL("Dd",8,LTRIM(TRIM(dire )))
  ENDIF
RETURN -9           && Returns an error number, 0=no error.


FUNCTION IsDir          && Syntax: IsDir(<expC>)
  PARAMETER dire 
  IF TYPE("dire ") = "C"
    RETURN CALL("Dd",9,LTRIM(TRIM(dire ))) = 0
  ENDIF
RETURN .F.          && Returns a logical true or false.


FUNCTION PathSpec       && Syntax: PathSpec(<expC>)
  *Returns a proper full DOS path spec. as in "C:\DBASE\SAMPLES"
  PARAMETER drive 
  PRIVATE temp 
  temp  = IIF("" = drive ,GetDrive(),drive )
  IF IsDrive(temp )
    RETURN UPPER(LEFT(temp ,1)) + ":" + GetDir(temp )
  ENDIF
RETURN ""         && Returns a character expression.

* EOF. DD.PRG



8  What Is an Array?

What Is  an Array?

Joe Stolz

The question used to be "Why doesn't dBASE use arrays?"
Now that dBASE IV supports them, the question is "What are they?"

Your average dictionary would define "array" as a splendid assortment, sort
of like your average box of Whitman's.  But in the computer language world,
arrays, somewhat like their chocolate counterparts, are an ordered group of
values kept in memory under the banner of a single name.  Arrays are very
useful if you know how to apply their usage.  If you have no idea what they
can be used for, it's just another page in the manual with techno-babble
that you have to turn through.  So, let us try to offer a simple view of
arrays and some solid (as opposed to soft-centered) examples of how they
can be used.  In the end they should appear to be useful additions to the
dBASE IV language.

Creating Arrays

As we said, an array is a list of items, kept in memory, that allows you to
reference the list in a simple logical fashion.  An array has a name and is
made up of a prearranged number of items.  You create an array somewhat
differently than you create memory variables.  Memory variables can be
created by assigning them a value.  They can also be created through the
less known PUBLIC or PRIVATE statements.  You can create the memory
variable m_sum by declaring it as a memory variable that is PRIVATE to the
program in which it is declared.  That is, you have the line:

PRIVATE m_sum 

on a line by itself in your program.  This creates a memory variable that
really has no specific value assigned to it.  In fact, all variables that
are created via PRIVATE or PUBLIC statements have a logical false (.F.)
value assigned to them initially.  When you explicitly give the variable a
value is when it can take on more than a simple boolean value.

The simplest and most common way to create variables is to directly assign
them a value.  That is:

m_sum = 0

both creates the new variable and also gives it a value of zero.

Arrays, however, must always be created in two steps: a declaration and
then an assignment.  This is done as follows:

DECLARE 3_values[3]

This creates an empty array of three items or elements and leaves them
unassigned.  As in the memory variable case, the 3 elements all have a .F.
value by default.

Actually, these three array elements appear as if they were three separate
memory variables when you look at them by typing DISPLAY MEMORY.  They show
up as
 3_values[1], 3_values[2] and 3_values[3].  You assign new values to these
array elements by direct assignment as if they were regular memory
variables except that you must indicate the proper "index" of the element
being assigned.  An index is the number in the square brackets which
indicates which element is which in the list of elements that make up the
array.  

Using Arrays

Let's discuss a common usage for arrays.  By now, a dBASE IV user knows the
difference between fields and records in a database.  The fields in the
file are a series of data elements that make up a record.  There may be 10
or 100 fields in each record.  A series of repeating records make up a file
or database.  An array can therefore be a memory based representative of a
record.  If your database has 12 fields in it, you can represent a record
by first declaring an array of 12 elements, then assigning values to each
of the elements in field order.  Here's an example:

DECLARE Curr Name[12]       && 12 fields per record

Curr_Name[1] = "John"     && equivalent to Firstname
Curr_Name[2] = "Wadsworth"  && Lastname 
Curr_Name[3] = "122 So. Main" && Address
Curr_Name[4] = "Los Angeles"  && City 
.
.
.

This way you can have a series of elements in memory that represent a
record in your database.  You can use your array as a record to enter and
validate data without even opening the database.  This raises a couple of
questions: What is the difference between 12 regular memory variables and
this 12 member array?  Just what does an array offer over memory
variables?  Well, dBASE IV has gone out of its way to offer several
commands that capitalize on your choice to use arrays.  First, now that you
have an array that simulates your record structure, you can use the new
dBASE IV command REPLACE FROM ARRAY which can, in one shot, copy the data
from your array into the appropriate matching fields in your file.  REPLACE
FROM ARRAY can be used to modify one or many records.  This allows you to
reference a single array name in this operation instead of 12 different
variable names.

Other commands that are related to arrays are COPY TO ARRAY and APPEND FROM
ARRAY.  These copy one or more records into a pre-DECLAREd array and add
one or more records from an array onto the bottom of a file, respectively.

Beyond a Single Dimension

The power of such an array as we've just described is welcome but one
quickly sees the limitations of a one-dimensional array.  It should then be
comforting to know that dBASE IV supports two dimensional arrays as well. 
This type of array follows the memory based database analogy even more
closely. A database is a series of fields that repeats its pattern over
many records.  One "dimension" is the number of fields in each record.  The
"second dimension" is the number of records contained in your file.  To
represent a file of 50 records where a record is made up of 12 fields you
could declare an array like this:

DECLARE Many_Recs[50,12]

where the first number in square brackets represent rows and the second
represents columns.  Many_Recs is an array where each record has 12 rows
and there are 50 records in the full array.  Consequently, the array
consists of 50 * 12 or 600 elements.  

In dBASE IV, if the array being used is two dimensional, the APPEND FROM
ARRAY and REPLACE FROM ARRAY commands will apply sequentially to each
element in the array.  If the array is one dimensional (columns only) and
the REPLACE FROM ARRAY command has a FOR or WHILE condition, when the
condition is met, the same values will be used every time a REPLACE is
made.

It is interesting to note that in most other programming languages, there
is array support, but that arrays are quite often used for storing a long
series of numbers or elements, then processing that series of elements
sequentially.  For example, you could create an array that contains as many
elements as you have records in your file.  Afterwards, copy a value from
each record into separate elements of the array.  You could then process
the array sequentially and produce,  say,  an average.  (This seems like an
awful lot of work to produce an average when dBASE IV provides an AVERAGE
command that works directly on your file, but it helps to underscore the
fact that a database is, in a sense, a two-dimensional array on disk.)

In earlier versions of dBASE,  "pseudo-arrays" could be created by
employing "& macros".   By attaching a trimmed, converted string of an
incrementing number to a literal (such as the word "TABLE", for example),
you could come with functionality close to that of storing indexed values. 
But the process of getting that information to and from records became
quite cumbersome.  Although it may have been a clever workaround in its
day, the process required some creative coding on the part of a new
programmer and not the easiest type of code to maintain. 

Now, in dBASE IV, a DO loop can variably reference an index in an array in
a sequential manner.  The basic idea involved in processing a series of
array elements is:

DECLARE Array1[100]
* assign values to the array
x= 1
DO WHILE x < 101
  some_var = Array[x]
  * do some processing here
  x = x + 1
ENDDO

Here, as x increases from 1 to 100, we can capture the value of each array
element one by one and perform some process.  The main advantage of an
array here is that it offers an orderly means of addressing a series of
memory variables in a sequential fashion.  Memory variables that do not
contain an "index" require difficult gyrations in order to address them
sequentially.

These are the basics of arrays.  There are only a few rules involved in
dealing with arrays but none that should dissuade you from using them.  The
only valid limitation is that of memory.  Large arrays may cause
insufficient memory conditions during their declaration if not enough
memory is available.  Otherwise, the ability to COPY TO , APPEND FROM or
REPLACE FROM an array to and from records is a welcome addition to dBASE
IV. These commands give you a simple means of addressing a sequence of
data, or as a temporary place to put one or many records from your file. 

