DelphiBasics
  Home  |  Writing a class unit
 Documents
 Tutorials

 Writing your first program
 Writing your second program
 Amending this program

 Delphi data types
   Numbers
   Text (strings and chars)
   Sets and enumerations
   Arrays
   Records

 Programming logic
   Looping
   SubRoutines
   Exception handling

 Dates and times

 Files

 Pointers

 Printing text and graphics

 Object Orientation basics
   Memory leaks!
   Inheritance
   Abstraction
   Interfaces
   An example class

 References

 Standard components

 Articles

 A brief history of Delphi

 Usability : file handling

 Usability : reference books

 Author links

  Writing a class unit
A string processing class
In this tutorial we will deomnstrate many of the features and benefits of classes, and the objects that derive from them. We'll do this by creating a new string processing class that provides some string handling utilities that Delphi doesn't:
 
 * The number of words in the string
 * Replacement of all substring occurences - returning the count of changes
 * Find first and find next for a substring in the string


Defining the external view of the class
Our new class will be called TStringy and will reside in its own Unit Stringy. In fact, this unit will contain only the class definition, plus its implementation.
 
So the first thing we will look at is the class definition:
 
 type
   TStringy = Class
    // These variables and methods are not visible outside this class
    // They are purely used in the implementation below
    // Note that variables are all prefixed bt 'st'. This allows us, for example
    // to use 'WordCount' as the property name - properties cannot use the same
    // name as a variable.
     private
       stText         : String;   // The string passed to the constructor
       stWordCount    : Integer;  // Internal count of words in the string
       stFindString   : String;   // The substring used by FindFirst, FindNext
       stFindPosition : Integer;  // FindFirst/FindNext current position
 
       procedure GetWordCount;                 // Calculates the word count
       procedure SetText(const Value: String); // Changes the text string
 
    // These methods and properties are all usable by instances of the class
     published
      // Called when creating an instance (object) from this class
      // The passed string is the one that is operated on by the methods below
       constructor Create(Text : String);
 
      // Utility to replace all occurences of a substring in the string
      // The number of replacements is returned
      // This utility is CASE SENSITIVE
       function Replace(fromStr, toStr : String) : Integer;
 
      // Utility to find the first occurence of a substring in the string
      // The returned value is the found string position (strings start at 1)
      // If not found, -1 is returned
      // This utility is CASE SENSITIVE
       function FindFirst(search : String) : Integer;
 
      // Utility to find the next occurence of the FindFirst substring
      // If not found, -1 is returned
      // If no FindFirst performed before this call, -2 is returned
      // This utility is CASE SENSITIVE
       function FindNext : Integer;
 
      // The string itself - allow it to be read and overwritten
       property Text : String
           read stText
          write SetText;   // We call a method to do this
 
      // The number of words in the document. Words are groups of characters
      // separated by blanks, tabs, carriage returns and line feeds
       property WordCount : Integer
           read stWordCount;
   end;

This is quite long, but a lot of it is commentary. Click on the highlighted keywords to learn more. The new TStringy class is a type - not a variable. We are defining a class - it must be built (instantiated) into an object to be used.
 
The definition is split into sections - private for private, internal fields (data) and methods (routines), and published for externally visible fields and methods (see also Protected and Public).
 
The class is built into an object by calling the constructor Create method. This takes a string called Text. As a user of the class, we do not care what it does with this string, just so long as it remembers it. Objects are very good at that - a feature of object orientation.
 
Our class has published functions to perform the replace and find operations that we want. And it has a property to retrieve the word count. Again, we just use the property, not caring how the object calculated tha value.
 
There is also a property for reading changing the original construction string. When writing a new value, the property calls a private method SetText. This allows us to do more than just store the new string value.
 
In the private section, we hold the string, and other bits and pieces, along with internal methods. These are not visible to users of the object.
 

Implementing the class
Step by step, we will describe how the published methods are implemented. We do not need to do anything for the properties - they simply read and write to our private data fields. We could have vetted these actions by defining get and set routines to be called by the properties. See Property for more on this topic.
 
The implementation is defined in the Implementation keyword:
 
 implementation
 
 uses
  // Units used only internally by this class
   SysUtils, StrUtils;
 
 ... methods are implemented from here

Note that we are using selected Delphi units to help code our implementation. They are defined in the Uses clause before we get to the code.
 
Implementing the constructor
The constructor method is traditionally called Create - it is strongly recommended that you do so for consistency.
 
 constructor TStringy.Create(Text: String);
 begin
   stText         := Text;        // Save the passed string
   stFindPosition := 1;           // Start a search at the string start
   stFindString   := '';          // No find string provided yet
   GetWordCount;                  // Call a subroutine to get the word count
 end;

The constructor stores the passed string, prepares for the Find processing, and calculates the number of words in the string. Well not exactly - it calls a private method to do this (shown a bit further on).
 
When we change the string, we must of course recalculate the word count. The WordCount write property calls SetText does just this:
 
 procedure TStringy.SetText(const Value: String);
 begin
   stText         := Value;       // Save the passed string
   stFindPosition := 1;           // Reposition the find mechanism to the start
   GetWordCount;                  // Recalculate the word count
 end;

