Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Mike Lewis on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

How to build and parse a list without using a deliminator?

Strings, Numbers and Text

How to build and parse a list without using a deliminator?

by  djjd47130  Posted    (Edited  )
It is a common difficulty in programming when you want to combine a list of strings together but don't want a deliminator, because the data the list contains might use that deliminator somewhere. I will explain how to accomplish this, using a method I use widely in sending packets through TCP/IP.

Technically, there is still a deliminator in this method, but it's not dividing each list item from each other. There's probably a more proper term for this, but I call it "Size Prefixing". What you do is for each list item, you put the size of the string first, then a deliminator, and then the data. When you're parsing, first you copy up to the deliminator and convert it to a number to acquire the size. Then remove that copied size text and the deliminator, and finally copy the amount of data as the size.

Take this data for example:

16|This is a sample

The number 16 is the number of characters in the data. The quick example of how to read this is like so:

Code:
const
  Delim = '|';
var
  L: TStringList;
  S, T: String
  P, Z: Integer;
begin
  L:= TStringList.Create;
  try
    S:= FullStringToBeParsed;  //Grab a copy of the full string which needs to be parsed
    P:= Pos(Delim, S);         //Find the position of the next deliminator
    T:= Copy(S, 1, P-1);       //Copy up to the deliminator
    Delete(S, 1, P);           //Delete up to and including the deliminator
    Z:= StrToIntDef(T, 0);     //Convert copied string to integer for the size
    T:= Copy(S, 1, Z);         //Copy the amount of data as the size
    Delete(S, 1, Z);           //Delete what was copied
    L.Add(T);                  //Add the copied data to the list
  finally
    L.Free;
  end;
end;

Now the above example only works for 1 list item. You would have to go into a loop and perform this. I have actually made a wrapper around the TStringList for exactly this, and called it TJDParseList. It is also thread-safe at the same time (I personally think everything everywhere should be made thread-safe from the beginning). Here's the full unit:

Code:
unit JD.Utils;

interface

uses
  Winapi.Windows, System.Classes, System.SysUtils;

const
  JD_DELIM = '|';

type
  TJDParseList = class(TPersistent)
  private
    FList: TStringList;
    FLock: TRTLCriticalSection;
    FDelim: String;
    FPrefix: String;
    function Lock: TStringList;
    procedure Unlock;
    function GetAsString: String;
    procedure SetAsString(const Value: String);
    procedure SetDelim(const Value: String);
    procedure SetPrefix(const Value: String);
    function GetItem(Index: Integer): String;
    procedure SetItem(Index: Integer; const Value: String);
  public
    constructor Create; overload;
    constructor Create(const Data: String); overload;
    destructor Destroy; override;
    procedure Clear;
    function Count: Integer;
    procedure Add(const S: String);
    procedure Delete(const Index: Integer);
    procedure Insert(const S: String; const Index: Integer);
    property Items[Index: Integer]: String read GetItem write SetItem;
  published
    property Deliminator: String read FDelim write SetDelim;
    property Prefix: String read FPrefix write SetPrefix;
    property AsString: String read GetAsString write SetAsString;
  end;

function GetToDelim(var S: String; const Delim: String = JD_DELIM): String;
function CopyDel(var S: String; const Index, Count: Integer): String;
function ParseNext(const Value: String): String;
function SizedText(const S: String; const Delim: String = JD_DELIM): String;


implementation


{ Global }

function GetToDelim(var S: String; const Delim: String = JD_DELIM): String;
var
  P: Integer;
begin
  Result:= '';                          //Initialize result
  P:= Pos(JD_DELIM, S);                 //Identify delim position
  if P > 0 then begin                   //Make sure delim exists
    Result:= Copy(S, 1, P-1);           //Copy data to result without delim
    Delete(S, 1, P);                    //Delete copied data and delim
  end;
end;

function CopyDel(var S: String; const Index, Count: Integer): String;
begin
  Result:= Copy(S, Index, Count);       //Copy data to result
  Delete(S, Index, Count);              //Delete copied data
end;

function ParseNext(const Value: String): String;
var
  S: String;
  Z: Integer;
begin
  Result:= '';                          //Initialize result
  S:= Value;                            //Copy value to temp
  Z:= StrToIntDef(GetToDelim(S), 0);    //Extract data size
  Result:= CopyDel(S, 1, Z);            //Extract data to result
end;

function SizedText(const S: String; const Delim: String = JD_DELIM): String;
begin
  Result:= IntToStr(Length(S)) + Delim + S;
