- 2023-02-09 – In the comments, Lee pointed out a type in the first example, which has been fixed, and an error in the logic of the final routine which tries to write a partial line if it cannot write all of it. The original code tried to write using LEN() of the string, but needs to be adjusted to subtract one, since the PRINT will add one extra character (carriage return) at the end. The code has been corrected, and actually tested not. Thanks, Lee!
In Disk BASIC, you can open a file for input and read from it like this:
10 CLEAR 255 20 OPEN "I",#1,FILE.TXT" 30 LINE INPUT #1,A$ 40 PRINT A$ 50 GOTO 30
That would keep reading lines (data terminated by an ENTER) from the file and printing it to the screen until it reached the end of the file. It would then crash and report an Input Past End Of File error:
?IE ERROR IN LINE 30
Avoiding IE ERROR
If we were using a fixed-format file, such as a configuration file, and we knew exactly what was expected to be in it, this wouldn’t be an issue. Suppose we had a file that just contained a Name, Address, City, State and Zip code. We could read just those entries like this:
0 ' NAME, ADDRESS, CITY, STATE, ZIP 10 PRINT "READING ADDRESS..." 20 OPEN "I",#1,"ADDRESS.TXT" 30 LINE INPUT #1,NM$ 40 LINE INPUT #1,AD$ 50 LINE INPUT #1,CT$ 60 LINE INPUT #1,ST$ 70 LINE INPUT #2,ZP$ 80 CLOSE #1
For arbitrarily-sized files, like a text document from a word processor, you don’t know how many lines may be in it so that won’t work.
One solution is to write out how many lines are in the file as the first entry. For a file containing seven lines of text, it might look like this:
7 THIS IS THE FIRST LINE. THIS IS SECOND. AND THIRD IS HERE. FOURTH CHECKING IN! FIFTH FOREVER. SIXTH GOES HERE. AND THIS IS THE SEVENTH AND FINAL LINE.
A routine to read this might look like:
10 INPUT "I",#1,"FILE.TXT" 20 INPUT NL 30 FOR I=1 TO NL 40 LINE INPUT #1,A$ 50 PRINT A$ 60 NEXT 70 CLOSE #1
This can also be used on cassette files just by changing the device number from #1 to #-1.
Disk BASIC provides the EOF function that will return the status of an open input file. It will return 0 as long as there is more data in the file, or -1 if the end of the file has been reached. That simplifies the reader code to look like this:
10 OPEN "I",#1,"FILE.TXT" 20 IF EOF(1)=-1 THEN 60 30 LINE INPUT #1,A$ 40 PRINT A$ 50 GOTO 20 60 CLOSE #1
And again, by changing the device from #1 to #-1 and EOF(1) to EOF(-1) it should work on cassettes. (Note to self: Does it? I haven’t actually tried this in almost forty years.)
When it comes to writing data, if you try to write more than the disk can hold, say, in an endless loop like this…
10 OPEN "O",#1,"FILE.TXT 20 PRINT #1,"THIS LINE GOES IN THE FILE" 30 GOTO 20
…you will eventually fill up the disk and get a Disk Full error:
?DF ERROR IN 20
Avoiding DF ERROR
Unfortunately, EOF only works on files opened for input and there is no similar command that tests output. And if there were, how would it work? If it just returned “yes, there is still room” versus “no, the disk is full”, what does “still room” mean? One byte left? What if you wanted to write two bytes?
Let’s pretend there is a Disk Is Full command called DIF:
10 OPEN "I",#1,"FILE.TXT" 15 ' FAKE DIF FUNCTION THAT DOESN'T EXIST 20 IF DIF(1)=-1 THEN 50 30 PRINT #1,"THIS LINE GOES IN THE FILE" 40 GOTO 20 50 CLOSE #1
Above, if the disk still had room (even if it was just one byte), line 20 would return a 0 (“yep, there is still room”) and then we’d crash with a ?DF ERROR in the next line, since we tried to write more than that one byte left.
What we really need is a command that tells us how many more bytes of data we can write to the disk, and we don’t have that.
But we can make something close. Close-ish.
New features for FREE
Disk BASIC has the FREE command which returns the number of free granules on a specified drive. There are 68 granules available on an empty disk. If you know what a granule is, that means something, but all I really knew back then was the largest program I’d ever seen was “13 grans”…
TL:DNR – A granule is 2304 bytes.
A CoCo RS-DOS (what we called Disk BASIC) disk is made up of thirty-five (35) tracks, each holding eighteen (18) 256-byte sectors. That is 161,280 bytes of storage (35 * 18 * 256), which really seemed like a bunch in the early 1980s! Track seventeen (17) is used for the disk directory and file allocation table (FAT), so there is really only 34 tracks you can use for programs or data storage which gives you 156,672 bytes (34 * 18 * 256).
For reasons I do not know, each track was divided up in to two (2) granules. That makes a granule represent nine (9) sectors. Therefore, a granule is 2304 bytes. (9 * 256 = 2304).
When FREE(0) returns 68 for an empty disk, that is 156,672 bytes free. You can multiply the value FREE returns by 2304 to see how many bytes are free.
PRINT FREE(0) 68 PRINT FREE(0)*2304 156672
Using FREE we could determine if there was still room on the disk for writing more data, as suggested by Hawksoft’s Chris Hawks on the CoCo mailing list:
On first glance, it seems like you could just check for FREE(0) to be 0, and stop writing when it is:
10 OPEN "I",#1,"FILE.TXT" 20 IF FREE(0)=0 THEN 50 30 PRINT #1,"THIS LINE GOES IN THE FILE" 40 GOTO 20 50 CLOSE #1
However, once a new file is created (even if you have written nothing to it yet), the number of free granules goes down by one since there is no longer a full granule available. This can be demonstrated using a program like this:
10 PRINT FREE(0) 20 PRINT "O",#1,"FREETEST" 30 PRINT FREE(0) 40 PRINT #1,"A"; 60 PRINT FREE(0) 70 CLOSE #1
Running that on an empty disk should print the following:
RUN 68 67 67
Above, initially there were 68 granules available, then we opened/created a new file and then there were 67 granules used. We wrote one byte (“A”) to that new file and there were still 67 unused granules. We are creating a file an allocating that 68th granule for it, whether we write any data to it or not. After writing one byte to that granule, we can not tell how much room is left in it. This means if you had started with only ONE free granule and ran this program, you would see:
1 0 0
The moment the file was opened, a granule was consumed for this new file, leaving zero granules available. Thus, the check for FREE(0) being zero would immediately be satisfied, and even though you had bytes available to use in that granule, the program would skip writing because FREE was returning zero…
This approach simple won’t work if there is only one granule left (a file would be created, then no data would be allowed to be written). And, if there was more than one granule left, it would be wasteful since even writing one byte in to the new granule would count as “full” and the rest of those 2304 bytes would be wasted.
Side Note: Since the smallest amount of allocated space you can have is one granule, a file of one byte consumed 2304 bytes on the disk, just as a file of 2304 bytes would. MS-DOS and other file systems have the same issue with their allocation sizes.
Disk BASIC has no solution for us, by we can easily come up with our own. As long as we control what is being written, we should be able to determine the size of what is being written and do something like this:
0 ' 1 ' DF - DISK FREE (BYTES) 2 ' DU - DISK USED (BYTES) 3 ' 10 DF=FREE(0)*2304:DU=0 20 OPEN "O",#1,"FREETEST" 30 A$="WRITE THIS TO THE FILE" 40 IF DU<DF THEN GOSUB 1000 ELSE 60 50 GOTO 30 60 CLOSE #1 999 END 1000 ' WRITE A$ TO BUFFER #1 1001 ' COUNT BYTES WE WILL WRITE, 1002 ' ADD ONE FOR THE ENTER 1010 DU=DU+LEN(A$)+1 1020 IF DU<DF THEN PRINT #1,A$ 1030 RETURN
The idea behind this code is that a subroutine is used to write whatever is in A$ to the disk file. That subroutine will keep track of how many bytes were written. In line 10, DF is initially set to the number of free bytes by taking FREE(0) and multiplying it by the size of a granule (2304 bytes). DU is how much of that has been used, so it starts out at 0.
In the main loop, as long as DU is less than DF, the subroutine can be called to (attempt to) write A$ to the disk file.
In the subroutine at line 1000, DU is incremented by the length of A$, and 1 is added since PRINT will add an ENTER character to the end of the line. If DU is less than DF, the line is actually written to the file. Otherwise, it is skipped.
This could be made more elegant, but it’s a simple approach that should work just fine.
Code compensation, improved
Some improvements might be to make it not just give up if all the data won’t fit, but to write as much as possibly leaving disk free at zero at the end. This could be done like this:
1000 ' WRITE A$ TO BUFFER #1 1001 ' COUNT BYTES WE WILL WRITE, 1002 ' ADD ONE FOR THE ENTER 1010 LN=LEN(A$)+1 1020 IF DU+LN>DF THEN LN=DF-DU 1030 IF LN>0 THEN PRINT #1,LEFT$(A$,LN-1) 1040 DU=DU+LN 1050 RETURN
…or something like that. The idea is if there is only ten bytes left, and you try to write 15, it will just right the first ten bytes of that string and then RETURN. This, too, could be made more elegant.
How would you improve it? Leave your suggestions in the comments…
Sadly, there’s not much you can do when using a cassette since there is no way to know how much tape is left.
Until next time…
In your first code example, you GOTO 20 which is the line that opens the file. I suspect you meant GOTO 30 to keep reading input from the already opened file. Also, in your last code example, I suspect if LEN(A$) + 1 won’t fit, then trying to write LEFT$(A$, bytesLeft) will write 1 byte too many with the carriage return (or is it line feed?) PRINT adds. You probably should write LEFT$(A$, bytesLeft – 1) so the extra byte PRINT adds doesn’t cause the DF error.
BTW, LOVE reading your posts. Look forward to seeing them pop into my inbox all the time. Keep up the great work! :)
I need to go back and check about the final carriage return. That’s an excellent observation. And thanks for catching that bug on the GOTO!
Article updated with both corrections, and I actually tested the second one this time. Thank you!
Pingback: The Coco Nation News stories for Episode 300, February 11, 2023 -