Private method for calculating the number of words
The GetWordCount procedure takes no parameters. It simply counts the number of words in the string, and stores the count in the private stWordCount field. Users of a TStringy object can get to this value via the WordCount property (we'll see how later).
 
Here is the code for the word count method:
 
 procedure TStringy.GetWordCount;
 const
  // Define word separator types that we will recognise
   LF    = #10;
   TAB   = #9;
   CR    = #13;
   BLANK = #32;
 var
   WordSeparatorSet : Set of Char; // We will set on only the above characters
   index  : Integer;    // Used to scan along the string
   inWord : Boolean;    // Indicates whether we are in the middle of a word
 begin
  // Turn on the TAB, CR, LF and BLANK characters in our word separator set
   WordSeparatorSet := [LF, TAB, CR, BLANK];
 
  // Start with 0 words
   stWordCount := 0;
 
  // Scan the string character by character looking for word separators
   inWord := false;
 
   for index := 1 to Length(stText) do
   begin
    // Have we found a separator character?
     if stText[index] In WordSeparatorSet
     then
     begin
      // Separator found - have we moved from a word?
       if inWord then Inc(stWordCount);   // Yes - we have ended another word
 
      // Indicate that we are not in a word anymore
       inWord := false;
     end
     else
      // Separator not found - we are in a word
       inWord := true;
   end;
 
  // Finally, were we still in a word at the end of the string?
  // If so, we must add one to the word count since we did not
  // meet a separator character.
   if inWord then Inc(stWordCount);
 end;

We'll cover this bit by bit. You can read further by clicking on the links in the code.
 
First, we define character constants for the characters that we will recognise as word separators. Then a Set variable WordSeperatorSet is defined. We set on just the word separator characters in this set of all characters. We can then use In to check if a character in the string is one of these types. This is very elegant and makes for very readable code. A nice feature of Delphi.
 
We do this in a For loop, scanning the whole length of the string. We use a Boolean flag to remember whether the last character we looked at was a character. This is quite compact code, and runs efficiently. Note that we could have done withouth the inner begin..end statements. We use them for code clarity.
 
Implementing the FindFirst method
The FindFirst method sort of cheats - it does a bit of setting up of private fields before calling the FindNext method. Nothing wrong with this - we really only need one lot of code that does the finding.
 
 function TStringy.FindFirst(search: String): Integer;
 begin
  // Here we sort of cheat - we save the search string and just call
  // FindNext after setting the initial string start conditions
   stFindString   := search;
   stFindPosition := 1;
 
   Result := FindNext;
 end;

Note that we store the return value from a function in a pseudo variable called Result. We do not have to set the value at the end of the routine. We can read from it as well.
 
Implementing the FindNext method
Here we define the nitty gritty of the substring find code:
 
 function TStringy.FindNext: Integer;
 var
   index    : Integer;
   findSize : Integer;
 begin
  /// Only scan if we have a valid scan string
   if Length(stFindString) = 0
   then Result := -2
   else
   begin
    // Set the search string size
     findSize := Length(stFindString);
 
    // Set the result to the 'not found' value
     Result := -1;
 
    // Start the search from where we last left off
     index  := stFindPosition;
 
    // Scan the string :
    // We check for a match with the first character of the fromStr as we
    // step along the string. Only when the first character matches do we
    // compare the whole string. This is more efficient.
    // We abort the loop if the string is found.
     while (index <= Length(stText)) and (Result < 0) do
     begin
      // Check the first character of the search string
       if stText[index] = stFindString[1] then
       begin
        // Now check the whole string - setting up a loop exit condition if
        // the string matches
         if AnsiMidStr(stText, index, findSize) = stFindString
         then Result := index;
       end;
 
      // Move along the string
       Inc(index);
     end;
 
    // Position the next search from where the above leaves off
    // Notice that index gets incremented even with a successful match
     stFindPosition := index
   end;
 
  // This subroutine will now exit with the established Result value
 end;

We use a While loop to scan along the string. We avoid using a For loop because we need to abandon the string scan at any time if we find the substring in it.
 
We use AnsiMidStr to extract a section of the current string at the current index position for comparing with the find string.
 
Implementing the Replace method
This is the most complex method of this class. It also uses a While loop to scan the string, but builds up the target string bit by bit. It takes characters one at a time from this string, and inserts the new toStr value in place of matches with the fromStr value.
 
 function TStringy.Replace(fromStr, toStr: String): Integer;
 var
   fromSize, count, index  : Integer;
   newText : String;
   matched : Boolean;
 begin
  // Get the size of the from string
   fromSize := Length(fromStr);
 
  // Start with 0 replacements
   count := 0;
 
  // We will build the target string in the newText variable
   newText := '';
   index := 1;
 
  // Scan the string :
  // We check for a match with the first character of the fromStr as we step
  // along the string. Only when the first character matches do we compare
  // the whole string. This is more efficient.
   while index <= Length(stText) do
   begin
    // Indicate no match for this character
     matched := false;
 
    // Check the first character of the fromStr
     if stText[index] = fromStr[1] then
     begin
       if AnsiMidStr(stText, index, fromSize) = fromStr then
       begin
        // Increment the replace count
         Inc(count);
 
        // Store the toStr in the target string
         newText := newText + toStr;
 
        // Move the index past the from string we just matched
         Inc(index, fromSize);
 
        // Indicate that we have a match
         matched := true;
       end;
     end;
 
    // If no character match :
     if not matched then
     begin
      // Store the current character in the target string, and
      // then skip to the next source string character
       newText := newText + stText[index];
       Inc(index);
     end;
   end;
 
  // Copy the newly built string back to stText - as long as we made changes
   if count > 0 then stText := newText;
 
  // Return the number of replacements made
   Result := count;
 end;

Notice that the Inc routine provided by Delphi has two methods of calling it. One with just the variable to be incremented by 1. The second where we provide a different increment value. Here, we increment teh string index by the length of the matching substring.
 
The whole of the Stringy is shown on the next page, along with sample code that illustrates use of it.
 

 
Page 1 of 2 | Using this Stringy unit
 
 
 

Delphi Basics © Neil Moffatt All rights reserved.  |  Home Page