1571-5.TXT rev 1 96-11-06 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * THIS DOCUMENT IS COPYRIGHT (C) 1988, 1996 BY HERNE DATA SYSTEMS LTD. THE MATERIAL CONTAINED HEREIN MAY BE FREELY USED FOR PERSONAL INFORMATION ONLY. IF YOU REPRODUCE IT, THIS COPYRIGHT NOTICE MUST NOT BE REMOVED. THIS MATERIAL MAY NOT BE EXPLOITED FOR COMMERCIAL PURPOSES. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Herne Data Systems Ltd., PO Box 250, Tiverton, ON N0G 2T0 CANADA. Voice/fax 519-366-2732, e-mail herne@herne.com, internet: http://www.herne.com * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Effective Use of Files The 1571 disk operating system recognizes a number of different types of files: sequential, user, program and relative. Each file type is denoted in the disk directory listing by the corresponding 3 character code listed after the filename: SEQ, USR, PRG, and REL. Each file type is normally reserved for a different primary use. However, this does not mean that a given file type cannot be used for a different job. This chapter will outline how to create and access each of the file types with BASIC. Examples will be given of both standard and non-standard uses for these files. Other file types, such as VLIR (very large indexed relative used by the GEOS operating system), "random files" and CP/M files are not discussed here. (The random file is not really a file in the true sense, but a loose collection of user associated sectors on the disk.) SEQuential Files: Sequential files are primarily used for the storage and retrieval of data used by other programs. The data can be numeric or text strings, or both in any combination. As its name might suggest, the data in a SEQ file are read or written in a pre-determined sequence. In order to get to an item in the middle of a SEQ file, you must first correctly process (that is, either read or write) everything in the file preceding that point. Sequential file access is a three step process: 1. open the file 2. read OR write the data 3. close the file After each step, it is a good idea to check the disk drive error status through either the command channel (BASIC 2.0) or the reserved disk status variables DS and DS$ (BASIC 3.5 or 7.0). This is especially important after write operations and will keep you informed about potentially disasterous situations such as writing to the wrong disk, disk full, bad or unformatted disks, hardware malfunctions, etc. OPENing the File A SEQ file can only be accessed through an appropriate OPEN type statement. (You cannot use LOAD or SAVE type commands for a SEQ file.) In addition, the file can be opened for either reading OR writing, but not both simultaneously. (If you wish to include simultaneous input and output capability for your data files, then they should be initially created as relative files. These are described later in this document.) Several options are available for opening a SEQ disk file, depending on what action is to be taken with the file after it has been opened. The following are some typical examples for opening a file in read mode on device 8, drive 0: 10 OPEN 8,8,8,"PEACHES,S,R" 10 DOPEN#1,"PLUM,S" 10 OPEN 1,8,4,"0:ACCOUNTS,S" 10 DOPEN#2,"PAYMENT*" In each of the above examples, the ",S" and ",R" are optional. BASIC will default to the read option if "R" is not specified. In the last example, since "S" is not specified, BASIC will open the first file which matches the specified file name pattern, regardless of its type. If "S" is explicitly specified, as in the other examples, and the file listed in the directory under the name that you requested is not a SEQ file, then a DOS error code 64, FILE TYPE MISMATCH, error will occur along with a BASIC "FILE NOT FOUND" error. The DOPEN command is not available in BASIC 2.0. Note also that all of the examples are preceded by a line number. This is because normally you can read a file in program mode only. BASIC will not let you use INPUT# or GET# in immediate mode. If you are using BASIC 2.0, the disk command channel should also be opened at this time to allow reading of the disk error status. The typical command used is: 20 OPEN 15,8,15 The disk error status is then read using a standard INPUT# statement: 30 INPUT#15,E1$,E2$,E3$,E4$ E1$ will represent the disk error code number, E2$ the description of the error and, E3$ and E4$ the track and sector where the error occured (if applicable). In BASIC 3.5 or 7.0, you can check the error status with DS (disk error code) or DS$ (error code plus description). The BASIC 2.0 DOS wedge can also be used for reading the error status without opening the command channel. The following examples illustrate opening a SEQ file for writing: OPEN 8,8,8,"PEACHES,S,W" DOPEN#1,"PLUM,S",W OPEN 1,8,4,"0:ACCOUNTS,S,W" DOPEN#2,"PAYMENT",W Files can be opened for writing in either program or immediate mode. With the BASIC 2.0 version, both "S" and "W" must be specified to open a sequential file for writing. If "S" is omitted from the OPEN or DOPEN command, BASIC will automatically default to a SEQ file. Note that wildcards and pattern matching are not allowed in filenames opened for writing. In each of the above examples, a new file bearing the indicated filename will be created for subsequent writing. If a file of the same name already exists in the disk directory (regardless of its type), a DOS error code 63, FILE EXISTS, error will be generated. However, the file number specified in the OPEN statement is still active to BASIC. You must first CLOSE this file before attempting to re-open a new file with the same number under a different name. If you attempt to write to the original file, the computer and/or disk drive may lock-up. The 1571 also allows SEQ files to be opened for appending data to the end of the existing file. The following are examples of this procedure: OPEN 8,8,8,"PEACHES,S,A" APPEND#1,"PLUM,S" OPEN 1,8,4,"0:ACCOU*,S,A" Note that in the third example a wild card character is used. Wild cards are allowed with the append option, but the same restrictions outlined above for opening a file in read mode apply. The fourth mode of the OPEN command, M for MODIFY, is designed for recovering information from improperly closed or "SPLAT" files. These are files which are indicated in the directory with an asterisk (*) next to the file type. The MODIFY mode of 1571 SEQ file handling allows you to read and recover the data in such a file for processing and re-writing to another file. The following are examples of such an open statement: 10 OPEN 8,8,8,"SPLAT FILE,S,M" 10 OPEN 1,8,3,"SPLAT*,S,M" In each case, the file type ("S") can be replaced by any other valid file type (U, P, or R). Once a file has been opened in the modify mode, it can be read just like any other data file. Care should be taken to check the data in the file to ensure that the bytes transferred still represent desired data. Because a SPLAT file will not have a valid end of file marker (EOF), it may contain some garbage bytes at the end. These should be discarded before writing the new file. More on SPLAT files can be found in section 7.1.5 of this chapter. Reading In BASIC, a disk file can only be read in program mode. Two commands can be used: INPUT# and GET#. Both commands are available in all versions of Commodore BASIC, however, the effect of the INPUT# command may vary slightly depending on the version of BASIC due to the length of BASIC's input buffer. On the C-128, the input buffer is 160 characters long. This is the same buffer which is used for input from the keyboard. (On the C-64 and VIC-20 it is 88 characters long.) Just like keyboard entry, the INPUT# command is limited to reading a maximum of that many characters at a time. If a character string in the disk file is longer, then you must either read the string with a series of GET#'s or insert one or more carriage returns into the string to divide it up into smaller chunks. The following are examples of INPUT# and GET#: 10 INPUT#3,A$,B$,C$,D 20 GET#3,A$,B$ The first example will read three strings and one numeric value from logical file 3. The second example will read the next two characters from logical file 3. Although both INPUT# and GET# work with either numeric or string arguments, it is often convenient to treat all disk data in string format, then convert to numeric form with the VAL(string) function where required. This is because BASIC will accept anything from disk as a valid string input but will produce a BASIC error code 24, FILE DATA, error if an attempt is made to read string data into a numeric variable. This is similar to the TYPE MISMATCH error generated when string data is entered from the keyboard in response to a prompt for numeric input. The following short example will read a specified SEQ file and print its contents to the screen: 10 INPUT"FILENAME TO READ";F$ 20 OPEN 8,8,8,F$ 30 GET#8,A$:IF ST THEN 50 40 PRINT A$;:GOTO 30 50 CLOSE 8:END The above example will work with all versions of Commodore BASIC. It will also work with all file types, although generally only SEQ and REL files will contain data that will make any sense when read and printed in this manner. (Remember: what you GET# is what you see!) In order for INPUT# to work correctly, the data in the file must be properly separated or "delimited". Failure to do so may result in an attempt to read more characters than the input buffer can handle, thus causing a BASIC error code 23, STRING TOO LONG error, or an attempt to read too many data items resulting in the equivalent to the keyboard input warning message EXTRA IGNORED and the loss of these data items. Even if these error conditions do not occur, the data from the file may be corrupted. Both numeric and string data will combine into longer non-usable entries if not properly delimited. Proper data delimitation is discussed in the next subsection on writing files. Delimitation is not required for reading a file with GET# commands since this method returns one character at a time, regardless of its value. The variable list included with the INPUT# statements must be properly formulated to match how the data are stored in the disk file. If sufficient data are not available in the file to fill all of the requested variables, INPUT# will essentially wait forever expecting to read more data from a file which has no more to give. GET# will return a null character for extra requests, but will not cause lock up. In addition, multiple data items which have been delimited in the disk file by commas must be read with a single INPUT# statement. Writing In BASIC, writing to SEQ files is done using the PRINT# (or PRINT#, USING;) command. A simple example would be: PRINT#1,A$ More frequently, a number of items will be written to the same data file. In this case, the data must be properly separated or "delimited" to allow the data to be read again correctly. Commodore BASIC and DOS recognize a number of delimiters. The simplest, and perhaps most versatile, is the carriage return (ASCII character code 13). A carriage return can be placed in a file by two methods: explicitly or automatically. The explicit method has the following format: PRINT#1,A$;CHR$(13);B$;CHR$(13);C$; ... etc The carriage return can also be asssigned to a string variable: CR$=CHR$(13) PRINT#1,A$;CR$;B$;CR$;C$; ... etc The second method of inserting carriage returns into a file takes advantage of the automatic carriage return added by BASIC to the end of a PRINT or PRINT# statement with no continuation character (, or ;) specified at the end of the variable list. For example: PRINT#1,A$ PRINT#1,B$ PRINT#1,C$ will automatically place a carriage return after each item. The second delimiter recognized by Commodore BASIC and DOS is the comma ",". This must be added to the file explicitly in a form such as: CO$="," PRINT#1,A$;CO$;B$;CO$; ... etc The third delimiter is the colon ":". This acts in its own peculiar manner: everything after a colon, up to a carriage return, will be skipped over. Data formatted in this manner can only be read with a series of GET#'s. Although all three of the above delimiters can be used to separate data, there is a significant difference in the way that the carriage return acts compared to the comma. Similar to the INPUT statement for keyboard data input, commas are used to handle multiple data items which will always be read by a single INPUT# statement, while carriage returns will work for both single and multiple INPUT#'s in any combination. The fourth type of delimitation involves the use of quotation marks around strings. The quotes can be used to delimit strings which contain commas or colons. If, for example, you wanted to write the following to a disk file as a single string: LAST NAME, FIRST NAME : GOLF HANDICAP it must be enclosed in quotes or else it will be interpretted as three different strings. Quotes must be added explicitly to a data file: PRINT#1,CHR$(34)+"LAST NAME, FIRST NAME : GOLF HANDICAP" +CHR$(34) In order to insert quotes, you must use the expression CHR$(34) or assign it to a string variable (such as Q$=CHR$(34)). You cannot insert a quote by simply printing it PRINT#1," because BASIC uses the quote mark to delimit string constants for printing. Alternatively, the trailing quote can be replaced by either an explicit or automatic carriage return. Unfortunately, there is no way to use INPUT# to read a string which contains a quote mark from a disk file. In this case, you must use a GET#. The PRINT# USING command can be used to write preformatted output to a disk file. This may seem to be of less use than sending such formatted output to the screen or printer, unless the file is intended to be input for a word processor document or another program which expects data in a rigid format. However, PRINT# USING can be used to delimit data. For example, the following statement will output properly delimited numerical data to logical disk file 1: PRINT#1,USING "####.##,";A,B,C Note the comma at the end of the definition string. Each item in the list will be printed separated by a comma. Alternatively, a carriage return can also be used in the following manner: B$="####.##"+CHR$(13):PRINT#1,USING (B$);A,B,C The use of "=" or ">" in the format definition string will allow text to be centered or right justified in the character field. Disk files can also be written with the aid of the CMD command which re-directs the normal screen output to an OPEN file. For example, the most common way to create a disk file listing of a BASIC program or assembly language source code (perhaps to include it in a document or to upload it to an electronic bulletin board system (BBS)) is: OPEN 8,8,8,"LISTING,S,W" CMD 8 LIST PRINT#8:CLOSE 8 This is generally done in immediate mode, although the same technique can be used in program mode to re-direct a program output from the screen to a disk file or printer. In this latter case, regular PRINT statements will send output to the open file. The CHAR statement used in BASIC 7.0 for placing text at certain locations on a text or graphics screen will always send output to the screen. CHAR cannot be used for writing to a disk file. Closing the File A SEQ file is closed using a standard CLOSE {file#} or DCLOSE type of statement. It is especially important to properly close a file which has been written to. Failure to do so will result in a SPLAT file which may lead to lost or corrupted data, not only in the guilty file but in other files on the disk as well. A simple PRINT# statement with not variable list is often used before closing the file. This ensures that the last of the disk buffer is written to the disk before the file is closed. An example would be: PRINT#1 CLOSE 1 The PRINT#1 statement in the above example is not applicable if the file was opened in read or modify mode. Recovering Data From SPLAT Files "SPLAT" files are created when a file is not properly closed after being written to. This is most frequently caused by sloppy file management practices or improper error recovery. The SPLAT file can be easily identified in the disk directory by an asterisk (*) next to the file type and 0 blocks showing for the file length. Even though 0 blocks are indicated in the directory entry, the file does contain data and has been allocated disk space. Usually this can be seen in a reduced value for the "blocks free" parameter in the directory listing. The correct method for recovering data from, then deleting, a SPLAT file is outlined below: 1. OPEN the SPLAT file in M for modify mode. 2. Read the data in the file with GET#'s and store it somewhere. 3. OPEN a new write file. 4. Write data to new file. 5. CLOSE the new file. 6. Validate (COLLECT) the disk to erase the SPLAT file. A SPLAT file can also be opened as a standard read file. However, a DOS error code 60, WRITE FILE OPEN, error will occur and the recovered data may not be as reliable as that read in the M mode. The following short program can be used for SPLAT file data recovery: 10 INPUT "SPLAT FILENAME";OF$ 20 INPUT "FILE TYPE (S, P, OR U)";FT$ 30 INPUT "NEW FILENAME";NF$ 40 OD=8 : ND=8 : REM DEVICE NUMBERS 50 OPEN 1,OD,5,OF$+","+OT$+",M" : REM OPEN IN MODIFY MODE 60 OPEN 2,ND,6,NF$+","+OT$+",W" 70 GET#1,A$ : PRINT A$; : PRINT#2,A$; 80 GET B$ : IF B$ THEN 100 90 GOTO 70 100 PRINT#2 : CLOSE 2 : CLOSE 1 This program will prompt you to enter the splat filename and type (S, P, or U), and the new filename. The new file need not be on the same device as the old (you can change the device numbers in line 40 if you wish). The splat file will be read character by character and printed to both the screen and the new file. When you see something on the screen that looks like it should not be part of the file (i.e. random garbage from the end of the splat file) you have come to the end of the usable data. (This method works best with files that contain alphanumeric text only.) Press any key to abort the file reading and the new file will be properly closed. At this point, the disk with the splat file should be VALIDATEd to remove the splat file. USeR Files: User files have no pre-defined structure: they can be totally defined by the user to suit virtually any purpose. They are sometimes used by commercial programs for data files because they can be easily distinguished in the disk directory by checking the file type. In most respects, they can be handled identically to SEQ files by merely changing the file type designator from S to U. USR files are also used by GEOS as the basis for VLIR files for data and application program storage. DOS Utility "&" Files USR files can also be used for some very special purposes such as automatic programming of the disk drive. In Commodore DOS these are known as ampersand or "&" files, due to the command used to execute them. The USR program is "loaded" into the drive's RAM and then executed with a command such as: OPEN 15,8,15,"&0:{filename}" Note that with the 1571, the "&0:" is not actually part of the filename. (This is different from earlier drives, such as the 1541, which required the "&0:" to be part of the filename). To create the file, a standard OPEN type statement is used, followed by PRINT#'ing the data to it, just like a SEQ file. For example the following forms can be used in BASIC 2.0 and 3.5/7.0 respectively. OPEN 8,8,8,"0:DOSPRG,U,W" DOPEN#1,"DOSPRG,U",W Note that the file type specifier "U" must be explicitly stated, otherwise a SEQ file would result. The format of a DOS "&0:" file must be strictly adhered to as outlined below. The file consists of a header, which tells DOS where to put the file in the drive's memory; the machine language program, in standard 6502 machine code; followed by a checksum byte. The specific byte usage is summarized in Table 7-1. Table 7-1: DOS "&" File Structure ............................................................. Byte Usage .............................................................. 0,1 Track and sector link (for long files) (normally handled by DOS automatically) 2,3 Load address (lo byte/hi byte). This is also the start of execution address. To avoid crashing, this should be one of the disk buffers. 4 Number of bytes to follow (except for checksum). 5-end Machine code bytes. end+1 Checksum = sum of lo and high bytes of bytes 2 to end added together. ................................................................ The length refers to the length of the machine language and can be up to 250 bytes long. If your program is longer, it must be broken up into 250 byte segments, each with its own length, load address and checksum bytes. This leads to the interesting observation that a DOS "&" file does not need to occupy contiguous areas of RAM. For example, the address bytes for the first sector might specify $400 as the starting point, while a second sector in the file may jump to $700. Since the 1571 is controlled by a 6502 processor, the code can be developed on any standard C-64/VIC-20/C-128 etc. type of assembler or monitor. Control is returned to DOS from your program by a simple RTS at the end. The file can be written as a byte string or you can use a sector editor. If using a sector editor, you must properly select the track and sector link bytes yourself. The check sum consists of the sum of bytes 2 through to the end of the machine language. (The address bytes, and length byte are included in the sum.) The low and high bytes of this sum are added together to form the checksum byte which is included at the end of the file. It should be noted that if you wish your program to remain active in the drive after it stops executing, you will have to allocate the buffer into which it was placed by opening it as a direct access buffer. If you do not do this, DOS will use the buffer for its own purposes and overwrite your program possibly causing a spectacular crash. The following simple example will turn off the re-try after disk errors to prevent head chatter on heavily copy protected disks: 10 OPEN 1,8,1,"0:KNOCK-IT-OFF,U,W" 20 FOR Z=0 TO 8:READ X:PRINT#1,CHR$(X);:NEXT:CLOSE 1 30 DATA 0, 3, 5, 169, 197, 133, 106, 96, 199 The machine code represented by the data bytes 169 to 96 is: 169 197 LDA #$c5 133 106 STA $6a 96 RTS The file will load and execute at $300. It is not necessary to protect this buffer, because the program is not required to be memory resident. The second example will turn off the "verify after write" of the 1571 for job buffer #0. This will greatly speed up writes to the disk from this buffer, but it should be used with extreme caution because it changes some very important vectors in the drive's operating system and may cause a crash if not used properly. It is shown here as an example only. Be aware of what you are doing and the possible consequences before trying to use it yourself. 1 REM CREATE & FILE 10 OPEN 8,8,8,"V-OFF,U,W" 20 FOR I=0 TO 32 : READ X : PRINT#8,CHR$(X); : NEXT 30 PRINT#8 : CLOSE 8 40 DATA 0, 6, 28 : REM FILE HEADER 50 DATA 120, 169, 13, 141, 169, 2, 169, 6 : REM CODE 60 DATA 141, 170, 2, 88, 96, 72, 165, 0 70 DATA 201, 160, 208, 4, 69, 0, 133, 0 80 DATA 104, 76, 222, 157 90 DATA 86, 0, 0 : REM CHECKSUM AND FILLER 1 REM TO USE THE FILE 10 OPEN 9,8,9,"#3" : REM RESERVE BUFFER #3 20 OPEN 15,8,15,"&0:V-OFF" : REM ACTIVATE CODE 30 (. . . the rest of your program) . . . 60000 PRINT#15,"UJ" : REM RESET DRIVE BEFORE GOING 60001 CLOSE 9 : CLOSE 15 Note that you should not close either the command channel or the buffer channel until just before exiting from your program. The above routine should be used in program mode only, for special cases, such as a disk copying program, which operate under well defined conditions. The program can cause considerable damage to a disk if not used properly. The source code for the machine language portion is as follows: .ORG $300 ; NOT RELOCATABLE SEI ; DISABLE INTERUPTS LDA #VEROFF ; REPEAT FOR HIGH BYTE STA $02AA CLI ; RESTORE INTERUPTS RTS ; AND EXIT VEROFF =* ; NEW INTERUPT HANDLER PHA ; SAVE .A LDA $00 ; GET JOB CODE FOR BUFFER #0 ; CAN USE OTHER BUFFERS ALSO CMP #$A0 ; CHECK FOR VERIFY BNE CONTINUE ; NO? THEN JUMP TO OLD INTERUPT EOR $00 ; YES? THEN CANCEL IT STA $00 ; AND STORE IT BACK CONTINUE =* ; EXIT TO OLD INTERUPT ROUTINE PLA ; GET BACK .A JMP $9DDE ; AND EXIT PRoGram Files: Program or PRG files generally contain an image or exact copy of a given area of a computer's RAM. The first two bytes of the file, known as the load address, contain the starting address (in standard low byte/high byte format) of the area of memory contained in the file. The potential length of a PRG file is limited only by the amount of RAM available in the computer. Since the maximum addressable RAM in the 65xx/75xx/85xx series of microprocessers used in most of the Commodore 8 bit computers is 64 K bytes (65535 bytes), a file of this type is, therefore, limited in length to somewhat less than 64 K bytes. (Actually, it is equal to 65535 - load address). Although a PRG file will normally contain a program (either machine language, tokenized basic or some other tokenized language), it can contain anything stored in the computer's memory. This may be a high resolution graphics screen, sprite definitions, color maps, text files from a word processor, assembly language source code, etc. It can even be used to store data from the variables definition area of the BASIC workspace. Creating a PRG File A PRG file is normally created with the SAVE type commands (SAVE, DSAVE, BSAVE, MONITOR-S, or their KERNAL equivalents). With a disk drive, a PRG file can also be used for storing data, identical to a conventional SEQ (sequential) file. (The DOS makes no distinction internally as to how data are stored in a SEQ and PRG file. The only difference to DOS is the value of the file type flag in the directory entry.) The procedure for accessing a PRG file as a data file is the same as for a SEQ file except that the {file type} specifier in the OPEN statement must be "P". PRG files can be either read or written this way. SEQ files are discussed in more detail in the first section of this chapter. The following example can be used to save a segment of memory to a disk file using BASIC 2.0. (In BASIC 3.5, this can be done with the MONITOR-S command, while in BASIC 7.0 it can be done with either MONITOR-S or BSAVE.) 10 INPUT"FILENAME";F$ 20 INPUT"STARTING ADDRESS";LO 30 INPUT"END ADDRESS";HI 40 OPEN 8,8,8,"0:"+F$+",P,W" 50 L2=INT(LO/256):L1=LO-L2*256 60 PRINT#8,CHR$(L1);CHR$(L2); 70 FOR I=LO TO HI:PRINT#8,CHR$(PEEK(I));:NEXT 80 PRINT#8:CLOSE 8 Although the example is given as a program, the same technique can be used in immediate mode if LO and HI are explicitly defined instead of INPUT from the keyboard. Reading a PRG File A PRG file is generally recovered from disk using the LOAD commands (LOAD, DLOAD, BLOAD, BOOT, RUN, MONITOR-L, or their KERNAL equivalents). A PRG file is the only type of file which can be recovered by the LOAD commands. It can, however, be opened and read with the SEQ file handling commands GET#, and even INPUT# if the data are in a suitable format. The following is a short BASIC 3.5/7.0 demonstration program which will open a PRG file as a sequential file, read the first two bytes and determine the load address. This is handy if, for example, you have forgotten the SYS address to start a machine language program, or you want to examine it with a disassembler and don't know where it resides in memory. 10 TRAP 70 20 INPUT"ENTER FILENAME";F$ 30 DOPEN#1,F$+",P" 40 GET#1,A$,B$:DCLOSE#1 50 AD=ASC(A$)+ASC(B$)*256 60 PRINT"PROGRAM "F$" START ADDRESS =";AD:END 70 PRINT"ERROR "ERR$(ER):RESUME 20 The TRAP statement in line 10 is used to divert the program to line 70 if an error occurs. The specific error that may be encountered is a "FILE NOT FOUND". This error will be detected by BASIC before it is picked up on the disk error channel. The equivalent program in BASIC 2.0 would be: 10 INPUT"ENTER FILENAME";F$ 20 OPEN 1,8,1,"0:"+F$+",P,R" 30 GET#1,A$,B$:CLOSE 1 40 AD=ASC(A$)+ASC(B$)*256 50 PRINT"PROGRAM "F$" START ADDRESS =";AD:END It should be noted that the BASIC 2.0 version will not trap the FILE NOT FOUND error. If the specified filename is not on the disk, the program will simply stop. Combining BASIC Programs Many people tend to write BASIC programs in a "modular fashion". You can create a library of standard subroutines, or program modules, to perform such tasks as selectively reading a disk directory, bubble sorting, disk file handling, screen formatting and graphics, etc. When creating the first draft of a new program, much of it can be produced by combining some of these standard subroutines. Customization and optimization are relatively minor tasks compared to retyping a number of twenty to thirty line, or more, subroutines every time you want to use them. The whole concept of combining several subroutines or program files into a single program usually depends on the existence of a BASIC command called MERGE, which performs this task automatically. Although Commodore DOS supports the combining of two or more data files (using APPEND or CONCAT), a MERGE command for BASIC programs is not available as an intrinsic command in any version of Commodore BASIC, not even the newest version 7.0 on the C-128. MERGE is available, however, with several BASIC extenders such as the Programmers' Aid Cartridges for the VIC-20 and C-64. In order to understand how appending BASIC programs works, we must first take a look at the structure of a Commodore BASIC program (PRG) file. A BASIC program line starts with two link bytes followed by two bytes representing the line number. This four byte sequence is followed by the text of the BASIC line with keywords in tokenized form (i.e. BASIC keywords, such as INPUT, PRINT, INT(, etc. are stored as single or double byte "tokens" rather than spelled out). The end of the line is designated by a zero byte (not the character zero "0", but ASCII character code zero CHR$(0)). The key to creating a program file with many lines is to establish the line links. These links are absolute addresses in low byte high byte format which tell the BASIC interpreter where to find the next BASIC line link. A program file ends with three zero bytes (an end of line zero byte plus two zero bytes to indicate that there are no more line links). Normally, when you add more BASIC lines from the keyboard or edit an existing line, the link bytes are automatically adjusted to reflect the new situation. In addition, in most Commodore computers, BASIC programs are relocatable when they are LOADed, that is, programs will be LOADed back into memory at the start of BASIC pointer, regardless of where they were originally SAVEd from. This allows you to temporarily move the BASIC workspace around in memory (to reserve memory for graphics screens, for example) and to LOAD programs written on one computer with a different one. In this process, the link bytes are automatically adjusted during program execution or LISTing to reflect the new RAM address locations. It is very simple to combine any number of program modules into a single BASIC program. Depending on the computer used and the length of the modules, one of several methods can be used. Although it will work with program modules of any length, the first method is generally only used when at least one of the program modules is relatively short (less than about 20 lines). This method, which incidentally can be used on any brand or model of computer which features a full screen BASIC editor, consists of only a few simple steps and does not involve any additional programming. The procedure is as follows: 1. LOAD the shorter program module into memory. 2. LIST on the screen the section of the module to be combined with the other program. If you are using a C-128, you should switch to the 80-column display if possible because you can fit more on the screen. You can even use both the 40 and 80 screens, if you have two monitors or a switchable one. On all Commodore computers you must leave at least five blank lines at the bottom of the screen. If you can't LIST everything on the screen at once that's OK, you can repeat steps 1 to 5 as many times as you wish. 3. Without erasing any of the LISTing of the first program on the screen, cursor up as far as you can go (it is OK to erase the "READY" message) and type in the command to LOAD the second program into memory. Normally when you do this, the program originally in memory will be erased and replaced by the newly LOADed program. Fortunately, some of the old program can be "protected" in the form of a screen listing. 4. With the cursor control keys, cursor up to the lines of the first program that you wish to add to the second and then press the key. Before pressing , you can change the line number or perform any other editing on the line that you wish. Do this for all the lines displayed on the screen that you wish to add to the second program. The lines from the first program will be added to the second program just as if you had re-typed them on the keyboard. 5. SAVE the combined program under a new or temporary filename. 6. Repeat steps 1 to 5 until all desired lines have been added to the new program. This technique takes advantage of the fact that the BASIC input editor can read data and program lines directly from the screen. LISTing a program, or a portion of a program, on the screen will "protect" the listed portion from being erased when you LOAD in a new program. The protection lasts only as long as the lines are displayed on the screen. When you clear the screen, the protected program will be erased also. Obviously, the longer the programs being merged with this technique are, the more times you will have to repeat the above procedure to complete the merger. The process is, however, extremely flexible in that it works on virtually any computer and you can select and edit the individual lines from one program to be merged with the other. The main disadvantage of the procedure is that you are limited in the number of lines which can be displayed on the screen and thus merged at a given time. The second method works on entire program files rather than just a segment of one. It involves LOADing one program into memory, setting the start of BASIC pointer to the end of the first program and LOADing the second program. The start of BASIC pointer is then re-set back to its original value and the combined program is SAVEd. This can be done in either immediate or program mode. On a C-64, C-16, Plus/4 or VIC-20, the immediate mode procedure is as follows: 1. LOAD"first program",8 This puts the first program into memory normally. This step may be skipped if the desired program is in memory already. 2. PRINT PEEK(43),PEEK(44) Note down these two numbers for future reference in step 7 (referred to as "{first#}" and "{second#}", respectively). 3. HI=PEEK(45)+PEEK(46)*256-2 This is the end of file address for the first file. Remember to include the "-2". This will compensate for the two zero bytes used as the end of file marker. 4. POKE 44,HI/256:POKE 43,HI-PEEK(44)*256 This sets the start of BASIC pointer to the end of the first file. 5. NEW This resets all of the other BASIC pointers to match the new start address. 6. LOAD"program 2",8 The second program is now loaded in at the end of the first. 7. POKE 43,{first#}:POKE 44,{second#} This resets the start of BASIC pointer to its normal location. The combined program is now ready to be SAVEd or used. The following short program can be used on the C-64 to perform the same task in program mode: 10 REM AUTO PROGRAM COMBINER 20 POKE 56,PEEK(44)+5:CLR:DE=8:L0=PEEK(56)*256+PEEK(55) :POKE L0-1,0 30 INPUT" ROOT FILE NAME >>";RF$ :INPUT" APPEND FILE NAME >>";AF$ 40 INPUT" MERGED FILE NAME >>";MF$ :F$=RF$:L=L0:GOSUB 60:L=MH-2:F$=AF$:GOSUB 60 50 SA=1:F$=MF$:L=L0:GOSUB 60:PRINT" ***DONE***":END 60 PRINT" *INSERT "F$" DISK*":PRINT"THEN PRESS A KEY" :POKE 198,0:WAIT 198,1 70 FOR I=1 TO LEN(F$):POKE 827+I,ASC(MID$(F$,I,1)):NEXT :POKE 780,LEN(F$): POKE 781,60 80 POKE 782,3:SYS 65469:POKE 780,1:POKE 781,DE:POKE 782,SA :SYS 65466:IF SA THEN 100 90 POKE 780,0:POKE 782,L/256:POKE 781,L AND 255 :SYS 65493:GOTO 120 100 POKE 252,L/256:POKE 251,L AND 255:POKE 780,251 110 POKE 782,MH/256:POKE 781,MH AND 255:SYS 65496 120 OPEN 15,8,15:INPUT#15,DS,DS$:CLOSE 15 :IF DS THEN PRINT" "DS;DS$:END 130 MH=PEEK(781)+256*PEEK(782):RETURN When you run the program, you will be prompted to enter three things: the ROOT file name; the APPEND file name; and the MERGED file name. The ROOT file is the program or module which contains the lowest line numbers as outlined below. The APPEND file is the program or module which contains the highest line numbers. The relative lengths of the ROOT and APPEND files are not important. The MERGED file name is the name under which you wish to store the new combined program file. All three file names should be different unless you want to use the infamous "SAVE with replace" DOS option for saving the new file. The example will also work on the VIC-20 and on the C-128 when it is in C-64 emulation mode. In addition, because of its simplicity of operation it is easily adaptable to any other Commodore computer which is capable of relocating BASIC programs during a normal LOAD. The program will prompt you to insert the disk with each file on it then press a key to continue. When the ROOT and APPEND files have been loaded, the program will prompt you to insert the disk to save the combined file on and then save it. The three files need not be on the same disk. There is one relatively minor restriction on the line numbering of the programs being appended either in immediate or program modes. Since these methods work by appending all of the lines in one program module to the end of the another program module, the line numbers of the second program module must all be greater than the line numbers of the first module. If you do not adhere to this restriction, some odd things may happen to the combined program. For example, when you list the program you may find that line 50 comes after line 100 or before line 20. This can be avoided by ensuring that the line numbers are in correct sequence before merging the programs. In BASIC 3.5 or 7.0, the combined programs can be renumbered with a RENUMBER command. If duplicate line numbers are included, as above, they will be correctly renumbered to reflect their sequence in the combined program but, references to the duplicate lines such as GOTO's and GOSUB's will not be correctly renumbered. The third way to combine program files on the C-128 is to use the programmable function keys to store the command sequence for the immediate mode method. The following is a short BASIC program which redefines function key and SAVEs the new key definitions in a disk file called "MERGE". Enter and run this program first to create the MERGE file. 10 REM FUNCTION KEY MERGE SETUP 20 A$=CHR$(13)+"POKE 250,PEEK(45):POKE 251,PEEK(46): X=PEEK(4624)+PEEK(4625)*256-2:POKE 45,X AND 255: POKE 46,X/256"+CHR$(13) 30 B$="DLOAD(ME$)"+CHR$(13)+"POKE 45,PEEK(250): POKE 46,PEEK(251)"+CHR$(13): KEY 2,(A$+B$) 40 BSAVE"MERGE",B0,P4096 TO P4352 To use this MERGE file, follow these three easy steps: 1. Before you start an editing session, type in: BLOAD"MERGE",B0 to retrieve the special function key defintions. 2. LOAD the first program (the ROOT program as described above) or enter it into memory from the keyboard. 3. When you want to add a previously SAVEd program module (equivalent to the APPEND file described above), type in ME$="filename2", then press function key instead of the key. Step three can be repeated as often as you wish without having to reload the MERGE definition file each time. A series of BASIC commands will be printed on the screen and executed while the disk drive comes on for a moment. The function key method of appending requires the same care with line numbers as the other program appending methods outlined above. It should also be noted that no error checking is done by the function key, so make sure that the file you specify as ME$ actually exists on your current disk or strange things may happen. Not to worry though, a simple - key sequence will abort the function key commands if necessary. The final method takes advantage of a peculiarity in the KERNAL ROM of the C-128 and involves MERGEing a SEQ file listing with a BASIC program using the CHKIN in immediate mode technique described in Chapter 6 as a sequential disk command file (SDC). The input routine in the C-128 KERNAL will accept line numbers in SDC's. These line numbered files will "execute" exactly as if the line, complete with line number, had been entered from the keyboard. That is, it will be added to any program currently in memory. This little trick is a simple yet powerful way to MERGE two or more program files on the C-128. Unlike other psuedo merge routines which merely append one program to the end of another, this technique allows full intermeshing of line numbers. Only a few steps are required. First, LOAD one of the programs into memory in the normal manner or enter it from the keyboard. Next convert it into a SEQ disk file listing with a series of commands such as: OPEN 8,8,8,"0:filename,S,W":CMD8:LIST PRINT#8:CLOSE8 LOAD in the second file or enter it from the keyboard. With the second file now in memory, read in the SEQ listing of the first file with: DOPEN#8,"filename" SYS 65478,0,8 After a brief period, the programs will be fully merged. The merge will terminate with a mysterious "OUT OF DATA" error message. This is a good sign: the process worked. The error message is caused by the last line of the listing in the disk file "READY.". (Commodore BASIC listings always include this.) The computer, not being able to recognize its own writing, thinks that someone typed in "READ Y". Since there are no accompanying DATA statements, the out of data error occurs and control is restored to the keyboard. If the READY. message did not appear at the end of the listing (for example if you edited it out with a word processor to give it a neater appearance), keyboard control would only be restored with a /. The programs will, however, be merged correctly. This technique can also be used for "loading" program listings produced on machines with incompatible keyword tokens and programs transferred into SEQ files via a modem download. This method is perhaps the easiest way to implement a MERGE command on the C-128. In additon, it is the only one of the methods presented which gives you the fully MERGEd program in RAM, ready to run when it is completed. RELative Files: Relative (REL) files are a convenient method for storing and retrieving a series of similar or otherwise related data that may be edited, added to, searched, sorted, etc. REL files are the only type in Commodore DOS that allow the simultaneous input and output of data to the same open file. In other operating systems, such as CP/M or MS-DOS, this concept is known as a "random" file. (Unfortunately, Commodore uses the term "random file" to signify access to specific tracks and sectors on a disk.) Whatever you choose to call it, a relative file handles data in packets of uniform size called "records". Each record may be further subdivided into logical or physical "fields", each of which contains a specific item. For example, a record may be an page in an address file. Each record in this example would typically have a field for name, address, city, zip code, phone number, etc. The total length of the record is the sum of the lengths of each of the fields. This physical record length is fixed when the file is first created. The length of each logical field within the record is usually fixed and the fields are always entered in the same order. Variable length (floating) physical fields can also be used, but the total length of a record still remains fixed. The choice of fixed or floating fields must be made when the datafile is first created because it will affect how data are read from and written to the file. Once the choice has been made, you must stick with it. In Commodore DOS, a relative file consists of two parts: "data sectors" and "side sectors". The data sectors contain the file data in a format identical to other file types. The side sectors contain the sector linking information for all data sectors in the file. This is used by the operating system to access each record selectively without having to trace through the sector link chain. Creating and maintaining REL files has never been easier than with the C-128. The procedure can be broken down into four easy steps: 1. OPEN the file 2. find the record you want 3. read or write the desired data 4. CLOSE the file. Each step will be discussed in turn below. Opening a Relative File To create a REL file, or to read or edit an existing file, the DOPEN statement is used in BASIC 3.5 or 7.0. This takes the form of: DOPEN#{file#},"{filename}",L{record length} In BASIC 2.0, the following procedure is used: OPEN {file#},{device#},{channel#},"{filename},L," +CHR$({record length}) In both cases, {file#} is the logical file number associated with the file for future input and/or output operations; "{filename}" is the name under which the file is stored in the disk directory (Relative files are denoted by the identifier REL listed after the filename in the disk directory.); and {record length} is the length in bytes to be used for each record in the file. {record length} need only be specified when a file is first created. The record length for a previously created file is stored as part of the file pointers in the directory entry and the side sectors. You should not attempt to change the length once a file has been created. If you incorrectly specify the record length for an already existing file, a DOS error code 50, RECORD NOT PRESENT, error will occur. If "{filename}" does not exist, then a new file will be opened automatically, otherwise the existing file is opened for reading and/or additions. The {channel#} parameter in the BASIC 2.0 version is used in conjunction with the record positioning procedure used by BASIC 2.0. Because the {channel#} is not specified explicitly with the BASIC 3.5/7.0 version, you should not mix the record specifying methods. That is, if you OPEN the file using BASIC 2.0, you should use the BASIC 2.0 record positioning method. Likewise, if you DOPEN the file from BASIC 3.5 or 7.0 you should use the BASIC 3.5/7.0 record positioning method. When setting up a relative file with fixed length fields, it is necessary to add up all of the character spaces required for each field, including punctuation, carriage return characters, and "hidden" items such as leading spaces associated with numeric values, to determine the required record length. It may be wise to add a few extra spaces for contingency. The other approach is to define a series of "floating" fields. The length of each individual field can vary from record to record, however, the total length of the record remains fixed and can be estimated using the procedure outlined above for fixed fields. In either case, the maximum record size allowed by Commodore DOS is 254 characters and the minimum is 2. The optimum record sizes based on speed and ease of access at the internal DOS level is either 127 bytes or 254 bytes. This corresponds to a half or whole data sector. It should be noted that Commodore DOS, as used on the 1571 drive, only permits one relative file to be OPEN at a time. This is due to a limitation on buffer space in the disk drive. When a REL file is first set up, it is initially assigned 1 data sector and 1 side sector. Unused record space is filled with a CHR$(255) in the first position of a record followed by a series of CHR$(0)'s in the remainder of a record. As the file grows, more data sectors are assigned, one at a time, and side sectors are added as required. The number of side sectors used depends on the number of records. A maximum of 6 side sectors can be associated with a single relative file. This will map out a total of 720 data sectors, although only 612 are actually used. Each additional data sector is automatically filled with a CHR$(255) and CHR$(0)'s as it is allocated. The following BASIC 7.0 technique can be used to determine the record length of an unknown file. The method relies on detecting the DOS error code 51, OVERFLOW IN RECORD, error. 10 DOPEN#1,"filename" 20 FOR I=2 TO 254:RECORD#1,1,I:IF DS<>51 THEN NEXT 30 PRINT "RECORD LENGTH IS"I-1" BYTES":DCLOSE The equivalent in BASIC 2.0 is: 10 OPEN 1,8,1,"filename":OPEN 15,8,15 20 FOR I=2 TO 254 30 PRINT#15,"P"+CHR$(97)+CHR$(1)+CHR$(0)+CHR$(I) 40 INPUT#15,DS:IF DS<>51 THEN NEXT 50 PRINT"RECORD LENGTH IS"I-1" BYTES":CLOSE 1:CLOSE 15 Finding a Record Once the REL file has been opened, it is ready to accept or provide data to the host program. Since the file is actually a collection of records, you must first determine where in the file you wish to read or write the data. Unlike conventional SEQ data files, where you must read or write each byte in sequence before you can read or write the next, you can read from, or write to, any record (in any order) with REL files. The same file can also be used for both reading and writing simultaneously. In BASIC 3.5 and 7.0, the RECORD command is used to position the relative file pointers to the correct location for subsequent input and/or output. The syntax of the command is: RECORD#{file#},{record#}[,{byte#}] This allows data to be read from or written to {record#} of logical {file#}, optionally beginning at {byte#}. The {byte#} parameter is generally only used for fixed field length record formats. A {byte#} value of 1 can be included for floating fields to ensure that the byte pointer is always positioned at the first character in the record. If {byte#} is greater than the record length, a DOS error code 51, OVERFLOW IN RECORD, error will result. In BASIC 2.0, record pointers are set through the disk drive command channel in the following manner (assuming file #15 has been previously opened as the disk command channel): PRINT#15,"P"+CHR$(96+{channel#})+CHR$({rec#lo})+CHR$({rec#hi}) [+CHR$({byte#})] where {channel#} is the channel (secondary address) used in the OPEN statement for the file, and {rec#lo} and {rec#hi} are the low and high bytes of the desired record number, respectively. {byte#} has the same meaning as in the BASIC 7.0 RECORD command. Although apparently not technically necessary for the 1571 drive, a 96 is added to the {channel#} parameter for consistent operation with other Commodore disk drives. The 1571 Disk Drive Users Guide recommends that the RECORD command (or its BASIC 2.0 equivalent) be issued once before and again after any attempt is made to read or write the file. While this precaution may seem unnecessary to some, it is wise to use it to prevent the corruption of data in your file if a major error occurs during attempted file access and, more importantly, to allow adequate time for DOS to locate and read in the desired record. This is especially important for "odd" sized records. (That is, anything but a half sector (127 bytes) or full sector (254 bytes) record size.) Another method frequently used to wait until the record pointers have been correctly set is to read the disk error status immediately after issuing the record positioning pointers. This will "tie-up" any further communication between the computer and the disk drive until after the record has been located and read into the buffer. This latter method is often used with machine language and KERNAL calls. It is also necessary to reposition the pointers for a direct read-after-write or write-after-read, to ensure correct placement of the data. If you attempt to read or write to a record which is outside of the range currently defined for the file, a DOS error code 50, RECORD NOT PRESENT, error may occur. If trying to read a record, this error indicates that you have gone past the last data sector used in the file. (Depending on the size of your record, you may be able to go one or two records past the current last record before you get this error. This is because the assigned space for the record is already included in the sectors previously read into the disk buffer.) An unused record can always be identified by a CHR$(255) as the first character in the record. If trying to write a record, this is not an error message but only an indication that you are writing a new record. The maximum record number allowed is 65535. The minimum is 1. The maximum file size (# of records x record length) that is currently supported on the 1571 drive is 167,132 bytes (163 K bytes). This is the same for both single and double sided disks. (I.e. relative files must all fit on one side of the disk.) Relative files have a habit of growing uncontrollably. You cannot "delete" records from the file. The most you can do is copy good records from a file into a new file, then scratch the old file. If the file grows to take over your whole disk, a DOS error code 52, FILE TOO LARGE, error will result. It is then time to split the file into several smaller ones, or too purge out all the unused records. Input and Output With the relative file pointers correctly positioned, you can proceed with input and output using the normal disk file commands such as INPUT#, GET#, PRINT#, and PRINT#,USING. In this respect, relative files are no different than any other type of disk file. However, because each record length is fixed, you should always check the length of anything being written to disk to make sure that it will fit. If you try to overrun a record boundary (i.e. there is not enough space in the record to contain all of the data), a DOS error code 51, OVERFLOW IN RECORD, error will occur. When this happens the extra characters will be dropped or truncated from the end of the record to make it fit. It will not, however, affect the rest of the file. It should be noted that not all input and output operations will appear to activatethe disk drive, especially if the record length is short. This is because the disk drive will read two sectors into the buffer at once. If the record you try to access is already contained entirely in the sectors in the buffer, no visible disk activity will take place. (When two sectors are in the disk buffers, at least one record of 254 bytes or less will always be in memory, no matter how it is physically contained on the disk.) Input On the C-128, the input buffer is 160 characters long. Therefore, the INPUT# command can only read 160 characters at a time. If your record length is less than 160 characters, then there is no problem: the entire record can be read at once. If it is greater than 160, then you must either read the record with a series of GET#'s or insert one or more carriage returns into the record to divide it up into smaller chunks (or physical fields). Breaking up a record into physical fields (as opposed to logical fields used in fixed field length format records) is the basis of the floating field method of relative file construction. Each field is separated by a carriage return and therefore, can be read directly with a separate INPUT# statement. Of course, you can use floating fields with a record length of less than 160, but remember: in both cases each carriage return character counts as one character in the record length. In many respects, floating fields are much easier to use in BASIC simply because you do not have to keep track of the lengths of individual fields. The disadvantage of using floating fields is that each record acts like a sequential file, that is each field must be read or written in the proper order. It is generally more difficult (although by no means impossible) to read and write floating fields in the selective manner of fixed fields. Assuming a file with a record length of 80 characters and four data fields, the following is an example of how to read a floating field record format (neglecting the error channel for a moment): 10 DOPEN#1,"DATAFILE",L80 20 INPUT "RECORD # TO READ";NU 30 RECORD#1,(RE),1 40 INPUT#1, F1$,F2$,F3$,F4$ 50 RECORD#1,(RE),1 . . . (the rest of your program) In the above example, each field can be any length, as long as all four totaled do not exceed 80 characters. The field lengths are quite often different from record to record. Note that the ",1" is included at the end of each RECORD statement. This is to ensure that the byte pointer is correctly positioned to the first character in each record. Assuming each field was 20 characters long, and lines 10, 20 and 30 are identical to those above, a similar program for reading fixed field records would appear as : . . . 40 INPUT#1,A$ 50 RECORD#1,(NU),1 60 F1$=LEFT$(A$,20):F2$=MID$(A$,20,20) 70 F3$=MID$(A$,40,20):F4$=RIGHT$(A$,20) . . . When using the fixed field model, no field can exceed 20 characters (in this example) even if some of the other fields contain less than the maximum number of characters. If the record length is to be longer than 160 characters, then either a carriage return would be required to break the record up into less than 160 character chunks, or the record should be read using a series of GET#'s. In the first case, line 40 in the example would read: 40 INPUT#1,A1$,A2$ Each of A1$ and A2$ would then be divided into its logical fields, using a method similar to line 60 in the above example. In the second case, line 40 might read: 40 A$="":FOR I=1 TO {record length}:GET#1,A1$:A$=A$+A1$:NEXT A$ would then be divided into its constituent logical fields as in the previous example. Output Output to relative files is similar to other disk file types: it is done using the PRINT# (or PRINT#, USING;) command. Of course, the data must be output to the file in the correct order and, if using floating fields, each item must be separated by a carriage return. The following example will write the data represented by F1$, F2$, F3$, and F4$ to the file created for the input demonstration outlined above using floating field format. 90 CR$=CHR$(13) 100 INPUT "RECORD #";NU 110 INPUT "FIELD #1 DATA";F1$ 120 INPUT "FIELD #2 DATA";F2$ 130 INPUT "FIELD #3 DATA";F3$ 140 INPUT "FIELD #4 DATA";F4$ 150 R$=F1$+CR$+F2$+CR$+F3$+CR$+F4$+CR$ 160 IF LEN(R$)>80 THEN PRINT"RECORD TOO LONG":GOTO 100 170 RECORD#1,(NU),1:PRINT#1,R$:RECORD#1,(NU),1:GOTO 100 Note the carriage return character specifically placed between each field. If you are absolutely certain that the total record length will not exceed the specified record size, the following can be substituted for lines 150 to 170: 150 RECORD#1,(NU),1 160 PRINT#1,F1$:PRINT#1,F2$:PRINT#1,F3$:PRINT#1,F4$ 170 RECORD#1,(NU),1:GOTO 100 The above example using fixed field records may look like: 90 SP$="{20 SPACES}" . . . 150 F1$=LEFT$(F1$+SP$,20):F2$=LEFT$(F2$+SP$,20) 160 F3$=LEFT$(F3$+SP$,20):F4$=LEFT$(F4$+SP$,20) 170 R$=F1$+F2$+F3$+F4$:RECORD#1,(NU),1 180 PRINT#1,R$;:RECORD#1,(NU),1:GOTO 100 Alternatively, for fixed field lengths, the following may be substituted in the above example beginning at line 170: 170 RECORD#1,(NU),1:PRINT#1,F1$:RECORD#1,(NU),1 180 RECORD#1,(NU),21:PRINT#1,F2$:RECORD#1,(NU),21 190 RECORD#1,(NU),41:PRINT#1,F3$:RECORD#1,(NU),41 200 RECORD#1,(NU),61:PRINT#1,F4$:RECORD#1,(NU),61 This last method has the advantage that each field can be written separately. It should be noted, however, that with any output method, DOS will always fill to the end of the record with CHR$(0)'s, regardless of whether or not the record contains any more valid data. Therefore, the selective field writing should only be done if all subsequent data in the record is going to be updated. Otherwise, you must write the entire record at once. Closing the File A relative file is closed using a standard CLOSE {file#} or DCLOSE type of statement. In this respect, it is identical to all other types of disk files. However, you should not use a dummy PRINT# statement before closing the file as is customary for other types of write files. Relative File Demo The following program is a short demonstration of the principles of relative file creation and input/output. It can be used to store addresses and phone numbers. Each record has a length of 80 bytes, consisting of 7 floating fields representing the following data: ................................... Field name Estimated length ................................... NAME 20 STREET ADDRESS 20 CITY 15 STATE 5 ZIP 5 AREA CODE 5 PHONE # 10 ===== total # bytes 80 ................................... 10 DOPEN#1,"DATAFILE",L80:IF DS THEN GOSUB 410:DCLOSE:END 20 PRINT"{CLR}RELATIVE FILE DEMO":CR$=CHR$(13) 30 PRINT"1: READ":PRINT"2: WRITE":PRINT"3: QUIT" 40 INPUT"ENTER SELECTION";S:IF S<1 OR S>3 THEN 20 50 ON S GOSUB 90,240,430:GOTO 20 60 : 70 REM READ RECORDS 80 : 90 WR=0:PRINT"{CLR}READ RECORDS":INPUT"RECORD NUMBER";NU: IF NU=0 THEN RETURN 100 RECORD#1,(NU),1:IF DS THEN GOSUB 410:GOTO 90 110 INPUT#1,N$,S$,C$,SA$,ZI$,AC$,PH$ 120 RECORD#1,(NU),1 130 IF N$=CHR$(255) THEN PRINT"RECORD NOT PRESENT": GOSUB 420:GOTO 90 140 PRINT" NAME >> "N$ 150 PRINT" ADDRESS >> "S$ 160 PRINT" CITY >> "C$ 170 PRINT" STATE >> "SA$ 180 PRINT" ZIP >> "ZI$ 190 PRINT"AREA CODE >> "AC$ 200 PRINT" PHONE >> "PH$:IF WR THEN RETURN:ELSE GOSUB 420: GOTO 90 210 : 220 REM WRITE RECORDS 230 : 240 PRINT"{CLR}WRITE RECORDS":INPUT"RECORD NUMBER";NU: IF NU=0 TNEN RETURN 250 RECORD#1,(NU),1:WR=1 260 IF DS=50 THEN PRINT"NEW RECORD":ELSE IF DS THEN GOSUB 410: GOTO 240 270 IF DS<>50 THEN GOSUB 100 280 INPUT" NAME >> ";N$:N$=N$+CR$ 290 INPUT" ADDRESS >> ";S$:S$=S$+CR$ 300 INPUT" CITY >> ";C$:C$=C$+CR$ 310 INPUT" STATE >> ";SA$:SA$=SA$+CR$ 320 INPUT" ZIP >> ";ZI$:ZI$=ZI$+CR$ 330 INPUT"AREA CODE >> ";AC$:AC$=AC$+CR$ 340 INPUT" PHONE >> ";PH$:PH$=PH$+CR$ 350 R$=N$+S$+C$+SA$+ZI$+AC$+PH$ 360 IF LEN(R$)>80 THEN PRINT"RECORD TOO LONG":GOSUB 420:GOTO 280 370 RECORD#1,(NU),1:PRINT#1,R$:RECORD#1,(NU),1:GOTO 240 380 : 390 REM ERROR AND EXIT ROUTINES 400 : 410 PRINT"DISK ERROR >> ";DS$:GOSUB 420:RETURN 420 PRINT"PRESS A KEY TO CONTINUE":GETKEY Z$:RETURN 430 DCLOSE:END Index Files Sequential data files are often used as index or "key" files for relative files. Commodore DOS will allow one SEQ and one REL file to be OPEN at the same time with a 1571 disk drive. An index can be read from a SEQ file and stored in the computer's memory in array form. Of course this method is limited by the size of the array, and hence, the computer's memory, but it does allow for a quick and easy way to search and sort data in relative files. (The index arrays are searched or sorted entirely in memory, then only the selected records are actually read from the relative file.) Index files can be used in a variety of ways. Often it is quicker to search an index for a specific record than searching the entire relative file. The only disadvantage is that index files require a bit of time to create and must be updated each time you change the relative file. However, if you have a large database that is fairly static (i.e. you are not constantly changing it) the advantages far outweigh the disadvantages. For example, supposing you had a relative file, similar to the one used in the example in the previous section, which contained 1000 records. You wish to find the address of a given name. There are two ways that this can be done. One is to read each record in sequence to until you find a match with the name field. This can be very time consuming for a large database. The second method is to build an index and scan it instead. Once the index has been built, it can be used to speed up subsequent searches. The following example demonstrates the creation and use of an index file. (Assume that the database file has been created similar to the one used in the example above.) 0 REM CREATE INDEX 1 REM RELATIVE FILE = DATAFILE 2 REM INDEX FILE = INDEX 10 DIM N$(1000) : REM SET UP INDEX ARRAY 20 OPEN 1,8,4,"INDEX,S,W" : REM OPEN INDEX FILE 30 OPEN 8,8,8,"DATAFILE" : REM OPEN REL FILE 40 RE=1 : REM FIRST RECORD 50 RECORD#8,(RE),1 : REM GET RECORD 60 IF DS THEN 100 : REM CHECK FOR END OF FILE 70 INPUT#8,NA$ : REM READ NAME 80 N$(RE)=NA$ : REM STORE IN ARRAY 90 RE=RE+1 : GOTO 50 : REM NEXT RECORD 100 RE=RE-1 110 PRINT#1,RE : REM SAVE # OF RECORDS 120 FOR I=1 TO RE 130 PRINT#1,N$(I) : NEXT : REM PRINT INDEX 140 PRINT#1 : CLOSE 1 : CLOSE 8 0 REM USE INDEX 10 DIM N$(1000) 20 OPEN 1,8,4,"INDEX" 30 INPUT#1,NU : REM NUMBER OF RECORDS 40 FOR I=1 TO NU 50 INPUT#1,N$(I) : NEXT : REM RECOVER INDEX 60 CLOSE 1 70 OPEN 8,8,8,"DATAFILE" : REM OPEN REL FILE 80 INPUT "NAME TO SEARCH FOR";NS$ 90 RE=0 : FOR I=1 TO NU : REM SEARCH INDEX 100 IF NS$=N$(I) THEN RE=I : I=NU+1 : REM RECORD FOUND 110 NEXT : IF RE=0 THEN 160 : REM CONTINUE SEARCH 120 RECORD#8,(RE),1 : REM GET RECORD 130 INPUT#8,N$,S$,C$,SA$,ZI$,AC$,PH$ 140 PRINT N$,S$,C$,SA$,ZI$,AC$,PH$ : REM DISPLAY RECORD 150 GOTO 80 : REM TRY AGAIN 160 PRINT "NAME NOT FOUND" : GOTO 80 Another type of index file might contain the sorted order of the records by various fields. For example, in the above demonstration, we wish to print out the entire list arranged alphabetically by name or by area code. A single index file can be used to contain the sorted parameters. Such a file may look like: 57, 102, 1, 17, 27, etc. where these numbers represent the sequence of records according to the specified sorting. In this case, the records are not actually moved in the file but you access them in a given sequence. Multi dimensional arrays can be used to contain a master cross reference of all the records according to all possible sorting mechanisms. For example, an index file might contain: 57, 1, 32, 4 19, 57, 22, 1 9, 7, 1, 2 etc. Each line of this example represents four distinct sorting creiteria. The records in sequence 57, 19, 9 might be the arrangement of the records by name, while 1, 57, 7, might be the arrangement by area code, 32, 22, 1 by city and 4, 1, 2 by zip code, etc. This shows that index files have a great deal of flexiblity and can be used for many purposes. The key factor is that it is faster to search a table in memory for a given entry number them get that entry directly rather than search each entry on disk looking for a match.