Understanding what files are and choosing a Delphi file type - part 2

What is a File?  How are they stored? What format is best for my project? - The second part of a series by Philip Rayment


Which file type should I use?

By now I hope you understand that, in theory, you can use any Delphi file type to read and write any file (although using a TextFile to read a file that is not text generally won't work very well). You could, for example, use an untyped file or a file of char, to read and write ASCII text. You could, in theory, use an untyped file to write an executable program file.

In practice, of course, particular file types work better with some files than others. If your file contains just strings of text, TextFile is the obvious choice. Similarly, if your file contains data of a single type (including records), a typed file is the way to go. However, there are always going to be those occasions when you want a file to contain data of various types. For example, a Windows resource file might contain some icons, some bitmaps, and some cursors.

Let us have a look at examples of how to save some employee data with each of the three file types. We will include a file version number at the start of the file. Unless you are sure that your file format will never change, a file version number is a good idea so that your program will know what format the data is in. The Untyped and Text files will also include a value indicating the number of records. This is not essential but assists with reading in the information.

The Typed file does not need this as the number of records can be easily calculated by dividing the size of the file by the size of the type. There is of course more than one way to write and read files; the examples below are not necessarily the only ways. In each of these examples let's assume the following declarations:

Type
  PersonRecord = packed record
    ChistianName: string[15];
    Surname:      string[15];
    Address1:     string[30];
    Address2:     string[30];
    Town:         string[15];
    Postcode:     word;	  {zip code for Americans}
    Birthdate:    Tdate;
    YearsService: byte
    ID:           word;
  End;   {PersonRecord}	{this record is 114 bytes long}

Var
  People: array of PersonRecord;

Const
   LatestFileVersion = 3;

We will use two records with the following values so that we can work out the file sizes for each method:

 

Record 1

Record 2

ChistianName

Fred

Josephine

Surname

Smith

Black-Forest

Address1

13 Railway Crescent

Flat 16

Address2

 

144 Carrington Highway

Town:

Smallville

Williamstown East

Postcode

9053

8405

Birthdate

29-2-1952

25-12-1970

YearsService

15

3

ID

14587

34423

In the code examples, the figures in square brackets are the bytes written by each statement for each of the two records.

Example 1: Untyped File
procedure WriteFile(filename:string);
var
  fil: file;
  i: integer;
  num: word;     {allows up to 65535 records}
const  ver:    byte = LatestFileVersion;

   procedure WriteString(s:ShortString);
   begin   {WriteString}
     BlockWrite(fil,s,succ(length(s));		
   end;   {WriteString}

begin   {WriteFile}
   assignFile(fil,filename); rewrite(fil,1); {Create the file}
   BlockWrite(fil,ver,sizeof(ver));                     [1]{Write the file version}
   num:=length(People);
   BlockWrite(fil,num,sizeof(num));                     [2]{Write the number of records}
   for i:=0 to high(people) do
       with people[i] do begin	{write the data}
         WriteString(ChristianName);                    [5,9]
         WriteString(Surname);                          [6,13]
         WriteString(Address1);                         [20,8]
         WriteString(Address2);                         [1,23]
         WriteString(Town);                             [11,18]
         BlockWrite(fil,Postcode,sizeof(Postcode));	[2,2]
         BlockWrite(fil,Birthdate,sizeof(Birthdate));	[8,8]
         BlockWrite(fil,YearsService,sizeof(YearsService)); [1,1]
         BlockWrite(fil,ID,sizeof(ID));                     [2,2]
       end;   {with}
   CloseFile(fil);
end;   {WriteFile}

procedure ReadFile(filename:string);
var    fil:    file;
        i:      integer;
        num:    word;     {allows up to 65535 records}
ver:    byte;
 
   function ReadString:ShortString;
   begin   {ReadString}
     BlockRead(fil,result,1);               {Read the length of the string}
     BlockRead(fil,s[1],length(s));         {Read the string itself}
   end;   {ReadString}

begin   {ReadFile}
   assignFile(fil,filename); reset(fil,1);  {Open the file}
   BlockRead(fil,ver,sizeof(ver));      {Read the file version}
   BlockRead(fil,num,sizeof(num));      {Read the number of records}
   SetLength(People,num);
   for i:=0 to high(people) do
       with people[i] do begin	  {Read the data}
         ChristianName:=ReadString;
         Surname:=ReadString;
         Address1:=ReadString;
         Address2:=ReadString;
         Town:=ReadString;
         BlockRead(fil,Postcode,sizeof(Postcode));
         BlockRead(fil,Birthdate,sizeof(Birthdate));
         BlockRead(fil,YearsService,sizeof(YearsService));
         BlockRead(fil,ID,sizeof(ID));
       end;   {with}
   CloseFile(fil);
end;   {WriteFile}

Analysis

The total file size is 143 bytes, the smallest of our examples, but the most complex to write. We had to use a temporary variable (ver) as the BlockWrite statement requires variables, not constants. If we later need to increase the maximum length of a surname, for example, changing the record declaration is all that is required.

Example 2: File of Record
procedure WriteFile(filename:string);
var
  fil:    file of PersonRecord;
  i:      integer;
  rec:    PersonRecord
begin   {WriteFile}
   assignFile(fil,filename); rewrite(fil); {Create file}
   fillchar(rec,sizeof(rec),0);  {clear fields (not necessary)}
   rec.postcode:=LatestFileVersion;       {any suitable numeric field would do}
   Write(fil,rec);              [114]       {write a record containing the file version}
   for i:=0 to high(people) do
     Write(fil,People[i]);      [114,114]  {Write the data}
   CloseFile(fil);
end;   {WriteFile}

procedure ReadFile(filename:string);
var fil:    file of PersonRecord;
  i: integer;
  rec: PersonRecord
  ver: byte;
begin   {ReadFile}
  assignFile(fil,filename); reset(fil);		  {Open the file}
  Read(fil,rec);					  {Read a record containing the file version...}	
  ver:=rec.postcode;				  {... and extract the file version from it}
  SetLength(people,pred(filesize(fil) div sizeof(rec));   {calculate number of records.}
  for i:=0 to high(people) do Read(fil,People[i]);	  {Read the data}
  CloseFile(fil);
end;   {ReadFile}

Analysis

The total file size is 342 bytes, by far the largest of our examples, but also the easiest to write. The space is in the unused parts of the strings, which were designed to hold the largest likely names and addresses, plus in the additional record we used at the start just to hold the file version. If we decide later that we need to allow longer strings, we not only need to change the record definition, but also all the files already written this way . Thus while it is the easiest to write, it is probably the hardest to change.

Click here to read the final part of this Article series.

<< Part 1 Part 2 Part 3 >>