Search code examples
delphifile-iofixed-length-record

Reading a text file with fixed length fields and records in Delphi


I need to read data from a text file where the field lengths and record lengths are fixed. Fields are either zero padded or space padded, always appear in the same order and each record is terminated by a CRLF. The file can have one of three possible record types determined by the first character in the record.

So far I've create a base class for all record types and a child class for each record type.

type
  TRecordBase = class abstract
  public
    // Various common fields...
    function ToString: string; virtual; abstract;
    procedure Read(AString: string); virtual; abstract;
  end;

  TRecordType1 = class(TRecordBase)
  public
    //RecordType1 fields...
    function ToString: string; override;
    procedure Read(AString: string); override;
  end;

  TRecordType2 = class(TRecordBase)
  public
    //RecordType2 fields...
    function ToString: string; override;
    procedure Read(AString: string); override;
  end;

  TRecordType3 = class(TRecordBase)
  public
    //RecordType3 fields...
    function ToString: string; override;
    procedure Read(AString: string); override;
  end;

Then I simply read each line of the file as a string, determine its type from the first character, create the appropriate class instance and call Read.

The idea is that the Record classes can be used for both reading and writing to a string representation of the record. The Read procedure needs to break up a string and assign it to public fields.

I have two(or three) questions:

  • Is this a good approach to handle this type of file?
  • If so, what would your implementation of the Read procedure look like? (I've dealt with delimited files but this is my first encounter with fixed length fields)
  • If not, what approach would you take?

Update

Just thought I'd fill in some of the missing details. These record classes are essentially DTOs (data transfer objects). The fields are declared public and the only methods are for conversion to/from a string. The only data validation on the fields is the compiler's type checking. Fields are converted to string in the required order using TStringBuilder.AppendFormat. This ensures fields are padded and/or truncated to the proper length.

I went with Rob's suggestion to use Copy combined with the appropriate StrTo* for getting data from the string. I've also defined field positions and lengths as class constants, i.e.

const Field1Pos = 1;
const Field1Length = 1;
const Field2Pos = 2;
const Field2Length = 5;

The consts are a little easier to read than "magic numbers" in the calls to Copy.

Any other suggestions would be appreciated.


Solution

  • I'd change one thing: Replace the read procedure with an Read constructor, something like this:

    TRecordBase = class
    public
      constructor CreateFromText(Text:string);virtual;abstract;
    end;
    
    TRecordType1 = class(TRecordBase)
    public
      constructor CreateFromText(Text:string);override;
    end;
    

    Depending on what you do with your records this will save some typing and make code easier to read:

    var s:string; // string from stream or string-list
    if s[1] = 'X'then DoSomethingWith(TRecordType1.Create(s));
    

    Having a virtual constructor is also handy if the number of record types grows. You can do something like this:

    // Define an class type
    type TRecordBaseClass = class of TRecordBase;
    
    // Using Delphi 2010? Use a dictionary to register (FirstChar, TRecordBaseClass) paris
    var RecordClassDictionary = TDictionary<char, TRecordBaseClass>;
    
    // Init the dictionary like this:
    RecordClassDictionary.Add('1', TRecordType1);
    RecordClassDictionary.Add('2', TRecordType2);
    RecordClassDictionary.Add('3', TRecordType3);
    
    // And use it like this:
    var RecordBaseClass: TRecordBaseClass;
    for line in TextToParse do
      if RecordClassDictionary.TryGetValue(line[1], RecordBaseClass) then
         // Read the record, do something with the record
         DoSomethingWithTheRecord(RecordBaseClass.CreateFromText(line))
      else
         raise Exception.Create('Unkown record type.');