String manipulation and parsing is often a place where we wish that we had
more functions and methods to do our work for us. There are many existing
methods that cover our work-a-day needs, but every now and then we need
something more.
In Paradox for Windows, there is an immensely useful function called
BreakApart(). It can do so many things that I have written one (or two)
for Delphi.
Here is how it works:
You pass the base string that you want to parse and the string that you
want to use for the BreakApart(). The function then separates the string
into several pieces and fills a TStringList with their values.
Example:
Base String: "Here we go."
Break String: " "
Resulting array:
TStringList[0]: "Here"
TStringList[1]: "we"
TStringList[2]: "go."
Note that the break string, in this case spaces, is not to be seen
anywhere in the output.
Now that we know what is done, lets take a look at how to do it.
First, lets look at the string version.
function sBreakApart(BaseString, BreakString: string; StringList: TStringList): TStringList;
var
EndOfCurrentString: byte;
TempStr: string;
begin
repeat
EndOfCurrentString := Pos(BreakString, BaseString);
if EndOfCurrentString = 0 then
StringList.add(BaseString)
else
StringList.add(Copy(BaseString, 1, EndOfCurrentString - 1));
BaseString := Copy(BaseString, EndOfCurrentString + length(BreakString), length(BaseString) - EndOfCurrentString);
until EndOfCurrentString = 0;
result := StringList;
end;
This is a fairly straight forward version (as compared with the PChar
version). We look for the break string. If we don’t find it at all,
then we just assign it to the TStringList and go on our merry way.
(Note: The TStringList must be created outside the function or there
will be problems when the variable goes out of scope.) If we do find it,
then we want to extract that portion of the string and assign it to the
next available position in the TStringList. Note that the string is
updated each step of the way. This is done just because it simplifies
the code. There are several possible ways of doing this. I just picked
one.
Let me answer some questions that I hear you asking.
1. Why didn’t I make this a procedure? It is true that the TStringList
would reflect all changes without a value returned, but I left it as a
function because it allows for use in another function that uses the
TStringList. I use the function in this way later in the article.
E.g. Listbox1.items.assign(sBreakApart(...));
2. It doesn’t interfere with calling it as if it was a procedure.
(I.e. You don’t need to catch the result if you don’t want to code it that
way.)
Now that we have seen that it does something, how about if we have it do
something useful.
Here is a search and replace that uses the string version of the
BreakApart() function.
function ReplaceStr(BaseString, ReplaceThis, WithThis: string): string;
var
t: TStringList;
i: integer;
begin
t := TStringList.create;
sBreakApart(BaseString, ReplaceThis, t);
if t.count > 1 then
begin
result := '';
for i := 0 to t.count - 2 do
result := result + t[i] + WithThis;
result := result + t[i + 1];
end
else result := BaseString;
t.free;
end;
This example requires a form with a pushbutton and 3 edit boxes.
You can call this function like this:
edit1.text := ReplaceStr(edit1.text, edit2.text, edit3.text);
It replaces all occurrences of edit2.text in edit1.text with edit3.text.
I told you that this was simple and useful!
Now lets take a look at the PChar version. Since we have an idea of how
this works now, I’ll show the code first.
function pBreakApart(BaseString, BreakString: PChar; StringList: TStringList): TStringList;
var
BreakStringLength: word;
pEndOfCurrentString, pEndOfBaseString: PChar;
{Automatically gets memory allocated for it.}
temp: array[0..255] of char;
begin
{Initialize the pointers.}
BreakStringLength := StrLen(BreakString);
pEndOfBaseString := BaseString;
inc(pEndOfBaseString, StrLen(BaseString));
repeat
pEndOfCurrentString := StrPos(BaseString, BreakString);
StringList.add(StrPas(StrLCopy(temp, BaseString, pEndOfCurrentString - BaseString)));
inc(BaseString, pEndOfCurrentString - BaseString + BreakStringLength);
until BaseString >= pEndOfBaseString - BreakStringLength;
result := StringList;
end;
This takes a different approach to solving the same problem. Since this
is done with a PChar that can be of greatly varying size, it was best to
do it with pointers and pointer arithmetic. Since we are not changing the
value passed in (which would be bad as it is passed by reference and that
would change the values in their original memory locations) we need a way
to keep track of just where we are in the current part of the process.
A word about pointer arithmetic... The Inc() and Dec() functions have an
undocumented feature that allows for pointer arithmetic. The relevant
feature is that the function "knows" the size of the object pointed to
and increments the pointer the correct number of bytes.
Here is an example that uses the PChar version:
procedure TForm1.Button1Click(Sender: TObject);
var
f: file;
pStr: PChar;
LengthOfFile: integer;
t: TStringList;
begin
{Get the information.}
AssignFile(f, 'c:\autoexec.bat');
{Because this is not a text file type, the record size is 1 (char)}
Reset(f, 1);
LengthOfFile := FileSize(f) + 1; {Add one for the null terminator.}
pStr := AllocMem(LengthOfFile); {Zeros the memory also.}
BlockRead(f, pStr^, LengthOfFile - 1);
CloseFile(f);
t := TStringList.create;
listBox1.items.assign(pBreakApart(pStr, #13#10, t));
t.free;
FreeMem(pStr, LengthOfFile);
end;
This example requires a form that has a listbox and a pushbutton. It reads
the autoexec.bat file into memory in a single gulp using Blockread().
There is just enough memory allocated to get the job done. This is done
by using the file’s size as the basis. The PChar version is called and
assigned directly, and the file is "broken apart" by carriage return/line
feed. (Note: I know that a LoadFromFile() will do this also, but this is
an exercise in memory juggling.) Then memory clean up is performed.
The contents of the autoexec.bat file are then displayed in the listbox
line by line.
The uses for this are many and varied. If you used this on a filename with
the full path and did a BreakApart() on "\", you would have element 0 of
the list as the drive, and the last element would be the file name. You
could break that apart on the ".", and get the separated file name and
extension.
I did not include error checking here. A string can only hold 255
characters. It is possible that your users might try to put more than
that in there. If you want to do the error checking for them, then I
wish you well. I thought that it would be beyond the scope of this short
article and left it to the reader.