Another Wordwrap BY KENNETH N. GETZ AND KAREN ROBINSON In earlier issues, TechNotes has presented articles that described word-wrapping routines written in the dBASE III PLUS interpretive language--effective routines, but somewhat slow. Here we'll present an alternative approach: an assembler routine that you can LOAD and CALL to wrap long strings. Wordwrap.ASM is an assembly language program that takes a long string and returns a wrapped string. The dBASE III PLUS program that you use to print the wrapped strings must define the margin settings, CALL Wordwrap to parse the text, and print the string returned by Wordwrap. Wraptest.PRG, a dBASE III PLUS sample program, demonstrates how to use Wordwrap. Wordwrap.BIN Wordwrap accepts up to 254 characters at a time and parses the string according to the line width you specify. For example, if the character string is 254 characters wide and your line width is 65, Wordwrap returns to the calling program 65 characters at a time. If the sixty-fifth character falls in the middle of a word, it wraps that word to the next line. You CALL Wordwrap WITH the character string you want to wrap as the parameter. Although you pass only one parameter on the command line, Wordwrap uses three: "linewidth" is the ASCII representation of the desired line width. "buffer" is a temporary buffer area used by Wordwrap to hold portions of the string when parsing. "wrapstring" is the character string to wrap. In the program that CALLs Wordwrap, you must declare these variables consecutively, as shown below. (These variable names are arbitrary.) linewidth = CHR(x) wrapstring = SPACE(254) buffer = SPACE(n) where x is the line width you specify and n is equal to 254 minus the width of the field to wrap. The order in which you declare these variables is important because Wordwrap locates them by their position in memory. Once you declare these memory variables, do not change the size of "linewidth" or "buffer." Note that you initialize "linewidth" using the ASCII representation because the value is stored in one byte, making access to it simple at the assembler level. Sending Parameters from dBASE III PLUS Because Wordwrap.BIN requires the use of three parameters, it is useful to understand the concept of passing parameters to and from external procedures. When you CALL a procedure WITH a parameter, the address of the parameter is passed to the procedure in the DS:BX register pair. Be aware that the DS register references the data segment from dBASE III PLUS, not the data segment of the CALLed program. One option is to pass multiple parameter strings as one string (as done in dBASE TOOLS for C) and then parse the multiple parameter string in the CALLed program. The second option is to declare dBASE III PLUS variables in strict order so that they are stored contiguously in memory. The assembly routine can then access the secondary parameters once it has established the location of the parameter passed to it on the command line (the DS:BX register pair). Wordwrap uses the second method. Variables are stored in strict order and located by their relative positions in memory. To be sure that you don't make mistakes in external procedures, you need to understand how dBASE III PLUS stores character variables in memory. Each character variable has two extra bytes, one at the beginning and one at the end. The byte at the beginning contains the length of the string and the byte at the end is a null (00 hex) byte that terminates the string. The length of the string includes the null byte; so, the length value is one greater than the number of characters in the string. The diagram below illustrates how the following two variables are stored in memory: linewidth = CHR(65) wrapstring = SPACE(254) buffer = SPACE(120) {diagram here} This method of passing multiple parameters is risky: It is easy to lose track of where the variables and null bytes are stored in memory. Remember: Once the variables "linewidth" and "buffer" are declared, do not reassign values to them. Setup Before you use Wordwrap.BIN, you must fulfill these basic requirements. First, your computer must have at least 320K of RAM. Second, in order to assemble Wordwrap.ASM as a .BIN file, you need the IBM or Microsoft Macro Assembler (MASM.EXE), version 2.0 or higher. The smaller assembler, ASM.EXE, will not suffice because it does not support macros, and Wordwrap.ASM makes extensive use of macros. You also need the programs LINK.EXE and EXE2BIN.EXE, which are included as supplemental programs with DOS 2.0 and higher. Follow the steps below to assemble Wordwrap.ASM, LINK, and convert it to a LOADable .BIN file. 1. From the DOS prompt, type: MASM WORDWRAP.ASM; This creates an object file (Wordwrap.OBJ). 2. Then type: LINK WORDWRAP; This creates Wordwrap.EXE. 3. To create a .BIN file, type: EXE2BIN WORDWRAP.EXE 4. Last, delete Wordwrap.EXE: ERASE WORDWRAP.EXE The semicolons used in the MASM and LINK command lines suppress any interactive prompts for additional parameters. ; Program ...: Wordwrap.ASM ; Author ....: Kenneth N. Getz ; Date ......: February 1, 1987 ; Version ...: dBASE III PLUS ; ; ;***************************************************************************** SAVE MACRO R1,R2,R3,R4,R5,R6,R7,R8 IRP X, IFNB PUSH X ENDIF ENDM ENDM ;----------------------------------------------------------------------------- RESTORE MACRO R1,R2,R3,R4,R5,R6,R7,R8 IRP X, IFNB POP X ENDIF ENDM ENDM ;***************************************************************************** CODESEG SEGMENT BYTE PUBLIC 'CODE' WRAP PROC FAR ASSUME CS:CODESEG START: JMP SHORT BEGIN LEN DB ? ; A storage space for the w length. BEGIN: PUSH DS POP ES ; Make ES = DS so we can refere DI. SAVE AX,DI,SI,CX CLD ; Clear direction flag. CALL GETBUFFER ; Point SI to first char Wrapstring ; and DI to first char in Buffer. CALL ZEROSTRING ; Zero out Buffer string. CALL FINDSPACE ; Point SI to character after last ; space in Wrapstring. CMP AL,32 ; See if we searched for a space. JNE FINISH ; If not, we're done. CALL COPYSTR ; Copy from SI to DI. FINISH: RESTORE AX,DI,SI,CX RET ;----------------------------------------------------------------------------- FINDSPACE PROC NEAR ; ; Findspace finds the next space at which to wrap. ; The SI register ends up pointing to the character after the last space, ; if there are any spaces in the section between the beginning of wrapstr ; and the linewidth. It doesn't deal at all with the problem ; of no spaces in the wrapping region. That's ; up to you. ; SAVE DI,CX MOV AL,[LEN] ; AL := wrap length. CMP AL,BYTE PTR [BX-1] ; Greater than length WrapString? JAE DONE ; If not, Return, MOV DI,BX ; Otherwise, point DI WrapString. AND AH,0 ; Zero high byte. ADD DI,AX ; Point DI to last possi character. AND CH,0 ; Zero high byte. MOV CL,[LEN] ; Move WrapLength into CX. DEC CL ; It's one longer than length word. MOV AL,32 ; Look for a space. STD ; Set flag to move backwards. REPNE SCASB ; Search for space. INC DI ; DI points to space. INC DI ; DI points to char after space. MOV SI,DI ; Need SI to point to gi character. DONE: CLD ; Clear direction flag. RESTORE DI,CX RET FINDSPACE ENDP ;----------------------------------------------------------------------------- ZEROSTRING PROC NEAR ; ; Zeroes out strings with nulls. It expects DI to point to beginning string ; to be cleared out. ; SAVE CX,AX,DI AND CH,0 ; Clear out top byte. MOV CL,[DI-1] ; Get length byte. DEC CL ; Don't clear out null at end. MOV AL,32 ; Move space to AL for clear out. REP STOSB ; Put in CX number of spaces. RESTORE CX,AX,DI RET ZEROSTRING ENDP ;----------------------------------------------------------------------------- GETBUFFER PROC NEAR ; ; Getbuffer expects BX to point to beginning of main string, returns w DI ; pointing to the first character in buffer, and SI pointing to fi character ; in main string. ; SAVE CX,AX MOV DI,BX ; Point DI to beginning WrapString. STD ; Set direction flag to w backwards. AND AL,0 ; Move 0 into AL for REPNE MOVSB. MOV CX,0FFFH ; Move a big number into CX. REPNE SCASB ; Find first null (end of Buffer). REPNE SCASB ; Find real null (end WrapLength). MOV AL,BYTE PTR [DI] ; Move length into AL. MOV LEN,AL ; Store away wrap length. ADD DI,3 ; Point DI to start of buffer. MOV SI,BX ; Point SI to beginning WrapString. CLD ; Clear direction flag. RESTORE CX,AX RET GETBUFFER ENDP ;----------------------------------------------------------------------------- COPYSTR PROC NEAR ; ; Expects to find SI pointing to main string somewhere, DI pointing buffer's ; first char. Copies out the 'extra' part of the WrapString to the Buffer ; and terminates the WrapString at the wrap position. ; SAVE AX,CX,DI,SI ; Must save SI since MOVSB mo it. AND CH,0 ; Clear out top byte. MOV CL,BYTE PTR [BX-1] ; Put length of original into CX. MOV AX,SI ; Offset of starting byte in AX. SUB AX,BX ; Subtract offset of first byte. AND AH,0 SUB CX,AX ; Substract difference from to len REPNZ MOVSB ; Move from SI to DI. DEC DI ; Move back to NULL byte. MOV BYTE PTR [DI],' ' ; Get rid of final null byte. POP SI ; Get back to end of WrapString. DEC SI ; Back up one space. MOV BYTE PTR [SI],0 ; Put a NULL there to indicate end. RESTORE AX,CX,DI RET COPYSTR ENDP ;----------------------------------------------------------------------------- WRAP ENDP CODESEG ENDS END START Wraptest.PRG Wraptest.PRG is a dBASE III PLUS program that demonstrates how to use Wordwrap. When you execute Wraptest, it prompts you for the line width and page offset (left margin). It then declares the necessary memory variables ("linewidth," "buffer," and "wrapstring") and CALLs Wordwrap for each record in Wrap.DBF. Wordwrap accepts the text, storing it in a buffer variable. It parses the string according to the linewidth and sends back a string for Wraptest to print. Wraptest then calls Empty.PRG to clear "buffer" before accepting the next record. To use Wraptest, you need a text file of information that you want to print out and a database file named Wrap.DBF with one character field called "Line." The width of "Line" can vary, but the sum of "linewidth" and "Line" cannot exceed 254 characters. In other words the following expression must evaluate to true (.T.). LEN(Line) = 254 - linewidth If linewidth = 35, the maximum length of Line is 219. If the sum of "linewidth" and LEN(Line) exceeds 254, dBASE III PLUS returns the error message "***Execution error on +: Concatenated string too large." Use APPEND FROM SDF to read your text file into Wrap.DBF. Execute the program with the command line: DO Wraptest * Program ...: Wraptest.PRG * Author ....: Kenneth N. Getz * Date ......: February 1, 1987 * Version ...: dBASE III PLUS * Note(s) ...: This program demonstrates how to use Wordwrap.BIN, * an assembly language program that wraps text according * to the specified line width and offset. It assumes that you * have available the database file, Wrap.DBF, which has one * field named Line and which already contains the text to wrap * SET TALK OFF LOAD Wordwrap SET PROCEDURE TO Empty USE Wrap * ---Define formatting memory variables. pagewidth = 80 linewidth = 0 offset = 0 * ---Get line width and offset, checking to make sure that * ---the text will fit within the page width (pagewidth). If it * ---will not fit, adjust the offset to accommodate. CLEAR INPUT "Enter the line width:" TO linewidth INPUT "Enter page offset: " TO offset offset = IIF(linewidth + offset > pagewidth, pagewidth - linewidth, offset) CLEAR * ---Initialize the variables that Wrap.BIN will use. They must be * ---created in the order shown below. width = CHR(linewidth) buffer = SPACE(254) wrapstring = SPACE(254) DO WHILE .NOT. EOF() * ---If the field is empty, print a blank line. This is important for * ---separating paragraphs with a blank line. IF LEN(TRIM(Line)) = 0 DO Empty WITH 0 ? SKIP LOOP ENDIF * ---If buffer is empty, store the contents of the next record to * ---wrapstring. If not, add to wrapstring a space and the * ---contents of the next record. * ---Line is the field name. wrapstring = IIF(LEN(TRIM(buffer)) <> 0, TRIM(buffer) + ' ' ; + TRIM(Line),TRIM(Line)) CALL Wordwrap WITH wrapstring @row(), offset SAY wrapstring * ---If there is data left in the buffer, CALL Wordwrap * ---again until length of buffer is less than linewidth. DO Empty WITH linewidth SKIP ENDDO * ---If there is data left in the buffer, CALL Wordwrap again until * ---the length of buffer is less than 0. DO Empty WITH 0 CLEAR ALL RETURN * EOP Wraptest.PRG * Program .....: Empty.PRG * Author ......: Kenneth N. Getz * Date ........: February 1, 1987 * Version .....: dBASE III PLUS * Note(s) .....: This procedure is called by Wraptest.PRG to clear the * variable buffer. * PARAMETERS linewidth DO WHILE LEN(TRIM(buffer)) > linewidth wrapstring = TRIM(buffer) CALL Wordwrap WITH wrapstring @ ROW() + 1, offset SAY TRIM(wrapstring) ENDDO * EOP Empty.PRG Other Uses of Wordwrap and Wraptest Wordwrap can be useful for many different applications. As the Wraptest.PRG program demonstrates, you can APPEND a text file of information into a database file and then print that database file as one continuous document. You can also use Wordwrap to wrap one record at a time, as you would when printing a report such as this one: In this example, Description is a 220-character field that should wrap within itself, instead of all the Description fields printing together and wrapping as one continuous paragraph. You can easily modify Wraptest.PRG to accommodate this type of report. Within the DO WHILE .NOT. EOF() loop, use ROW() or PROW() to place the additional fields in the correct columns. Initialize "linewidth" to the length that you want Description to be (in this case 33) and "offset" to the column position where you want Description to start printing. In addition, in order to empty the buffer completely between records, modify the command DO Empty WITH linewidth to read DO Empty WITH 0 This modification separates the records into distinct items to wrap, printing the contents of the buffer before SKIPping to the next record. Here is an example of the changes you must make to the DO WHILE loop in order to produce the example report: DO WHILE .NOT. EOF() @ ROW(), 6 SAY Date @ ROW(), 17 SAY Hours * ---Line is the field to word wrap. wrapstring = IIF(LEN(TRIM(buffer)) <> 0, TRIM(buffer) + ; " " + TRIM(Line), TRIM(Line)) CALL Wordwrap WITH wrapstring @ ROW(), offset SAY wrapstring * ---Empty the buffer before going to the next record. DO Empty WITH 0 * ---Print a blank line between records. @ ROW() + 2, 0 SKIP ENDDO Conclusion Further Information: See "A Procedure for Wrapping Long Strings" in the April 1985 issue; "LISTing a Database File in a Vertical Form" in the April 1986 issue; and "Mailmerge Application Programming" in the September 1986 issue for other algorithms and applications using word wrapping.