dBASE III Anomalies and Workarounds 1.1 How to use this Section 1.2 @...GET alias to @...SAY...PICTURE >>> @...GET alias in Format File Any command that allows editing of a record will only operate in the SELECTed work area. However, attempting an @...SAY...GET using an ALIAS or SELECT to access another work area in a format (.FMT) file will not produce an error message. Instead, the entire line is suppressed, including the SAY statement. The Developer's Release of dBASE III will return the "Variable not found" error message if any attempt is made to GET a field from another work area. In versions 1.0 and 1.1 of dBASE III, the command: @ 10,4 SAY "Testing " GET B->Test in a format file will not display anything on the screen. Issuing this command from the dot prompt or in a command file will return "Variable not found." An alias name is acceptable in a SAY statement, even in a format file. @ 10,4 SAY "Testing " + B->Test will display properly. Notice that in the first example neither the SAY nor the GET pertaining to the second work area are displayed. [1.0, 1.1, D.R.] >>> @...SAY...PICTURE "@(" Displaying negative numeric memory variables with the @...SAY command and the PICTURE function "@(" to enclose negative numbers in parentheses, will not display the PICTURE correctly. Blanks will display between the beginning parenthesis and the memory variable contents. For example: number = 23.15 @ 10,10 SAY number PICTURE "@(" ( 23.15) The workaround is as follows: STORE STR( number, 12, 2 ) TO mem1 ^------------ Use an appropriate length and decimal value. STORE AT( "-", mem1 ) TO mpos @ 10,10 SAY SPACE(mpos-1) + "(" + SUBSTR(mem1,mpos+1) + ")" (23.15) [1.0, 1.1, D.R.] >>> @...SAY...PICTURE "$", ",", and "9" Using the template symbols "$", ",", and "9" in an @...SAY PICTURE clause may cause more than one dollar sign to be displayed on the screen. Specifically, if the value being displayed does not fill positions in the display string preceding the comma position, a dollar sign will be displayed in place of the comma. For example: mem = 123456 @ 10,0 SAY mem PICTURE "$999,999,999" @ 11,0 SAY mem PICTURE "$999,999,999,999" will output: $ $123,456 $ $ $123,456 The PICTURE template, ($), is intended to replace leading zeros in a numeric display with dollar signs. This means that dollar signs should always display in a fixed position format. To display a fixed position dollar sign leading a numeric expression with embedded commas, use two @...SAY commands, one to display the dollar sign and the other to display the numeric variable with the embedded commas. For example, mem = 1234.56 @ 10,10 SAY "$" @ ROW(),COL() + 1 SAY mem PICTURE "99,999.99" If you wish a leading dollar sign that is floating for numeric variables with embedded commas, a feature not directly supported by dBASE III, use the following command file to format the numeric variables. * Commas.PRG PARAMETERS mem mvar = SUBSTR( STR( mem, 9, 2 ), 1, 3 ) + ","; + SUBSTR( STR( mem, 9, 2 ), 4, 3 ); + SUBSTR( STR( mem, 9, 2 ), 7, 3 ) cntr = 1 DO WHILE SUBSTR( mvar, cntr, 1 ) $ ", " cntr = cntr + 1 ENDDO cnum = SPACE( cntr - 1 ) + "$" + SUBSTR( mvar, cntr ) @ 10,0 SAY cnum * EOP Commas.PRG STORE 1234.56 TO x DO Commas WITH x outputs: $1,234.56 [1.0, 1.1, D.R.] This formula will work with numbers as large as $999,999.99. Larger numbers require that the length argument of the STR() function and the length and starting point arguments of the SUBSTR() function be changed to accommodate the larger number. For numbers as large as $999,999,999.99 the second line of the preceding program should be changed to: mvar = SUBSTR( STR( mem, 12, 2 ), 1, 3 ) + ","; + SUBSTR( STR( mem, 12, 2 ), 4, 3 ) + "," ; + SUBSTR( STR( mem, 12, 2 ), 7, 3 ) +; SUBSTR( STR( mem, 12, 2 ), 10, 2 ) This problem persists in the Developer's Release including both the PICTURE clause and the TRANSFORM() function. The work-arounds are a little bit easier given some new functions including TRANSFORM() itself. To display a numeric expression with a leading dollar sign in fixed position and embedded commas concatenate a dollar sign to the result of the TRANSFORM() of the numeric expression. For example, mem = 1234.56 @ 10,10 SAY "$" + TRANSFORM( mem, "99,999.99" ) To display a floating dollar sign leading a numeric expression with embedded commas, insert the dollar sign into the result of the TRANSFORM() function on the numeric expression. For example, mem = 1234.56 @ 10,10 SAY SPACE(10-LEN(LTRIM(TRANSFORM(mem,"99,999.99")))); ^--------- Length of the TRANSFORM() picture. + "$" + TRANSFORM(mem,"99,999.99") It is important to note that the first element in the SPACE() function argument must be the length of the TRANSFORM() picture. If, for example, the picture is "999", then the element is three. >>> @...SAY...PICTURE "@X" and "@C" The C and X functions documented in the manual reference to the @...SAY command will always display positive numbers as credits (CR) and negative numbers as debits (DB). Use the following program segment if you need them to display in the reverse order (that is, positive numbers as debits (DB) and negative numbers as credits (CR)): DO CASE CASE number > 0 @ row,col SAY STR(number,17,2) + "DB" CASE number < 0 @ row,col SAY STR(-number,17,2) + "CR" OTHERWISE @ row,col SAY STR(number,17,2) ENDCASE [1.0, 1.1, D.R.] >>> @...SAY...PICTURE "@A" and "@!" The A and ! PICTURE clause functions are mutually exclusive. That is, you cannot combine the two to make a new function that will limit data input to alphabetic characters and force the letters to uppercase. If A and ! are used together, only the second function will be in effect. For example: PICTURE "@A!" is equivalent to PICTURE "@!" PICTURE "@!A" is equivalent to PICTURE "@A" [1.0, 1.1, D.R.] >>> @...SAY...PICTURE Logical Variable There is no PICTURE clause that applies to a logical variable in an @...GET command. However, if you attempt to use a PICTURE clause on a logical field or memory variable, dBASE III will not trap it as an error. When the READ is executed, dBASE III will not pause for user input, and the value of the field or variable will not be changed. One possible occurrence of this is when a memory variable is declared PUBLIC but is not initialized before an @...GET command. (dBASE III will automatically initialize a PUBLIC memory variable to a logical .F.) If the variable was not intended to be a logical variable, the above symptoms will manifest themselves. You may want to use the SPACE() function to initialize character variables and zero to initialize numeric variables. [1.0, 1.1, D.R.] >>> @...SAY...PICTURE "@Z" The zero-suppress function, @Z, will not suppress the decimal point when it is used with a non-integer value of zero. To work around this display problem, use the following command sequence instead of the @Z function. IF STR( number, 5, 2 ) <> " 0.00" @ 10,0 SAY number ENDIF [1.0, 1.1] 1.3 APPEND to CONFIG.DB >>> CONFIG.DB with SCOREBOARD The entry SCOREBOARD=OFF has no effect if set in CONFIG.DB. SETing the SCOREBOARD OFF suppresses the display of status and error messages to line 0 . The SCOREBOARD setting can only be changed from a command file or the dot prompt with the command SET SCOREBOARD OFF. 1.4 COPY [STRUCTURE] to DO WHILE 1.5 EDIT to LABEL limitations >>> EDIT If BROWSE is used on an indexed file and is terminated with a Ctrl-Q or Ctrl-W and followed with an EDIT command, keys which advance or regress through the database file (PgUp, PgDn, uparrow, downarrow, Ctrl-E, Ctrl-R, Ctrl-C) will not work properly. Sometimes these keys will drop the user out of EDIT to the dot prompt. Other times they will move two or three records instead of one. The display will sometimes lock on the current record. These keys may also cause the pointer to be positioned at the next to last record in the index. [1.0, 1.1, D.R.] >>> INKEY() The INKEY() function of the Developer's Release does not always read the leftarrow key as CHR(19). Running the following program will demonstrate that dBASE III will trap this key only rarely. i = 0 DO WHILE i <> 13 i = INKEY() ? i ENDDO When you run this program, you will notice that dBASE III treats the leftarrow key as a Ctrl-S, and will pause or start scrolling accordingly. Sometimes the key will be trapped and INKEY() will return 19; however, this is far less common than the former result. [D.R.] 1.6 MODIFY STRUCTURE to PCOL() and SET MARGIN TO 1.7 RECNO() to ROUND() 1.8 SET to SORT >>> SET RELATION TO The command syntax SET RELATION INTO TO will not SET the RELATION although no error message is displayed and DISPLAY STATUS shows the RELATION as SET. The SET RELATION command is sensitive to the order that the INTO and the TO clauses are specified. Apparently, everything after the INTO clause is ignored. Be sure to check that you have specified the TO clause before the INTO clause if your RELATIONs appear to fail. [1.0, 1.1] >>> SORT (1) Attempting to SORT a file with SET FILTER TO SUBSTR(fieldname,n,n) = will return a "*** STACK OVERFLOW ***" and drop you out of dBASE III to the operating system. (2) SORTing a database file from a command file that is nested 13 levels deep will fail with a system crash. The SORT command will return a status message stating that a percentage of the records in the original file were SORTed, but then the computer will freeze with no error message or warning. (3) Attempting to use the command SORT TO ON with a RELATION SET produces the error message "*** STACK OVERFLOW ***" and will drop you out of dBASE III to the operating system prompt. This will not occur if the RELATION is SET to RECNO(). (4) One cannot SORT to a file in a directory other than the current one. An attempt to do this will not produce an error message and will not create the designated file. Instead, a file name W44 will be created in the current directory of the default drive. (5) The effective limit of SORT is in the 32,000 record range. SORTing a file with more than 32,000 records will produce the following error messages: nn% Sorted Records do not balance...(PROGRAM ERROR) 100% Sorted The SORTed file will not contain all the records from the original. The workaround for sorting databases in excess of 32,000 records is to INDEX and then COPY. The procedure is as follows: USE INDEX ON to Temp COPY TO Sorted [1.0, 1.1] >>> SORT with 66 records SORTing a file that has more than 66 records in such a way that the resulting file has exactly 64 records will produce a file in which the first record is corrupted. [1.0, 1.1] 1.9 STORE to ZAP >>> USE An attempt to use the PC/MS-DOS directory navigation path (..) in a USE command will give unexpected results. USEing the database will make the file active, but there will be no ALIAS. An attempt to USE another database file in another SELECT area using the same technique, will cause the error message "ALIAS name already in use" to be displayed. * ---Open first database file. USE ..\nme\Budget DISPLAY STATUS Currently selected database: Select area - 1, Database in use: C:..\nme\Budget.dbf Alias - * ---Open another database file. SELECT 2 USE ..\nme\Sumfrm ALIAS name already in use ? USE ..\nme\sumfrm If an ALIAS is assigned in both USE commands, no error results. If you need to avoid specifying the full path name, use the RUN command to log the previous directory and then specify the datafile to USE. For example, RUN CD .. USE nme\Budget SELECT 2 USE nme\Sumfrm [1.0, 1.1, D.R] 1 dBASE III Programming 1.1 Program documentation 1.2 Database file structure 1.3 Debugging - program break points 1.4 dFORMAT 1.5 MEMO fields 1.6 Setting the system date and time 1.7 Recreating a corrupted dBASE III header 1.8 Simulating the JOIN command 1.9 Limitations 1.10 Capatilizing 1.11 Left-justifying Character Fields 2 dBASE III Frequently Asked Questions 2.1 Installation 2.2 Commands 2.3 New data types 2.4 Memory variables 2.5 Printing 2.6 Data transfer 3 dBASE III Reference 3.1 @...GET to Boolean Operators >>> @...GET PICTURE "@B" The function "@B" is used with the @...SAY...GET statement to left-justify variables of numeric type. It is most useful when SAYing a numeric field or memory variable that is more easily understood as a character string, such as a part number. Use of this FUNCTION with GET, however, causes a slight change in the way numeric variables are read from the screen that may cause some difficulties. A numeric memory variable will default to a length of ten digits when initialized; however, if you are using the PICTURE function "@B" in an @...GET statement, a numeric memory variable will GET the width of the memory variable exactly as initialized, even if a template symbol is used. Initializing a memory variable to zero will cause the GET statement to display one zero on the screen. A READ command will allow one digit only to be entered to the memory variable. This occurs whether the memory variable is initialized to 0 or 0000. For example: SET DELIMITERS ON w = 1234 x = 0 y = 0000 @ 9,0 GET w PICTURE "@B9999" @ 10,0 GET x PICTURE "@B9999" @ 11,0 GET y PICTURE "@B9999" will produce: :1234: :0: :0: A READ command will allow four characters to be entered in the first variable, but only one character in the next two variables. If the "@B" function is used, initialize the memory variable to the proper length or the input may be truncated. 3.2 CHR() to FILE() function >>> Demonstration Disk (Runtime+) In the Developer's Release of dBASE III, the dBRUN programs from the Demonstration Disk are not compatible with the full system. Code crunched with DBC.COM from the Demonstration Disk can only be run with dBRUN from the Demonstration Disk. Code crunched with DBC.COM from the Developer's Disk can only be run with the full dBASE III system or the dBRUN disk, purchased separately. The error message: No database is in USE. Enter filename: is a common indicator that the incorrect dBRUN or dBC is being used. 3.3 FIND to MODIFY STRUCTURE 3.4 Numeric fields to PARAMETERS 3.5 PRIVATE to PROW() >>> PROW() When issuing SET PRINT ON, PROW() will be set to 2, regardless of its previous setting. An EJECT will reset PROW() to 2, not 0. Issuing an EJECT with SET PRINT OFF will reset PROW() to 0. When SET PRINT is ON, PROW() may never equal 0 or 1. Any attempt to test for PROW() = 0 or PROW() = 1 will work only when SET PRINT is OFF. 3.6 PUBLIC to REPORT FORM 3.7 RELEASE to SET PROCEDURE >>> SET COLOR TO , The command: SET COLOR TO , will produce black on black, even if there is no space after the comma. 3.8 SET RELATION to warnings 3.9 dBase III File Structure 3.10 dBASE III Memo File Structure 3.11 Installation and Configuration 4 dBASE III Sample Programs 4.1 Left Justifying Character Fields by Stanley Ballenger Converting a numeric field to a character field using MODIFY STRUCTURE will leave the character field right-justified. If you find that you need the new character field to be left-justified, the following procedure will be a welcome addition to your procedure library. This procedure takes any database file and left-justifies all of the character fields in each record, trimming all the leading blanks. It uses an EXTENDED STRUCTURE to hold the names of all the character fields. Then, taking each character field in turn, it passes through the entire database file left-justifying that character field's values. The next character field is read from the EXTENDED STRUCTURE and the process is repeated. This proceeds until there are no more character fields to left-justify. Note that this procedure makes as many passes through your database file as there are character fields and, as such executes quite slowly. * Program....: Ljustify.PRG * Author.....: Stanley Ballenger * Date. .....: July 1, 1985 * Notes......: Left-justifies all character type fields in a * database file. * PRIVATE start, end, string, fname * ---Open files. SELECT 1 USE Yourfile COPY STRUCTURE EXTENDED TO Temp SELECT 2 USE Temp SET FILTER TO Field_type = "C" GO TOP CLEAR * ---Set up field count and display it. fldcount = 0 column = COL() + 10 @ ROW(),column SAY STR( fldcount,10 ) + " Field values replaced" * ---Justify character fields. DO WHILE .NOT. EOF() fname = Field_name SELECT Yourfile GO TOP end = LEN( &fname ) * ---Remove leading blanks for the current field. DO WHILE .NOT. EOF() start = 1 string = &fname * ---Remove leading blanks from cu~rrrent field. DO WHILE SUBSTR( string,start,1 ) = " " .AND. start < end start = start + 1 ENDDO * ---Replace field if it is left-justified. IF start <> 1 REPLACE &fname WITH SUBSTR( string,start,end ) fldcount = fldcount + 1 @ ROW(),c~orlumn SAY STR( fldcount,10 ) ENDIF * ---Get next record. SKIP ENDDO SELECT Temp * ---Get next character field name. SKIP ENDDO * ---Clean up. CLOSE DATABASES ERASE Temp.DBF RETURN * EOF Ljustify.PRG 4.2 Creative Uses of Database Files for Reporting Database files are central to dBASE II and dBASE III. They are the repositories of the information we want to record and access. Typical uses of database files are mailing lists, customer lists, records of purchase requisitions, or accounts receivable and payable. These will always be the most important uses for database files. However, there are other ways to use database files that may seem a bit more exotic at first, but they can be very powerful. The following program shows how a database file can be used to supply a program with variable parameters. In this case, the database file Conditn.DBF holds the number of different conditions on which the program is REPORTing, sparing us the necessity of hard-coding the conditions into the program itself. If later we want to modify or add conditions, we need only modify Conditn.DBF. This program performs the following function. It reports from the log of all the support calls that I have received in the last two weeks, breaking them down by product (dBASE II, dBASE III, Framework, Friday, and other utility programs). It uses REPORT FORM for its ease, but also does some statistical reporting that the REPORT FORM cannot perform. Since my database file has no numeric fields, I have no need for totals and subtotals. However, I do want to know the count of calls for each product. The program uses EJECT, NOEJECT, PROW(), and PCOL() to position the printer correctly. The structure for the database file Conditn.DBF is as follows: Structure for database file: Conditn.DBF Field Field Name Type Width Dec 1 Condition Character 15 ** Total ** 16 It has one record for each product: Product = 'D3' Product = 'D2' Product = 'FW' Product = 'FR' Product = 'UT' for a total of five records. * Program ..: Cumprint.PRG * Author ...: Ralph Davis * Date .....: October 1,1985 * Note .....: Prints counts from cumulative call log. SET TALK OFF ACCEPT " FILENAME: " TO filename ACCEPT "FORM NAME: " TO formname USE &filename SELECT 2 USE Conditn * ---EJECT page to reset line and column counters. SET PRINT ON EJECT * ---Initialize control variables. printmore = .T. memcond = Condition DO WHILE printmore SELECT &filename COUNT FOR &memcond TO mcount IF mcount > 0 REPORT FORM &formname NOEJECT TO PRINT FOR &memcond SET DEVICE TO PRINT * ---Skip to next page if line counter is past 58. IF PROW() > 58 @ 5,0 ENDIF @ PROW()+2,1 SAY "NUMBER OF CALLS: " @ PROW(),PCOL()+1 SAY mcount SET DEVICE TO SCREEN EJECT ENDIF * ---Return to control file. SELECT Conditn * ---Get next condition. SKIP * ---Leave program if no more conditions. IF EOF() EXIT ELSE memcond = Condition ENDIF ENDDO SET PRINT OFF SET TALK ON RETURN * EOP Cumprint.PRG 5 dBASE III Technical Notes 5.1 Simple List Program dBASE III offers standard list reporting through the REPORT FORM command. The REPORT FORM is easy to learn and very useful for column formatting whether you use one file or several files that are linked with the SET RELATION command. Often, however, you may find the REPORT FORM restrictive and so find it necessary to design a custom report. There are two fundamental reasons for this. First, there are physical limitations to the REPORT FORM definition. Specifically, there are limits to the number of fields and characters in a report definition. If you have a complex or large report, you may have run into these limits. Second, your report requires features that are not supported by the REPORT FORM. These may include statistical calculations such as averaging, multiple parent-child relations, formatted numeric fields, and non-columnar format, such as a pre-printed form.In all of these cases a custom report must be written. The purpose of this article is to help you understand the concepts behind writing a custom report by way of a working example. The following sample program illustrates a method to report on two files with one to many relationships. The program was originally written for a school that employs sales people to recruit students. It generates a report that details all the students recruited for each recruiter and the current session. There are two files used. One is Recruit.DBF and the other is Referral.DBF. The two files contain a common field, Ref_code, a unique code assigned to each recuiter. In the Referral.DBF file, Ref_code indicates which recuiter signed up that particular referral. For each record in the Recruit file there may be several associated records in the Referral file. The Recruit database file contains information regarding the sales people: name, address, city, state, zip, and the unique referral code (Ref_code). The Referral database file contains student and class information: name, school location, class level, and referral code (Ref_code), and is INDEXed ON Ref_code TO Code.NDX. As with any program, the first part sets up a specific environment. SET defaults are changed, the screen is CLEARed, the necessary files are opened, and the printer is accessed. All the memory variables utilized globally by the program are then initialized. A page counter is set up (m_pagectr) and a line counter is established (m_line). The line counter, m_line, is initialized to a large number so that the first page is EJECTed. Next, a loop is established to instruct dBASE III to process the commands until there are no more records in Recruit.DBF. This is indicated when the EOF() function returns a true value. The idea is to use Recruit.DBF as the point of reference into the Referral.DBF. The next step is to find and print all the records from the Referral.DBF file, that have the same Ref_code as the current record in Recruit.DBF. This is done by SELECTing the second file and SEEKing Recruit->Ref_code. Note the use of the alias name when SEEKing with a field form a non-SELECTed work area. A test must then be made to determine if there are any referrals for the current recruiter. The statement IF EOF() tests for this. If there are no referrals found by the SEEK command, EOF() returns a true (.T.). In this example, if a record was not found, dBASE III is instructed to SELECT Recruit.DBF, SKIP to the next record, LOOP around to the DO WHILE .NOT. EOF() statement, and start the process again. If a record is found, then the record pointer is positioned at the first occurrence of Ref_code in Referral.DBF. In order to display all the records that have the same referral code, it is necessary to enter a second DO WHILE loop that will skip through Referral.DBF until the Ref_code in this file no longer matches Recruit->Ref_code, or the end of the file is reached. The statement "IF m_line > 56" tests the line counter to determine whether or not to eject to a new page before printing. This is where the statement "m_line = 90" makes sense. Storing 90 to m_line every time a new record is selected from the Recruit file ensures that the first time the statement IF m_line > 56 is evaluated, headings will be printed since m_line is greater than 56. Notice that right after that IF statement m_line is reset to 11, the location where the first line of data will be printed. After the ENDIF, the first record from the Referral.DBF is printed, the line counter is incremented, and the record pointer SKIPs to the next record. It is necessary to increment the line counter so that the printer moves forward. When sending data to the printer for a custom report, @...SAYs should move from top to bottom and left to right. If an attempt is made to print to a row and column position that has already been printed on the current page, then a formfeed is sent to the printer. In our example, we use this concept to our advantage. The headings are set up to begin printing on line 1, which requires the printer to eject to the top of a new page if it isn't already positioned there. dBASE III stays in this loop until all the records from Referral.DBF that match Recruit->Ref_code are printed. Each time a new record is to be printed, m_line is tested, and ejects to a new page when appropriate. Once all the matched records are printed from Referral.DBF, the program SELECTs the Recruit.DBF file, moves the record pointer to the next record, and loops around to the first DO WHILE .NOT. EOF() to begin processing a new record. When all the records in Recruit.DBF have been reported on, an EJECT is sent to be sure that the last line of data is printed. The printer is then disabled (SET DEVICE TO SCREEN), the files are closed, and control is RETURNed to the calling program or the dot prompt. To test this program, add two or three records to Recruit.DBF with Ref_codes 101 and 102. Then add a few records to Referral.DBF and include the same Ref_codes, 101 and 102. Once this is done, INDEX Referral.DBF ON Ref_code TO Code.NDX. To execute the program, turn the printer on and type DO Report. Pseudocode 1. Set Up the Working Environment. Clear the screen. Define environmental SETtings. Define work areas. Open database files with appropriate indices. Define relations and filters. Select the initial work area. Initialize global variables. Set the page counter to 0. Set the line counter to a value greater than the page length. Direct output to the printer. 2. Print the Report. Process all the records in the primary work area. Select the secondary work area. Move to the first secondary record. If there aren't any. Select the primary work area. Move to the next primary record. Start the process over. Process all the records in the secondary work area that match the primary record. If the line counter is greater than the page length Advance the printer to a new page. Print page heading. Print page number and date. Print report title. Print field titles. Increment the page counter. Set the line count to the row position of the first detail line. Print one detail line. Increment the line count. Move to next secondary record. Select the primary area Move to next primary record. 3. Restore the Environment. Advance the printer to a new page. Direct output to the screen. Close all the work areas. Return to calling program or the dot prompt. Report The following command file was built from the pseudocode listed above. * Program..: Report.PRG * Author...: Karen A. Robinson * Date.....: October 1, 1985 * Notes....: USEs Referral.DBF and Recruit.DBF to generate a * custom report of student referrals by recruiter. * CLEAR SET TALK OFF * --- Open work areas. SELECT 2 USE Referral INDEX Code SELECT 1 USE Recruit SET DEVICE TO PRINT * --- Initialize memory variables for the page headings. STORE 1 TO m_pagctr STORE 90 TO m_line * --- Print all recruiters and their respective referrals. DO WHILE .NOT. EOF() * --- Calculate column positions so that the name and address * --- of the recruiter will be centered. STORE 40 - LEN(TRIM(Name))/2 TO m_1 STORE 40 - LEN(TRIM(Address))/2 TO m_2 STORE 40 - ((LEN(TRIM(City)) + LEN(State) + LEN(Zip) + 4)/2); TO m_3 * -- Find the first referral from Recruit.DBF. SELECT Referral SEEK Recruit->Ref_code * --- Test for the existence of a referral. * --- If one is not found, then get the next recruiter * --- and start the process over. IF EOF() SELECT Recruit SKIP LOOP ENDIF * --- The first referral has been found. The following * --- prints all the records from Referral.DBF that have * --- the same Ref_code as the Ref_code in Recruit.DBF. DO WHILE Ref_code = Recruit->Ref_code .AND. .NOT. EOF() * --- Test for a new page. If the line count exceeds * --- 56, a new page is required. IF m_line > 56 * --- Print page and column headings. @ 1, 28 SAY 'STUDENT REFERRAL REPORT' @ 1, 65 SAY 'Page ' + STR(m_pagctr,3) @ 2, 65 SAY 'Date ' @ 2, 72 SAY DATE() @ 4,m_1 SAY Recruit->Name @ 5,m_2 SAY Recruit->Address @ 6,m_3 SAY TRIM(Recruit->City) + ', ' +; Recruit->State + ' ' + Recruit->Zip @ 9,14 SAY 'Level' @ 9,23 SAY 'Center' @ 9,43 SAY 'Student Name' * --- Increment page and line counts. STORE 11 TO m_line STORE m_pagctr + 1 TO m_pagctr ENDIF * --- Print a referral, a record from the Referral * --- database file. @ m_line,14 SAY Level @ m_line,23 SAY Center @ m_line,43 SAY Name * --- Increment the line counter. STORE m_line + 1 TO m_line * --- Get the next Referral for the current * --- recruiter. SKIP ENDDO * --- Get the next recruiter. SELECT Recruit SKIP ENDDO * --- Restore the environment and return. EJECT SET DEVICE TO SCREEN CLOSE DATABASES RETURN * EOF Report.PRG Conclusion In this article, we have discussed custom reporting using Report.PRG as an example. To recapitulate, Report.PRG utilizies several reporting techniques. First, the program reports data from two database files that have a common field, Ref_code. Second, a page header is centered on the top of each page. Third, a line counter is incremented to a large number which causes the formatted report to print on a separate page for each record that meets a sepcified condition. You can change Report.PRG to fit your needs, replacing filenames and field names with those of your own. 5.2 Passing Parameters to Assembly-language Subroutines by Ralph Davis General Considerations With the introduction of the Developer's Release, dBASE III has acquired the ability to pass parameters to assembly language subroutines. In combination with the ability to LOAD and CALL the routines, you now have the possibility of much closer linking between a dBASE program and an assembler routine than was previously possible using the RUN command. The dBASE III Developer's Release Reference Manual documents this feature on pages 4-26A and 4-72A through 72C. It states that the address at which the variable is stored is passed in DS:BX; that you can pass any type of memory variable; that character variables terminate with the null character (ASCII zero); and that you must not shorten or lengthen the variables that you pass. In this article, I will explore some techniques for passing parameters to assembler routines from the Developer's Release of dBASE III, and make some suggestions for the most efficient ways to do this. How to Pass Variables One thing you will need to understand is the information dBASE III passes you. What form are the different variables in? What can you do with them? What are the best ways to do so? First of all, it is interesting to note that DS:BX points to the first byte of data, not the first byte of the variable. The first byte of all variables contains the length of the variable. You can obtain this easily by decrementing BX. For character variables, the length includes the null character string terminator at the end; therefore, the length you are concerned with is the length descriptor minus one. All other variable types--numeric, date, and logical--have a fixed length of eight bytes. Each one has a different format in memory. Secondly, although it appears that you can only pass one variable at a time, if you are methodical, you can pass as many variables as you want. As you might expect, variables are assigned to memory storage space sequentially. As long as they are not RELEASEd, and the memory space reassigned, they will be stored one right after the other. Therefore, by passing the first variable, you can easily locate all subsequent variables by using the length descriptors. Later in this article, I will go into greater detail about how to do this. Format of Variables in Memory Memory variables are stored in the following formats: 1. Character variables are stored with the length in the first byte, followed by the actual string, and ending with 00. 2. Numeric variables are stored in IEEE long-real format. They use eight bytes, which appear in reverse order (most significant byte at the highest memory address). The first bit is the sign bit: 0 for positive and 1 for negative. The next 11 bits are the exponent (that is, the power of 2) plus 1023 decimal (3FF hexadecimal). The final 52 bits are the mantissa, the part of the number following the decimal point. The actual value of the number is computed as 1. times 2 to the exponent-1023). For instance, 2 (1.000 times 2^1) is stored as: 00 00 00 00 00 00 00 40 Negative four, -4, (-1.000 times 2^2) is stored as: 00 00 00 00 00 00 10 C0 Notice that the bits of the mantissa represent negative powers of 2, from 2^(-1) (1/2) to 2^(-52) (approximately 1 divided by 4.5 quadrillion). 3. Logical variables are stored as eight bytes, apparently to reserve space for redefining them. All PUBLIC variables are initially defined as logical type with a value of false until they are given a value. The first byte is 01 for .T. and 00 for .F. This is the only byte you need be concerned with. 4. Date variables are also stored in reverse order MSB last) in encoded binary form. Suggestions Passing Multiple Variables When you pass a variable to an assembly language subroutine, you do so by issuing the command: CALL WITH For instance, if you store 'JOHN Q. PUBLIC' to mem1 and then CALL a routine with mem1, when your routine gains control, DS:BX will be pointing to the J in JOHN. Decrement BX to retrieve the length descriptor, in this case 15 (0F hex). To re-emphasize: the length includes the 00 which terminates all string variables. The actual string is only 14 characters long. If you initialize several memory variables at once, then CALL a routine with the first one, you can use the length descriptor to locate the next one. If, after storing 'JOHN Q. PUBLIC' to mem1, you store 'LOS ANGELES' to mem2 and 'CA' to mem3, you can pass mem1 to your routine, and locate mem2 and mem3 in the following manner. Decrement BX to get mem1's length. Add it to BX. Add one to BX for the terminating descriptor. Now BX is pointing to mem2's length descriptor. Add this value to BX and add one again; you are now pointing to mem3's length descriptor. To pass multiple variables in this fashion, it is critical that you initialize them one right after the other. For example: STORE 'JOHN Q. PUBLIC' TO mem1 STORE 'LOS ANGELES' TO mem2 STORE 'CA' TO mem3 Then you can CALL your routine and easily locate mem1, mem2, and mem3. If, however, you RELEASE and then redefine a variable, you will not be able to do this. The following sequence will not work: STORE 'JOHN Q. PUBLIC' TO mem1 STORE 'LOS ANGELES' TO mem2 ... RELEASE mem1 STORE 'CA' TO mem1 I therefore suggest that when you pass variables to subroutines, STORE all of them immediately prior to invoking the routine. Even if the variables have been defined elsewhere, redefine them here with new names. Then replace the old variables upon returning from the routine. This will give you the greatest degree of control over the variables you pass to your routine. Passing Different Variable Types As I mentioned earlier, numeric variables are stored in IEEE format, and dates are stored in an encrypted binary form. Both of these formats are cumbersome to work with. So I recommend that you pass all date and numeric variables as characters. There will undoubtedly be applications where you will want the actual binary representation of numeric or date information-- ones using the 8087 or 80287, for example--and in these instances, of course, it is appropriate to pass the variable in its actual form. But for most purposes, it is much easier to pass the ASCII codes, then process them as needed in your routine. Since the only part of a logical variable that concerns us is the first byte, it is best to pass it as is. You then return 0 for false and 1 for true. The program Printchk.ASM, listed later in this issue, passes a logical variable, and determines program action based on the value returned by the assembler routine. Checking Printer Status by Ralph Davis Introduction We are all human, and need not be ashamed to admit that sometimes we try to print files when our printer is either turned off or not ready. If we do this from dBASE II or III, we may soon be facing the DOS prompt. Neither dBASE II nor III checks the printer before trying to print. The error recovery is left to DOS. If the printer is off-line, DOS issues the familiar "Write fault error writing device PRN Abort, Retry, Ignore" message. Turning the printer on-line and pressing "R" for "Retry" resumes the printing operation, and all goes well. Pressing "A" or "I," however, returns us to the operating system prompt, and may cause database file corruption. The following assembler routine, Prntchk.ASM, checks the printer status port for LPT1 and controls program flow according to its findings. It is written according to the specifications outlined in the September issue of TechNotes, with conditional assembler directives to permit the same source code to create programs either for dBASE II, dBASE III 1.0 and 1.1, or dBASE III Developer's Release. It functions somewhat differently in each environment. The program is called Prntchk rather than Printchk because of an anomaly in the Developer's Release with the LOAD command which obliges us to name LOAD modules with seven characters or less. Keep this in mind when naming programs for use with dBASE III. Discussion of Prntchk.ASM When you use Prntchk with dBASE II or pre-Developer's Release versions of dBASE III, you RUN it or QUIT TO it after assembling it as a .COM file. This version of the program checks the printer status and does one of three things: 1. Reports that the printer is available, and returns control to dBASE II or dBASE III. 2. Reports that the printer is turned off, advises you to turn it on, and retains control until it is turned on. 3. Reports that the printer is off-line and retains control until it is on-line. Notice that in cases 2 and 3, the program does not relinquish control. After issuing its report, it waits for you to adjust the printer and press any key to continue. It will not release control until it obtains a ready report from the printer. Thus it assures that dBASE will not attempt a printing operation until the printer is ready to carry it out, protecting your files from potential data loss. The program operates somewhat differently with the Developer's Release. Here, it initializes a logical variable to false (.F.), then passes this variable to Prntchk. Prntchk checks the printer status, and places either a 0 (.F.) or a 1 (.T.) back into the variable. Upon return, your dBASE program can test this variable and determine program flow based on it. Here is a short section of code from a printing program that is based on this idea. SET TALK OFF LOAD Prntchk ok2print = .F. DO WHILE .NOT. ok2print CALL Prntchk WITH ok2print IF .NOT. ok2print ? ? 'Printer is not ready - PLEASE CORRECT' WAIT ENDIF ENDDO The program cannot get past this DO WHILE loop until the printer is ready for output. .LFCOND PAGE 60,132 ; D3DR EQU 1 ; Assemble for dBASE III, ; Developer's Release. COM EQU 0 TRUE EQU 1 ; Define symbols FALSE EQU 0 ; for .T. and .F. ;************************************** CODESEG SEGMENT BYTE PUBLIC 'CODE' ASSUME CS:CODESEG,ES:CODESEG ;-------------------------------------- PRNTCHK PROC FAR IF COM ; ORG at 100H ORG 100H ; for .COM file. ENDIF START: JMP NEXT_STEP ; Skip past data. ; MESS1 DB 'PRINTER OFF-LINE - PLEASE ADJUST',0DH,0AH,'$' MESS2 DB 'PRINTER NOT TURNED ON - PLEASE TURN IT ON' DB 0DH,0AH,'$' MESS3 DB 'PRINTER AVAILABLE FOR PRINTING',0DH,0AH,'$' MESS4 DB 'PRESS ANY KEY TO CONTINUE',0DH,0AH,'$' OK DB ? ; NEXT_STEP: PUSH AX ; Save registers. PUSH BX PUSH DS PUSH ES PUSH CS POP ES MOV AX,40H ; Point to system data segment. MOV DS,AX MOV SI,8 ; Point to LPT1 port address. MOV DX,[SI] ; Load it into DX. INC DX ; Point to LPT1 status port IN AL,DX ; and read it. CMP AL,0DFH ; Printer OK? JNE OFF_LINE ; No, is it off-line? MOV ES:OK,TRUE ; Place .T. in temporary variable. IF COM MOV DX, OFFSET MESS3 ; Print report if .COM file. CALL PRINTMESS ENDIF JMP SHORT EXIT ; Return to dBASE. OFF_LINE: CMP AL,0CFH ; Is printer off-line? JNE TURNED_OFF ; No, it must be turned off. MOV ES:OK,FALSE ; Place .F. in temporary variable. IF COM MOV DX,OFFSET MESS1 ; Print report if .COM file. CALL PRINTMESS MOV DX,OFFSET MESS4 CALL PRINTMESS CALL CRLF ; Skip line. POP ES POP DS POP BX POP AX CALL WAIT ; Wait for keypress. JMP NEXT_STEP ; Go back and check status again. ENDIF JMP SHORT EXIT ; Leave if OK. TURNED_OFF: MOV ES:OK,FALSE ; Place .F. in temporary variable. IF COM MOV DX,OFFSET MESS2 ; Print report if .COM file. CALL PRINTMESS MOV DX,OFFSET MESS4 CALL PRINTMESS CALL CRLF ; Skip line. POP ES POP DS POP BX POP AX CALL WAIT ; Wait for keypress. JMP NEXT_STEP ; Go back and check status again. ENDIF EXIT: POP ES ; Restore registers. POP DS ; Now DS and BX are pointing POP BX ; to variable passed by dBASE III. IF D3DR MOV AL,ES:OK ; Get .T. or .F. from temporary ; variable. MOV BYTE PTR [BX],AL ; Place it in dBASE variable. ENDIF POP AX ; Restore remaining registers. IF COM INT 20H ; INT 20H if .COM file. ELSE RET ; Far return if Developer's Release. ENDIF ; PRNTCHK ENDP ;---------------------------------------- ; SUBROUTINES ;---------------------------------------- CRLF PROC NEAR ; Skips line. PUSH AX ; Save registers. PUSH DX MOV DL,0DH ; Print carriage return. MOV AH,2 INT 21H MOV DL,0AH ; Print line-feed. MOV AH,2 INT 21H POP DX ; Restore registers. POP AX RET ; Return to caller. CRLF ENDP ;---------------------------------------- PRINTMESS PROC NEAR ; Print message. PUSH AX ; Save registers. PUSH DS MOV AX,ES ; ES points to Prntchk's MOV DS,AX ; data. XOR AX,AX ; Zero AX. MOV AH,9 ; Call DOS print string function. INT 21H POP DS ; Restore registers. POP AX RET ; Return to caller. PRINTMESS ENDP ;---------------------------------------- WAIT PROC NEAR ; Waits for keypress. ; Does not check for Ctrl-Break. PUSH AX ; Save register. MOV AH,1 ; Call DOS, wait for keypres. INT 21H ; Don't check Ctrl-Break function. POP AX ; Restore register. RET ; Return to caller. WAIT ENDP ;---------------------------------------- CODESEG ENDS ;****************************************** END START 6 dBASE III Version 1.1 Change Summary