RELATIVE FILES MADE RELATIVELY EASY Part 2 by Bill Brier DELPHI Mail: TROUBLESOME V. READING A RELATIVE FILE RECORD In RELATIVE FILES MADE RELATIVELY EASY Part 1 we learned what a RELative file is, how to organize such a file, how to create a file and how to position to a given record. In Part 2 we'll now look at merging all of this information together to make a cohesive program. Reading a RELative file record requires that your program operate in a logical sequence. Essentially , it goes like this. First, you OPEN the error channel, then the RELative file itself. Next, you check for a possible disk error. If one exists you CLOSE your files and branch to a routine that evaluates the error and takes appropriate action. Otherwise, you position to the desired record and field and use INPUT# or GET# to read in the data. The position and read operation is performed for each field if the entire record is to be read in, or just for one field if you are searching for a particular item of data. When you have read in the data you close the RELative file and lastly, close the error channel. It's actually quite simple once you see how it's done. Reading in the whole record is best done by setting up a subroutine for that purpose. Let's write such a subroutine for reading in one record from our mailing list. The subroutine will start at line 8400 in our program. The position routine, you may recall, is located at line 8100 and falls through to line 8200 to check the disk status. Prior to actually calling the record reading routine you must OPEN the error channel (as demonstrated in Part 1) by a GOSUB6900, OPEN the RELative file by GOSUB7000 and check the error channel for a possible error by using the errorchecking routine (GOSUB8200). If an error occurred, the program will return from 8200 with the error "flag" EF set to 1 and the variables X and X$ containing the error number and text, respectively. In such a case no attempt should be made to read a record or else the program will crash. Once the preliminary operations have been performed, you should set the record number variable J to the desired record number. Now you are ready to read in the record: 8400 P=1:GOSUB8100:IF EF THEN 8470 8405 INPUT#2,NA$ 8410 P=34:GOSUB8100:IF EF THEN 8470 8415 INPUT#2,AD$ 8420 P=60:GOSUB8100:IF EF THEN 8470 8425 INPUT#2,CT$ 8430 P=79:GOSUB8100:IF EF THEN 8470 8435 INPUT#2,SA$ 8440 P=82:GOSUB8100:IF EF THEN 8470 8445 INPUT#2,ZI$ 8450 P=88:GOSUB8100:IF EF THEN 8470 8455 INPUT#2,AC$ 8460 P=92:GOSUB8100:IF EF THEN 8470 8465 INPUT#2,TN$ 8470 RETURN Several patterns emerge from this example. First, each call to the position subroutine at 8100 is preceded by assigning a value to the variable P, which is of course the field position. These values were derived from the table that we discussed in Part 1. Also, notice that after each call to the position subroutine, the generic error flag EF is checked. EF is set to 0 if there is no error and is set to 1 if an error occurs. The actual error number and message are contained in the variables X and X$. However, at this point in the program we only want to know whether an error actually occurred. Later on an evaluation will take place. If EF is equal to 1 the expression IF EF THEN 8470 will succeed and the routine will automatically abort. The importance of making an error check after each disk command cannot be overemphasized. If an error occurs and your program ignores it, a crash of some sort is inevitable. Assuming that no error occurs, the next step is to actually read the field into a variable. Never use numeric variables for working with disk files. Reading numeric data into a string variable will never cause any problem but reading string data into a numeric variable will cause the program to crash. Also, INPUT# restricts the maximum length of a data string to 88 characters and assumes that the field is terminated by a carriage return. If the field is longer than 88 characters, use GET# to retrieve the characters one at a time and then concatenate the characters into one string variable. In looking at our record reading subroutine it becomes obvious that the data has to be read in a certain order. This is because when the record is written out to the disk it is also written in a certain order. A subroutine to do that will be presented later in this article. Once all of the fields have been read in, the routine will reach the RETURN in line 8470 and exit back to the rest of the program. If an error occurred at some point the variable EF will be set to 1 upon exit from the routine and X and X$ will contain the actual error (review Part 1 if you aren't sure as to how the values for EF, X and X$ were obtained). The previous example demonstrated the means by which a RELative file record can be read into variables for use within your program. But, a RELative file allows more than just reading a record. The real power in a RELative file lies in the structuring of the records into fields. It is possible to examine individual fields and thus perform searches for different data items. VI. SEARCHING ON FIELDS Let's suppose that you are going to make a bulk mailing of advertising flyers to customers. The post office has advised you that you can get a reduced rate if you bundle all of your flyers into groups sorted by zip codes. Obviously, when the mailing labels are printed the records should be retrieved by zip code. Such a search means searching on the zip code field. To do that requires that you set a string variable equal to the zip code that you wish to retrieve, read the zip code field in each record and, if the zip code in that record is the same as the one that you want, read in the whole record and print the label. Sounds complicated? Not at all. Here's an example, which assumes that you've already OPENed the error channel and RELative file, and that the value of J has been set to the record to be read: REM SEARCH FOR '606' AS THE ZIP CODE FRAGMENT : ZS$="606":L=LEN(ZS$):REM ZS$ IS THE SEARCH VALUE P=82:GOSUB8100:IF EF THEN 9500:REM 9500 IS ERROR ROUTINE INPUT#2,ZI$:REM READ IN THE ZIP FIELD FROM THE RECORD IF LEFT$(ZI$,L)<>ZS$ THEN TRY NEXT RECORD : GOSUB8400:REM READ IN ENTIRE RECORD FOR PRINTING A FOR-NEXT loop would be used to cycle through all the records on file. With the above routine, each time a record was found with the proper zip code, the entire record would be fetched and used to print a label. Because only the zip code field is actually checked rather that the whole record, time is not wasted in reading in fields that aren't needed. Only when the zip field matches the search value or parameter is the whole record fetched. It is also possible to make multiple field searches using the same technique. You would simply read each field that is being searched and if all fields match then read in the whole record. The possibilities are limited more by the imagination then anything else. VI. WRITING A RECORD Writing a record entails program activity similar to that required for reading a record. You must OPEN the error channel and the RELative file, position to each field within the record and write out the data. As with reading a record there are some rules to be followed. Although reading a single field of a record can be done in a random fashion, writing a field may not. This is due to the manner in which the DOS handles RELative file records. For this reason and others, a subroutine to write the whole record to the disk should be constructed. Here is how we would write our mailing list record: 8300 P=1:GOSUB8100:IF EF THEN 8375 :REM TRAP FOR ERROR 8305 PRINT#3,NA$:REM WRITE NAME FIELD 8310 P=34:GOSUB8100:IF EF THEN 8375 8315 PRINT#3,AD$ 8320 P=60:GOSUB8100:IF EF THEN 8375 8325 PRINT#3,CT$ 8330 P=79:GOSUB8100:IF EF THEN 8375 8335 PRINT#3,SA$ 8340 P=82:GOSUB8100:IF EF THEN 8375 8345 PRINT#3,ZI$ 8350 P=88:GOSUB8100:IF EF THEN 8375 8355 PRINT#3,AC$ 8360 P=92:GOSUB8100:IF EF THEN 8375 8365 PRINT#3,TN$ 8370 P=1:GOSUB8100 8375 RETURN If the above routine looks similar to the routine for reading a record, it is because the record must be written out in the exact same manner as you wish to read it back. Obviously, the program wouldn't work correctly if the name field was written to the wrong place in the record. Several precautions must be heeded when writing the record to avoid a possible corruption of the data. The writing of the record must always start at the lowest field position (normally 1), as shown in the example. If you were to first write the zip field (position 82) and then the name field (position 1) the zip field would be destroyed. For that reason, the record must be written sequentially from the beginning. If your program is modifying one field (such as the zip code) you must read in the whole record, change the variable that corresponds to the field to be altered and then write the whole record back. Reading in the zip code, modifying it and writing it back to the record alone would destroy the area code and telephone number fields. Another thing to be careful of is the content of each variable. The INPUT# statement will not handle nulls or leading blanks (CHR$(32)). If a variable is to be unused in a particular record it must be padded with a suitable character that your program will recognize as a blank field. For example, in our mailing list you could make the area code field an optional entry that the user could default. A default would normally result in AC$ having an ASCII value of zero. This is not acceptable for use in a disk file. In such a case change AC$ so that it has an ASCII value of 160 (a SHIFTed space). INPUT# will retrieve such a character without any trouble. If a variable containing a SHIFTed space is printed to the screen it will have the same effect as a regular space. Incidentally, some Centronics printer interfaces, such as the Card/?+G by CardCo, have trouble handling a SHIFTed space. Leading blanks are equally troublesome. A leading blank will cause INPUT# to malfunction and result in the program "locking up", with the disk file left open. For this reason, you must prevent a leading blank from being written to the disk. A simple way to detect a leading blank in a variable is to test the ASCII value of the variable. ASCII will always return the value of the first character in the variable, no matter what length the variable is. To test for a leading blank in NA$, for example, you could do this: A=ASC(NA$+CHR$(0)) Even if NA$ is twenty characters in length A will containing the ASCII value of the first character only. The CHR$(0) part is needed to prevent a program crash in case NA$ is a null. INPUT# terminates fetching of characters from the disk once a carriage return (CHR$(13)) is received. Unfortunately, a comma or a colon will do the same thing. Therefore, your program must trap out these characters. If you must write them as part of the record, use GET# to fetch the characters one at a time, and concatenate a string variable. You will have to evaluate each character as it comes in so you may detect the CHR$(13) that acts as the delimiter. Because of this, you will find GET# to be rather slow. Because you have defined the length of each field, your program must prevent a variable length from exceeding the corresponding field length. This is where the LEN function becomes useful. Simply check the LENgth of the variable and if it exceeds the field size limit, chop off the excess with the LEFT$ function. For example, here is how to limit the name to 32 characters: IF LEN (NA$) > 32 THEN NA$=LEFT$(NA$,32) Actually, a better way to do this is to write an input routine which can control how many characters the user can type in. This way the user knows when his input is being restricted. Using the LEFT$ function to limit the variable length does not let the user know exactly what he is sending to the disk. Finally, after your program has written out the record to the disk it should reposition back to the first field of the record before CLOSEing the file. This will prevent a DOS bug, known as the SPAN-SPILL bug, from corrupting certain records. That is the function of line 8370 in the example. Do not actually write anything with PRINT#...simply reposition back to the start of the field. VII. RELATIVE REVIEW Let's look back a bit at what has been discussed. We have developed techniques to create a relative file, we have seen how to position to a given record and field, we have seen how to read and write to a record, and we have seen a simple technique for searching records for a particular item of data. We have learned how to correctly structure our records, calculate the approximate storage space needed on the disk and how to deal with disk errors. Obviously, RELATIVE FILES MADE RELATIVELY EASY is not an exhaustive treatment of the subject, nor is it a manual on database programming. It is hoped however that the budding programmer will be able to construct an efficient database using this information. The only sure route to programming knowledge is via experimentation and observation. Experiment with the routines described above on a scratch disk and watch what does or doesn't happen. Don't be afraid to make a mistake. And, above all, remember the KISS philosophy: (K)eep (I)t (S)imple, (S)tupid! W.J. Brier (Delphi mail: TROUBLESOME)