FILES HANDLE SERIAL DATA
One of the most common operations when using a computer is to either read from, or write to a file. You are already somewhat experienced in file handling from the last chapter, because in computer terminology, the keyboard, terminal, and printer are all classified as files. A file is any serial input or output device that the computer has access to. Since it is serial, only one piece of information is available to the computer at any instant of time. This is in contrast to an array, for example, in which all elements of the array are stored internally and are all available at any time.
A SHORT HISTORY LESSON
Several years ago computers were all large cumbersome machines with large peripheral devices such as magnetic tape drives, punch card readers, paper tape readers or punches, etc. It was a simple task to assign the paper tape reader a symbol and use that symbol whenever it was necessary to read a paper tape. There was never more than one file on the paper tape being read, so it was simply read sequentially, and hopefully the data was the desired data. With the advent of floppy disks, and hard disks, it became practical to put several files of data on one disk, none of which necessarily had anything to do with any of the other files on that disk. This led to the problem of reading the proper file from the disk, not just reading the disk.
Pascal was originally released in 1971, before the introduction of the compact floppy disk or the hard disk. The original release of Pascal had no provision for selecting a certain file from among the many included on the disk. Each compiler writer had to overcome this deficiency, and he did so by defining an extension to the standard Pascal system. Unfortunately, all of the extensions were not the same, and there are now several ways to accomplish this operation. There are primarily two ways, one using the Assign statement, and the other using the Open statement. They are similar to each other and they accomplish the same end result.
BACK TO THE PRESENT TIME
All of the above was described to let you know that we will have a problem in this chapter, namely, how do we cover all of the possible implementations of Pascal available? The answer is, we can't. Most of what is covered in this chapter will apply to all compilers, and all that is covered will apply to the TURBO Pascal compilers. As we have mentioned before, this tutorial is especially written for the TURBO Pascal compilers, but you should be warned that you will find differences in Pascal implementations if you have occasion to use a different Pascal compiler someday. You may, for example, need to use a mini-computer or a mainframe computer someday to complete a programming assignment. When that happens, you will find that the area of input/output control will probably be the biggest difference in the implementations of Pascal.
READING AND DISPLAYING A FILE
Example program ------> READFILE.PAS
Examine the file READFILE.PAS for an example of a program that can read a text file from the disk. In fact, it will read itself from the disk and display itself on the video monitor. The first statement in the program is the Assign statement. This is TURBO Pascal's way of selecting which file on the disk will be either read from or written to. In this case we will read from the disk. The first argument in the Assign statement is the device specifier similar to Lst used in the last chapter for the printer. We have chosen to use the name Turkey for the device identifier, but could have used any valid identifier. This identifier must be defined in a var declaration as a text type variable as illustrated in line 4. The next argument is the filename of the file we wish to read from. The filename can be defined as a string constant, as it is here, or as a string variable.
The text type is a predefined type and is used to define a file identifier. It is predefined as a "file of char", so it can only be used for a text file. We will see later that there is another type of file, a binary file.
You will find that the operating system that you are using requires a file name to follow certain conventions when it is named, and the Pascal programming language has a set of rules by which an identifier can be named. Since these two conventions are not necessarily the same, it is necessary to give each file an external name which the operating system is happy with, and an internal filename which Pascal is happy with. It becomes necessary to tie these two names together, and this is the primary job of the Assign statement.
Now that we have a file identified, it is necessary to prepare it for reading by executing a reset statement in line 9. The Reset statement positions the read pointer at the beginning of the file, ready to read the first piece of information in the file. Once we have done that, data is read from the file in the same manner as it was when reading from the keyboard. In this program, the input is controlled by the while loop which is executed until we exhaust the data in the file.
WHAT ARE THE "EOF" AND "EOLN" FUNCTIONS?
The Eof function is new and must therefore be defined. When we read data from the file, we move closer and closer to the end of the file, until finally we reach the end and there is no more data to read. This is called "end of file" and is abbreviated Eof. Pascal has this function available as a part of the standard library which returns FALSE until we reach the last line of the file. When there is no more data to read left in the file, the function Eof returns TRUE. To use the function, we merely give it our file identifier as an argument. It should be clear to you that we will loop in this program until we read all of the data available in the input file.
The Eoln function is not used in this program but is a very useful function. If the input pointer is anywhere in the text file except at the end of a line, the Eoln returns FALSE, but at the end of a line, it returns a value of TRUE. This function can therefore be used to find the end of a line of text for variable length text input.
To actually read the data, we use the Readln procedure, giving it our identifier Turkey and the name of the variable we want the data read into. In this case, we read up to 80 characters into the string and if more are available, ignore them. You should remember when we did this in the last chapter from the keyboard input. We are using the same technique here except we are reading from a file this time. Since we would like to do something with the data, we output the line to the default device, the video monitor. It should be clear to you by now that the program will read the entire file and display it on the monitor.
Finally, we Close the file Turkey. It is not really necessary to close the file because the system will close it for us automatically at program termination, but it is a good habit to get into. It must be carefully pointed out here, that you did not do anything to the input file, you only read the data and left it intact. You could Reset it and reread it again in this same program. Compile and run this program to see if it does what you expect it to do.
A PROGRAM TO READ ANY FILE
Example program ------> READDISP.PAS
Examine the next program READDISP.PAS for an improved file reading program. This is very similar to the previous program, except that it asks you for the name of the file which you wish to display. After you enter the filename, it enters the name into a 12 character string which is named Name_Of_File_To_Input which will be the external filename. This is then used in the Assign statement to select the file to be read, and to connect the external filename to the internal filename, Chicken. The file is then reset as before. Lines 15 through 18 display a header, and from that point on, the program is identical to the last one with a few small additions. In order to demonstrate the use of a function within the Writeln specification, the program calls for the length of the input string in line 23 and displays it before each line. The lines are counted as they are read and displayed, and the line count is displayed at the end of the listing. Both of these operations are done only to illustrate to you how they can be done.
You should be able to understand clearly how each of these operations is accomplished. Compile and run this program, entering any filename we have used so far as the file to be listed on the monitor (be sure to include the .PAS extension). After a successful run, enter a nonexistent filename to see the I/O error generated by the Pascal runtime system. The next example program will illustrate a more graceful method of detecting this error.
HOW TO COPY A FILE (SORT OF)
Example program ------> READSTOR.PAS
Examine the file READSTOR.PAS for an example of reading from a file and writing to another one. In this program we request an operator input for the filename to read, after which we Assign the name to the file and Reset it. When we reset the file however, we go to a bit of extra trouble to assure that the file actually exists. Note that this is an extension to TURBO Pascal and will probably not work with other Pascal compilers.
Suppose we input a filename, and the file did not exist because the file was actually missing, or because we entered the name of the file wrong. Without this extra effort, the TURBO Pascal runtime system would indicate a run-time error, and terminate the program returning us to the operating system. In order to make a program easier to use, it would be nice to tell the operator that the file didn't exist and give him the opportunity to try again with another file name. The method given in lines 16 through 20 of this program will allow you to do just that.
USING A COMPILER DIRECTIVE
First you must disable the built in TURBO Pascal I/O checking by inserting the compiler directive in line 16. This tells the system to ignore any I/O errors from this point on and if the file doesn't exist, the system will not abort when you attempt to reset it in line 17. Another compiler directive is given in line 18 to enable I/O checking again for the remainder of the program.
WE DO OUR OWN FILE CHECKING
If the file didn't exist and could not therefore be reset, we have a problem because the program thinks the file is available for use but it actually isn't. Fortunately, TURBO Pascal has a built in variable, named IOResult that informs us of the result of each I/O operation. Following any I/O operation, if this variable contains the value of zero, the I/O operation was correct, and if it contains any other value, the operation had some sort of error. In our case, we simply compare it to zero to generate a boolean value, then based on the boolean value we either give an error message and stop, or perform the desired file operations.
It would be good programming practice to check all file openings in this manner to allow the operator to recover from a simple oversight or spelling error.
If the file was opened properly, then in line 21 through 24 we request a different filename to write to, which is assigned to a different identifier. Note that the output file is not checked for a valid opening in this example as it should be. The statement in line 24 is new to us, the Rewrite statement. This name apparently comes from the words REset for WRITEing because that is exactly what it does. It clears the entire file of any prior data and prepares to write into the very beginning of the file. Each time you write into it, the file grows by the amount of the new data written.
Once the identifier has been defined, and the Rewrite has been executed, writing to the file is identical to writing to the display, provided you add the file identifier prior to the first output field. With that in mind, you should have no trouble comprehending the operation of the program. This example program is very similar to the last example program, except that it numbers the lines as the file is copied. After running the program, look in your default directory for the new filename which you input when the system asked for the output filename. Examine that file to see if it is truly a copy of the input file with line numbers added.
Actually a much better style would result if the logic for each file opening was put into a procedure of its own. In addition to this being easier to debug and understand, the completed procedures could be reused in other programs in the future.
One word of caution. If you used an existing filename for the output file, the file was overwritten, and the original destroyed.
Compile and run this program two different ways, once with a valid input filename that should run properly, and the second time with an input filename that doesn't exist to prove to yourself that the test actually does work correctly.
HOW TO READ INTEGER DATA FROM A FILE
It is well and good to be able to read text from a file, but now we would like to read other forms of data from a file. First we will look at an example program to read data from a text file, then later we will see an example program that reads from a binary file.
Example program ------> READINTS.PAS
Examine the program READINTS.PAS for an example of reading data from a text file. A text file is an ASCII file that can be read by a text editor, printed, displayed, or in some cases, compiled and executed. It is simply a file made up of a long string of char type data, and usually includes linefeeds, carriage returns, and blanks for neat formatting. Every file included with this tutorial is a text file.
The example program has nothing new, you have seen everything in it before. We have an assignment, followed by a reset of our file, followed by four read and write loops. Each of the loops has a subtle difference to illustrate the Read and Readln statements. Notice that the same file is used for reading four times with a Reset prior to each, illustrating the nondestructive read mentioned a few paragraphs ago.
The file we will be using is named INTDATA.TXT and is on your disk. You could display it at this time using the program READDISP.PAS we covered recently. Notice that it is simply composed of the integer values from 101 to 148 arranged four to a line with a couple of spaces between each for separation and a neat appearance. The important thing to remember is that there are four data points per line.
READ AND READLN ARE SLIGHTLY DIFFERENT
As variables are read in with either procedure, the input file is scanned for the variables using blanks as delimiters. If there are not enough data points on one line to satisfy the arguments in the input list, the next line is searched also, and the next, etc. Finally when all of the arguments in the input list are satisfied, the Read is complete, but the Readln is not. If it is a Read procedure, the input pointer is left at that point in the file, but if it is a Readln procedure, the input pointer is advanced to the beginning of the next line. The next paragraph should clear that up for you.
The input data file INTDATA.TXT has four data points per line but the first loop in the program READINTS.PAS requests only three each time through the loop. The first time through, it reads the values 101, 102, and 103, and displays those values, leaving the input pointer just prior to the 104, because it is a Read procedure. The next time through, it reads the value 104, advances to the next line and reads the values 105, and 106, leaving the pointer just prior to the 107. This continues until the 5 passes through the loop are completed.
The loop in lines 19 through 22 contains a Readln procedure and also reads the values 101, 102, and 103, but when the input parameter list is satisfied, it moves the pointer to the beginning of the next line, leaving it just before the 105. The values are printed out and the next time we come to the Readln, we read the 105, 106, and 107, and the pointer is moved to the beginning of the next line. It would be good to run the program now to see the difference in output data for the two loops. Remember that the only difference is that the first loop uses the Read procedure, and the second uses the Readln procedure.
When you come back to the program again, observe the last two loops, which operate much like the first two except that there are now five requested integer variables, and the input file still only has four per line. This is no problem. Both input procedures will simply read the first four in the first line, advance to the second line for its required fifth input, and each will do its own operation next. The Read procedure will leave the input pointer just before the second data point of the second line, and the Readln will advance the input pointer to the beginning of the third line. Compile and run this program and observe the four output fields to see an illustration of these principles.
NOW TO READ SOME REAL VARIABLES FROM A FILE
Example program ------> READDATA.PAS
Examine the file named REALDATA.TXT supplied on your Pascal Tutorial disk. You will see 8 lines of what appears to be scrambled data, but it is good data that Pascal can read. Notice especially line 4 which has some data missing, and line 6 which has some extra data. Examine the program file READDATA.PAS which will be used to illustrate the method of reading real type data. Everything should be familiar to you, since there is nothing new here. The Readln statement is requesting one integer type variable, and three real type variables, which is what most of the input file contains. When we come to the fourth line, there are not enough data points available, so the first two data points of the next line are read to complete the fourth pass through the loop. Since the file pointer is advanced to the beginning of the next line, we are automatically synchronized with the data again. When we come to the sixth line, the last two data points are simply ignored. Run the program to see if the results are as you would predict.
If a Read were substituted for the Readln in line 14 of the program, the file pointer would not be advanced to the beginning of line 6 after the fourth pass through the loop. The next attempt to read would result in trying to read the value 0.0006 as an integer, and a run time error would result. Modify the program, substituting a Read for the Readln in line 14, and see if this is not true. It will be left as an exercise for the diligent student to add code to detect and act on the error in a manner similar to that illustrated in READSTOR.PAS earlier in this chapter.
It should be pointed out that TURBO Pascal requires a digit both before and after the decimal point in all data that is to be read in as real type data or it will be flagged as a run-time error and the program will be halted. The digits can be zero as they are in several places in the example file but they must be there.
That is all there is to reading and writing text files. If you learn the necessities, you will not be stumbling around in the area of input/output which is very intimidating to many people. Remember to Assign, then Reset before reading, Rewrite before writing, and Close before quitting. It is of the utmost importance to close a file you have been writing to before quitting to write the buffer to the disk file, but it is not as important to close read files unless you are using a lot of them, as there is an implementation dependent limit of how many files can be open at once. It is possible to read from a file, close it, reopen it, and write to it in one program. You can reuse a file as often as you desire in a program, but you cannot read from and write into a file at the same time.
NOW FOR BINARY INPUT AND OUTPUT
Example program ------> BINOUT.PAS
Examine the file BINOUT.PAS for an example of writing data to a file in binary form. First there is a record defined in the type declaration part composed of three different variable types. In the var part, Output_File is defined as a "file of Dat_Rec", the record defined earlier. The variable Dog_Food is then defined as an array of the record, and a simple variable is defined.
Any file assigned a type of text, which is a "file of char", is a text file. A text file can be read and modified with a text editor, printed out, displayed on the monitor, etc. If a file is defined with any other definition, it will be a binary file and will be in an internal format as defined by the Pascal compiler and may not be readable by any compiler other than the one used to write it. Attempting to display such a file will result in very strange looking gibberish on the monitor.
When we get to the program, the output file is assigned a name in line 15, and a Rewrite is performed on it to reset the input pointer to the beginning of the file, empty the file, and prepare for writing data into it. The loop in lines 18 through 22 simply assigns nonsense data to all of the variables in the 20 records so we have something to work with.
We write a message to the display that we are ready to start outputting data, then we output the data one record at a time with the standard Write statement. A few cautions are in order here. The output file can be defined to store any simple variable type, integer, byte, real, or a record, but the types cannot be mixed. The record itself however, can be any combination of data including other records if desired, but any file can only have one type of record written to it.
A Writeln statement is illegal when writing to a binary file because a binary file is not line oriented. A Write statement is limited to one output field per statement. This is not a serious limitation since it is a simple matter to put one Write statement in the program for each variable you wish to write out to the file. It is important to Close the file when you are finished writing to it.
WHY USE A BINARY FILE
A binary file written by a Pascal program cannot be read by a word processor, a text editor or any other application program such as a database or spreadsheet, and it may not even be readable by a Pascal program compiled by a different companies compiler because the actual data structure is implementation dependent. It can't even be read by a Pascal program using the same compiler unless the data structure is identical to the one used to write the file. With all these rules, it seems like a silly way to output data, but there are advantages to using a binary output.
A binary file uses less file space than a corresponding text file because the data is stored in a packed mode. Since all significant digits of real data are stored, it is more precise unless you are careful to output all significant data to the corresponding text file. Finally, since the binary data does not require formatting into ASCII characters, it will be considerably faster than outputting it in text format. When you run this example program, it will create the file KIBBLES.BIT, and put 20 records in it. Look for this file in your default directory to verify its existence. If you try to edit this file, you will have a real mess on your monitor because it does not contain char type data.
READING A BINARY FILE
Example program ------> BININ.PAS
BININ.PAS is another example program that will read in the file we just created. Notice that the variables are named differently, but the types are all identical to those used to write the file and they are in the same order. An additional line is found in the program, the if statement. We must check for the "end of file" marker to stop reading when we find it or Pascal will list an error and terminate operation. Three pieces of information are written out to verify that we actually did read the data file in.
Once again, a few rules are in order. A Readln is illegal since there are no lines in a binary file, and only one variable or record can be read in with each Read statement.
FILE POINTERS, GET, AND PUT STATEMENTS
File pointers and the Get and Put procedures are a part of standard Pascal, but since they are redundant and therefore not needed, they are not a part of TURBO Pascal. The standard Read and Write procedures are more flexible, more efficient, and easier to use. The use of Get and Put will not be illustrated or defined here. If you ever have a need for them, they should be covered in detail in your Pascal reference manual for the particular implementation you are using.
Pointers will be covered in detail in the next chapter of this tutorial.
Advance to Chapter 12
Return to the Table of Contents