end;

{ TJDParseList }

constructor TJDParseList.Create;
begin
  inherited Create;
  FDelim:= '|';
  FPrefix:= '';
  FList:= TStringList.Create;
  InitializeCriticalSection(FLock);
end;

constructor TJDParseList.Create(const Data: String);
begin
  Create;
  AsString:= Data;
end;

destructor TJDParseList.Destroy;
begin
  DeleteCriticalSection(FLock);
  FList.Free;
  inherited;
end;

procedure TJDParseList.SetDelim(const Value: String);
begin
  if Length(Value) = 1 then begin
    FDelim := Value;
  end else begin
    raise Exception.Create('Deliminator must be 1 character.');
  end;
end;

procedure TJDParseList.SetPrefix(const Value: String);
begin
  FPrefix := Value;
end;

function TJDParseList.Lock: TStringList;
begin
  Result:= nil;
  EnterCriticalSection(FLock);
  Result:= FList;
end;

procedure TJDParseList.Unlock;
begin
  LeaveCriticalSection(FLock);
end;

procedure TJDParseList.Add(const S: String);
var
  L: TStringList;
begin
  L:= Lock;
  try
    L.Add(S);
  finally
    Unlock;
  end;
end;

procedure TJDParseList.Clear;
var
  L: TStringList;
begin
  L:= Lock;
  try
    L.Clear;
  finally
    Unlock;
  end;
end;

function TJDParseList.Count: Integer;
var
  L: TStringList;
begin
  Result:= 0;
  L:= Lock;
  try
    Result:= L.Count;
  finally
    Unlock;
  end;
end;

procedure TJDParseList.Delete(const Index: Integer);
var
  L: TStringList;
begin
  L:= Lock;
  try
    L.Delete(Index);
  finally
    Unlock;
  end;
end;

procedure TJDParseList.SetItem(Index: Integer; const Value: String);
var
  L: TStringList;
begin
  L:= Lock;
  try
    L[Index]:= Value;
  finally
    Unlock;
  end;
end;

function TJDParseList.GetItem(Index: Integer): String;
var
  L: TStringList;
begin
  Result:= '';
  L:= Lock;
  try
    Result:= L[Index];
  finally
    Unlock;
  end;
end;

procedure TJDParseList.Insert(const S: String; const Index: Integer);
var
  L: TStringList;
begin
  L:= Lock;
  try
    L.Insert(Index, S);
  finally
    Unlock;
  end;
end;

function TJDParseList.GetAsString: String;
var
  X: Integer;
  L: TStringList;
begin
  Result:= '';
  L:= Lock;
  try
    Result:= FPrefix + FDelim;
    Result:= Result + IntToStr(L.Count) + FDelim;
    for X := 0 to L.Count - 1 do
      Result:= Result + SizedText(L[X]);
  finally
    Unlock;
  end;
end;

procedure TJDParseList.SetAsString(const Value: String);
var
  T, S: String;
  P, C, X: Integer;
  L: TStringList;
begin
  S:= Value;
  P:= Pos(FDelim, S);
  T:= Copy(S, 1, P-1);
  System.Delete(S, 1, P);
  if T = FPrefix then begin
    L:= Lock;
    try
      L.Clear;
      P:= Pos(FDelim, S);
      T:= Copy(S, 1, P-1);
      System.Delete(S, 1, P);
      C:= StrToIntDef(T, 0);
      for X := 0 to C - 1 do begin
        P:= Pos(FDelim, S);
        T:= Copy(S, 1, P-1);
        System.Delete(S, 1, P);
        P:= StrToIntDef(T, 0);
        T:= Copy(S, 1, P);
        System.Delete(S, 1, P);
        L.Append(T);
      end;
    finally
      Unlock;
    end;
  end else begin
    raise Exception.Create('Prefix does not match.');
  end;
end;

end.

To convert to/from a full string, use the AsString property. Keep in mind that there is still room for optimization. It's not perfect, but it does a great job. The Prefix property isn't necessary, it's just a way of validating that the full string is in fact what it should be before it attempts to parse.

A sample string to test with:

3|One3|Two5|Three3|Six5|Seven5|Eight4|Nine3|Ten6|Eleven6|Twelve8|Thirteen8|Fourteen7|Fifteen


Register to rate this FAQ  : BAD 1 2 3 4 5 6 7 8 9 10 GOOD
Please Note: 1 is Bad, 10 is Good :-)

Part and Inventory Search

Back
Top