{ *********************************************************************** }
{                                                                         }
{ Delphi Visual Component Library                                         }
{                                                                         }
{ Copyright (c) 1995-2004 Borland Software Corporation                    }
{                                                                         }
{ *********************************************************************** }

///TODO: Document that CLR strings are zero relative not one relative (like Delphi)

unit Borland.Vcl.StrUtils;

interface

{ ResemblesText returns true if the two strings are similar (using a
  Soundex algorithm or something similar) }

function ResemblesText(const AText, AOther: string): Boolean;

{ ContainsText returns true if the subtext is found, without
  case-sensitivity, in the given text }

function ContainsText(const AText, ASubText: string): Boolean;

{ StartsText & EndText return true if the leading or trailing part
  of the given text matches, without case-sensitivity, the subtext }

function StartsText(const ASubText, AText: string): Boolean;
function EndsText(const ASubText, AText: string): Boolean;

{ ReplaceText will replace all occurrences of a substring, without
  case-sensitivity, with another substring (recursion substring replacement
  is not supported) }

function ReplaceText(const AText, AFromText, AToText: string): string;

{ MatchText & IndexText provide case like function for dealing with
  strings }

function MatchText(const AText: string; const AValues: array of string): Boolean;
function IndexText(const AText: string; const AValues: array of string): Integer;

{ These function are similar to some of the above but are case-sensitive }

function ContainsStr(const AText, ASubText: string): Boolean;
function StartsStr(const ASubText, AText: string): Boolean;
function EndsStr(const ASubText, AText: string): Boolean;
function ReplaceStr(const AText, AFromText, AToText: string): string;
function MatchStr(const AText: string; const AValues: array of string): Boolean;
function IndexStr(const AText: string; const AValues: array of string): Integer;


{ These function are similar to some of the above but work on AnsiStrings.
  Overloaded WideString versions are provided to avoid unneccessary
  implicit conversions if a WideString is passed in place of an AnsiString }

function AnsiResemblesText(const AText, AOther: AnsiString): Boolean; overload;
function AnsiResemblesText(const AText, AOther: WideString): Boolean; overload; deprecated;

function AnsiContainsText(const AText, ASubText: AnsiString): Boolean; overload;
function AnsiContainsText(const AText, ASubText: WideString): Boolean; overload; deprecated;

function AnsiStartsText(const ASubText, AText: AnsiString): Boolean; overload;
function AnsiStartsText(const ASubText, AText: WideString): Boolean; overload; deprecated;

function AnsiEndsText(const ASubText, AText: AnsiString): Boolean; overload;
function AnsiEndsText(const ASubText, AText: WideString): Boolean; overload; deprecated;

function AnsiReplaceText(const AText, AFromText, AToText: AnsiString): AnsiString; overload;
function AnsiReplaceText(const AText, AFromText, AToText: WideString): WideString; overload; deprecated;

function AnsiMatchText(const AText: AnsiString;
  const AValues: array of string): Boolean; overload;
function AnsiMatchText(const AText: WideString;
  const AValues: array of string): Boolean; overload; deprecated;

function AnsiIndexText(const AText: AnsiString;
  const AValues: array of string): Integer; overload;
function AnsiIndexText(const AText: WideString;
  const AValues: array of string): Integer; overload; deprecated;

function AnsiContainsStr(const AText, ASubText: AnsiString): Boolean; overload;
function AnsiContainsStr(const AText, ASubText: WideString): Boolean; overload; deprecated;

function AnsiStartsStr(const ASubText, AText: AnsiString): Boolean; overload;
function AnsiStartsStr(const ASubText, AText: WideString): Boolean; overload; deprecated;

function AnsiEndsStr(const ASubText, AText: AnsiString): Boolean; overload;
function AnsiEndsStr(const ASubText, AText: WideString): Boolean; overload; deprecated;

function AnsiReplaceStr(const AText, AFromText, AToText: AnsiString): AnsiString; overload;
function AnsiReplaceStr(const AText, AFromText, AToText: WideString): WideString; overload; deprecated;

function AnsiMatchStr(const AText: AnsiString;
  const AValues: array of string): Boolean; overload;
function AnsiMatchStr(const AText: WideString;
  const AValues: array of string): Boolean; overload; deprecated;

function AnsiIndexStr(const AText: AnsiString;
  const AValues: array of string): Integer; overload;
function AnsiIndexStr(const AText: WideString;
  const AValues: array of string): Integer; overload; deprecated;

{ DupeString will return N copies of the given string }

function DupeString(const AText: string; ACount: Integer): string;

{ ReverseString simply reverses the given string }

function ReverseString(const AText: string): string;

{ StuffString replaces a segment of a string with another one }

function StuffString(const AText: string; AStart, ALength: Cardinal;
  const ASubText: string): string;

{ RandomFrom will randomly return one of the given strings }

function RandomFrom(const AValues: array of string): string; overload;

{ IfThen will return the true string if the value passed in is true, else
  it will return the false string }

function IfThen(AValue: Boolean; const ATrue: string;
  AFalse: string = ''): string; overload;

{ Basic-like functions }

function LeftStr(const AText: string; const ACount: Integer): string;
function RightStr(const AText: string; const ACount: Integer): string;
function MidStr(const AText: string; const AStart, ACount: Integer): string;

type
  TStringSeachOption = (soDown, soMatchCase, soWholeWord);
  TStringSearchOptions = set of TStringSeachOption;

{ SearchBuf is a search routine for arbitrary text buffers.  If a match is
  found, the function returns the index of the matching
  string in the buffer.  If no match, the function returns -1.  Specify
  soDown to search forward otherwise the search is performed
  backwards through the text.  Use SelStart and SelLength to skip "selected"
  text so that the search starts before or after (soDown) the specified text.
  Note that both SelStart and the return value are 0-offset if using an
  array of char, and 1-offset if using a string. }

function SearchBuf(Buf: array of Char; BufLen: Integer; SelStart, SelLength: Integer;
  SearchString: String; Options: TStringSearchOptions = [soDown]): Integer; overload;
function SearchBuf(Buf: string; SelStart, SelLength: Integer;
  SearchString: String; Options: TStringSearchOptions = [soDown]): Integer; overload;

{ PosEx searches for SubStr in S and returns the index position of
  SubStr if found and 0 otherwise.  If Offset is not given then the result is
  the same as calling Pos.  If Offset is specified and > 1 then the search
  starts at position Offset within S.  If Offset is larger than Length(S)
  then PosEx returns 0.  By default, Offset equals 1.  }

function PosEx(const SubStr, S: string; Offset: Integer = 1): Integer;

{ Soundex function returns the Soundex code for the given string.  Unlike
  the original Soundex routine this function can return codes of varying
  lengths.  This function is loosely based on SoundBts which was written
  by John Midwinter.  For more information about Soundex see:

    http://www.nara.gov/genealogy/coding.html

  The general theory behind this function was originally patented way back in
  1918 (US1261167 & US1435663) but are now in the public domain.

  NOTE: This function does not attempt to deal with 'names with prefixes'
        issue.
  }

type
  TSoundexLength = 1..MaxInt;

function Soundex(const AText: string; ALength: TSoundexLength = 4): string;

{ SoundexInt uses Soundex but returns the resulting Soundex code encoded
  into an integer.  However, due to limits on the size of an integer, this
  function is limited to Soundex codes of eight characters or less.

  DecodeSoundexInt is designed to decode the results of SoundexInt back to
  a normal Soundex code.  Length is not required since it was encoded into
  the results of SoundexInt. }

type
  TSoundexIntLength = 1..8;

function SoundexInt(const AText: string; ALength: TSoundexIntLength = 4): Integer;
function DecodeSoundexInt(AValue: Integer): string;

{ SoundexWord is a special case version of SoundexInt that returns the
  Soundex code encoded into a word.  However, due to limits on the size of a
  word, this function uses a four character Soundex code.

  DecodeSoundexWord is designed to decode the results of SoundexWord back to
  a normal Soundex code. }

function SoundexWord(const AText: string): Word;
function DecodeSoundexWord(AValue: Word): string;

{ SoundexSimilar and SoundexCompare are simple comparison functions that use
  the Soundex encoding function. }

function SoundexSimilar(const AText, AOther: string;
  ALength: TSoundexLength = 4): Boolean;
function SoundexCompare(const AText, AOther: string;
  ALength: TSoundexLength = 4): Integer;

{ Default entry point for AnsiResemblesText }

function SoundexProc(const AText, AOther: string): Boolean;

type
  TCompareTextProc = function(const AText, AOther: string): Boolean;

{ If the default behavior of AnsiResemblesText (using Soundex) is not suitable
  for your situation, you can redirect it to a function of your own choosing }

var
  {! Document that ResemblesProc replaces AnsiResemblesProc}
  ResemblesProc: TCompareTextProc = SoundexProc;

type
  TCharArray = array of Char;

{ CharArrayOf returns a AText as an array of char }

function CharArrayOf(const AText: string): TCharArray;

{ FindDelimiter returns the index in S of the leftmost whole
  character that matches any character in Delimiters (except null (#0)).
  The search starts at the character position specified by Offset,
  or at the beginning of the string if not specified (Offset = 1).
  Example: FindDelimiter('\.:', 'c:\filename.ext') returns 2. }

function FindDelimiter(const Delimiters, S: string): Integer; overload;
function FindDelimiter(const Delimiters, S: string; Offset: Integer): Integer; overload;

implementation

uses
  System.Text, SysUtils;

function ResemblesText(const AText, AOther: string): Boolean;
begin
  Result := False;
  if Assigned(ResemblesProc) then
    Result := ResemblesProc(AText, AOther);
end;

function ContainsText(const AText, ASubText: string): Boolean;
begin
  Result := Pos(ASubText, AText) > 0;
  if not Result then
    Result := Pos(Uppercase(ASubText), Uppercase(AText)) > 0;
end;

function ContainsStr(const AText, ASubText: string): Boolean;
begin
  Result := Pos(ASubText, AText) > 0;
end;

function StartsText(const ASubText, AText: string): Boolean;
begin
  Result := StartsStr(ASubText, AText);
  if not Result then
    Result := StartsStr(Uppercase(ASubText), Uppercase(AText));
end;

function StartsStr(const ASubText, AText: string): Boolean;
begin
  Result := (AText <> nil) and System.String(AText).StartsWith(ASubText)
end;

function EndsText(const ASubText, AText: string): Boolean;
begin
  Result := EndsStr(ASubText, AText);
  if not Result then
    Result := EndsStr(Uppercase(ASubText), Uppercase(AText));
end;

function EndsStr(const ASubText, AText: string): Boolean;
begin
  Result := (AText <> nil) and System.String(AText).EndsWith(ASubText)
end;

function ReplaceText(const AText, AFromText, AToText: string): string;
begin
  Result := StringReplace(AText, AFromText, AToText, [rfReplaceAll, rfIgnoreCase]);
end;

function ReplaceStr(const AText, AFromText, AToText: string): string;
begin
  Result := StringReplace(AText, AFromText, AToText, [rfReplaceAll]);
end;

function MatchText(const AText: string;
  const AValues: array of string): Boolean;
begin
  Result := IndexText(AText, AValues) <> -1;
end;

function MatchStr(const AText: string;
  const AValues: array of string): Boolean;
begin
  Result := IndexStr(AText, AValues) <> -1;
end;

function IndexText(const AText: string;
  const AValues: array of string): Integer;
var
  I: Integer;
begin
  Result := -1;
  for I := Low(AValues) to High(AValues) do
    if SameText(AText, AValues[I]) then
    begin
      Result := I;
      Break;
    end;
end;

function IndexStr(const AText: string;
  const AValues: array of string): Integer;
var
  I: Integer;
begin
  Result := -1;
  for I := Low(AValues) to High(AValues) do
    if SameStr(AText, AValues[I]) then
    begin
      Result := I;
      Break;
    end;
end;

{ These function are similar to some of the above but work on AnsiStrings }

function AnsiResemblesText(const AText, AOther: AnsiString): Boolean;
begin
  Result := ResemblesText(AText, AOther);
end;

function AnsiResemblesText(const AText, AOther: WideString): Boolean;
begin
  Result := ResemblesText(AText, AOther);
end;

function AnsiContainsText(const AText, ASubText: AnsiString): Boolean;
begin
  Result := ContainsText(AText, ASubText);
end;

function AnsiContainsText(const AText, ASubText: WideString): Boolean;
begin
  Result := ContainsText(AText, ASubText);
end;

function AnsiStartsText(const ASubText, AText: AnsiString): Boolean;
begin
  Result := StartsText(ASubText, AText);
end;

function AnsiStartsText(const ASubText, AText: WideString): Boolean;
begin
  Result := StartsText(ASubText, AText);
end;

function AnsiEndsText(const ASubText, AText: AnsiString): Boolean;
begin
  Result := EndsText(ASubText, AText);
end;

function AnsiEndsText(const ASubText, AText: WideString): Boolean;
begin
  Result := EndsText(ASubText, AText);
end;

function AnsiReplaceStr(const AText, AFromText, AToText: AnsiString): AnsiString;
begin
  Result := ReplaceStr(AText, AFromText, AToText);
end;

function AnsiReplaceStr(const AText, AFromText, AToText: WideString): WideString;
begin
  Result := ReplaceStr(AText, AFromText, AToText);
end;

function AnsiReplaceText(const AText, AFromText, AToText: AnsiString): AnsiString;
begin
  Result := ReplaceText(AText, AFromText, AToText);
end;

function AnsiReplaceText(const AText, AFromText, AToText: WideString): WideString;
begin
  Result := ReplaceText(AText, AFromText, AToText);
end;

function AnsiMatchText(const AText: AnsiString;
  const AValues: array of string): Boolean;
begin
  Result := MatchText(AText, AValues);
end;

function AnsiMatchText(const AText: WideString;
  const AValues: array of string): Boolean;
begin
  Result := MatchText(AText, AValues);
end;

function AnsiIndexText(const AText: AnsiString;
  const AValues: array of string): Integer;
begin
  Result := IndexText(AText, AValues);
end;

function AnsiIndexText(const AText: WideString;
  const AValues: array of string): Integer;
begin
  Result := IndexText(AText, AValues);
end;

function AnsiContainsStr(const AText, ASubText: AnsiString): Boolean;
begin
  Result := ContainsStr(AText, ASubText);
end;

function AnsiContainsStr(const AText, ASubText: WideString): Boolean;
begin
  Result := ContainsStr(AText, ASubText);
end;

function AnsiStartsStr(const ASubText, AText: AnsiString): Boolean;
begin
  Result := StartsStr(ASubText, AText);
end;

function AnsiStartsStr(const ASubText, AText: WideString): Boolean;
begin
  Result := StartsStr(ASubText, AText);
end;

function AnsiEndsStr(const ASubText, AText: AnsiString): Boolean;
begin
  Result := EndsStr(ASubText, AText);
end;

function AnsiEndsStr(const ASubText, AText: WideString): Boolean;
begin
  Result := EndsStr(ASubText, AText);
end;

function AnsiMatchStr(const AText: AnsiString;
  const AValues: array of string): Boolean;
begin
  Result := MatchStr(AText, AValues);
end;

function AnsiMatchStr(const AText: WideString;
  const AValues: array of string): Boolean;
begin
  Result := MatchStr(AText, AValues);
end;

function AnsiIndexStr(const AText: AnsiString;
  const AValues: array of string): Integer;
begin
  Result := IndexStr(AText, AValues);
end;

function AnsiIndexStr(const AText: WideString;
  const AValues: array of string): Integer;
begin
  Result := IndexStr(AText, AValues);
end;

function DupeString(const AText: string; ACount: Integer): string;
var
  LResult: System.Text.StringBuilder;
begin
  LResult := System.Text.StringBuilder.Create(Length(AText) * ACount);
  while ACount > 0 do
  begin
    LResult.Append(AText);
    Dec(ACount);
  end;
  Result := LResult.ToString;
end;

function ReverseString(const AText: string): string;
var
  LResult: System.Text.StringBuilder;
  I: Integer;
begin
  LResult := System.Text.StringBuilder.Create(Length(AText));
  for I := Length(AText) downto 1 do
    LResult.Append(AText[I]);
  Result := LResult.ToString;
end;

function StuffString(const AText: string; AStart, ALength: Cardinal;
  const ASubText: string): string;
begin
  Result := Copy(AText, 1, AStart - 1) +
            ASubText +
            Copy(AText, AStart + ALength, MaxInt);
end;

function RandomFrom(const AValues: array of string): string;
begin
  Result := AValues[Random(High(AValues) + 1)];
end;

function IfThen(AValue: Boolean; const ATrue: string;
  AFalse: string = ''): string;
begin
  if AValue then
    Result := ATrue
  else
    Result := AFalse;
end;

function LeftStr(const AText: string; const ACount: Integer): string;
begin
  Result := Copy(AText, 1, ACount);
end;

function RightStr(const AText: string; const ACount: Integer): string;
begin
  Result := Copy(AText, Length(AText) + 1 - ACount, ACount);
end;

function MidStr(const AText: string; const AStart, ACount: Integer): string;
begin
  Result := Copy(AText, AStart, ACount);
end;

const
  AlphaNumeric = ['a'..'z', 'A'..'Z', '0'..'9'];

function IsWordDelimiter(const AChar: Char): Boolean;
begin
  Result := not (AChar in AlphaNumeric);
end;

function SearchBuf(Buf: string; SelStart, SelLength: Integer;
  SearchString: String; Options: TStringSearchOptions): Integer;
begin
  Result := SearchBuf(CharArrayOf(Buf), Length(Buf), SelStart - 1,
    SelLength, SearchString, Options);
  if Result >= 0 then Inc(Result);
end;

function SearchBuf(Buf: array of Char; BufLen: Integer; SelStart, SelLength: Integer;
  SearchString: String; Options: TStringSearchOptions): Integer;
var
  SearchCount, I: Integer;
  Direction: Shortint;

  function GetCompareChar(const AChar: Char; Options: TStringSearchOptions): Char;
  begin
    if not (soMatchCase in Options) then
      Result := UpCase(AChar)
    else
      Result := AChar;
  end;

  function FindNextWordStart(Buf: array of Char; var BufPos: Integer; var SearchCount: Integer; var Direction: shortint): Boolean;
  begin                   { (True XOR N) is equivalent to (not N) }
                          { (False XOR N) is equivalent to (N)    }
     { When Direction is forward (1), skip non delimiters, then skip delimiters. }
     { When Direction is backward (-1), skip delims, then skip non delims }
    while (SearchCount > 0) and
          ((Direction = 1) xor (IsWordDelimiter(Buf[BufPos]))) do
    begin
      Inc(BufPos, Direction);
      Dec(SearchCount);
    end;
    while (SearchCount > 0) and
          ((Direction = -1) xor (IsWordDelimiter(Buf[BufPos]))) do
    begin
      Inc(BufPos, Direction);
      Dec(SearchCount);
    end;
    Result := SearchCount > 0;
    if Direction = -1 then
    begin   { back up one char, to leave BufPos on first non delim }
      Dec(BufPos, Direction);
      Inc(SearchCount);
    end;
  end;
begin
  Result := -1;
  if BufLen <= 0 then Exit;
  if soDown in Options then
  begin
    Direction := 1;
    Inc(SelStart, SelLength);  { start search past end of selection }
    SearchCount := BufLen - SelStart - Length(SearchString) + 1;
    if SearchCount < 0 then Exit;
    if Longint(SelStart) + SearchCount > BufLen then Exit;
  end
  else
  begin
    Direction := -1;
    Dec(SelStart, Length(SearchString));
    SearchCount := SelStart + 1;
  end;
  if (SelStart < 0) or (SelStart >= BufLen) then Exit;
  Result := SelStart;

  if not (soMatchCase in Options) then
    SearchString := UpperCase(SearchString);

  while SearchCount > 0 do
  begin
    if (soWholeWord in Options) and (Result <> SelStart) then
      if not FindNextWordStart(Buf, Result, SearchCount, Direction) then Break;
    I := 0;
    while (GetCompareChar(Buf[Result + I], Options) = SearchString[I+1]) do
    begin
      Inc(I);
      if I >= Length(SearchString) then
      begin
        if (not (soWholeWord in Options)) or
           (SearchCount = 0) or
           IsWordDelimiter(Buf[Result + I]) then
          Exit;
        Break;
      end;
    end;
    Inc(Result, Direction);
    Dec(SearchCount);
  end;
  Result := - 1;
end;

function PosEx(const SubStr, S: string; Offset: Integer = 1): Integer;
begin
  if (Offset <= 0) or (S = nil) or (OffSet > Length(S)) then
    Result := 0
  else
  // CLR strings are zero relative
    Result := System.String(S).IndexOf(SubStr, Offset - 1) + 1;
end;

const

  // This table gives the Soundex score for all characters upper- and lower-
  // case hence no need to convert.  This is faster than doing an UpCase on the
  // whole input string.  The 5 non characters in middle are just given 0.
  CSoundexTable: array[65..122] of ShortInt =
  // A  B  C  D  E  F  G  H   I  J  K  L  M  N  O  P  Q  R  S  T  U  V  W   X  Y  Z
    (0, 1, 2, 3, 0, 1, 2, -1, 0, 2, 2, 4, 5, 5, 0, 1, 2, 6, 2, 3, 0, 1, -1, 2, 0, 2,
  // [  /  ]  ^  _  '
     0, 0, 0, 0, 0, 0,
  // a  b  c  d  e  f  g  h   i  j  k  l  m  n  o  p  q  r  s  t  u  v  w   x  y  z
     0, 1, 2, 3, 0, 1, 2, -1, 0, 2, 2, 4, 5, 5, 0, 1, 2, 6, 2, 3, 0, 1, -1, 2, 0, 2);

{ This function is loosely based on SoundBts which was written by John Midwinter }
function Soundex(const AText: string; ALength: TSoundexLength): string;

  function Score(AChar: Integer): Integer;
  begin
    Result := 0;
    if (AChar >= Low(CSoundexTable)) and (AChar <= High(CSoundexTable)) then
      Result := CSoundexTable[AChar];
  end;

var
  I, LScore, LPrevScore: Integer;
begin
  Result := '';
  if AText <> '' then
  begin
    Result := Upcase(AText[1]);
    LPrevScore := Score(Ord(AText[1]));
    for I := 2 to Length(AText) do
    begin
      LScore := Score(Ord(AText[I]));
      if (LScore > 0) and (LScore <> LPrevScore) then
      begin
        Result := Result + IntToStr(LScore);
        if Length(Result) = ALength then
          Break;
      end;
      if LScore <> -1 then
        LPrevScore := LScore;
    end;
    if Length(Result) < ALength then
      Result := Copy(Result + DupeString('0', ALength), 1, ALength);
  end;
end;

function SoundexInt(const AText: string; ALength: TSoundexIntLength): Integer;
var
  LResult: string;
  I: Integer;
begin
  LResult := Soundex(AText, ALength);
  Result := Ord(LResult[1]) - Ord('A');
  if ALength > 1 then
  begin
    Result := Result * 26 + StrToInt(LResult[2]);
    for I := 3 to ALength do
      Result := Result * 7 + StrToInt(LResult[I]);
  end;
  Result := Result * 9 + ALength;
end;

function DecodeSoundexInt(AValue: Integer): string;
var
  I, LLength: Integer;
begin
  Result := '';
  LLength := AValue mod 9;
  AValue := AValue div 9;
  for I := LLength downto 3 do
  begin
    Result := IntToStr(AValue mod 7) + Result;
    AValue := AValue div 7;
  end;
  if LLength > 2 then
    Result := IntToStr(AValue mod 26) + Result;
  AValue := AValue div 26;
  Result := Chr(AValue + Ord('A')) + Result;
end;

function SoundexWord(const AText: string): Word;
var
  LResult: string;
begin
  LResult := Soundex(AText, 4);
  Result := Ord(LResult[1]) - Ord('A');
  Result := Result * 26 + StrToInt(LResult[2]);
  Result := Result * 7 + StrToInt(LResult[3]);
  Result := Result * 7 + StrToInt(LResult[4]);
end;

function DecodeSoundexWord(AValue: Word): string;
begin
  Result := IntToStr(AValue mod 7) + Result;
  AValue := AValue div 7;
  Result := IntToStr(AValue mod 7) + Result;
  AValue := AValue div 7;
  Result := IntToStr(AValue mod 26) + Result;
  AValue := AValue div 26;
  Result := Chr(AValue + Ord('A')) + Result;
end;

function SoundexSimilar(const AText, AOther: string; ALength: TSoundexLength): Boolean;
begin
  Result := Soundex(AText, ALength) = Soundex(AOther, ALength);
end;

function SoundexCompare(const AText, AOther: string; ALength: TSoundexLength): Integer;
begin
  Result := AnsiCompareStr(Soundex(AText, ALength), Soundex(AOther, ALength));
end;

function SoundexProc(const AText, AOther: string): Boolean;
begin
  Result := SoundexSimilar(AText, AOther);
end;

function CharArrayOf(const AText: string): TCharArray;
begin
  if (AText <> nil) then
    Result := System.String(AText).ToCharArray
  else
    SetLength(Result, 0);
end;

function FindDelimiter(const Delimiters, S: string): Integer;
begin
  Result := FindDelimiter(Delimiters, S, 1);
end;

function FindDelimiter(const Delimiters, S: string; Offset: Integer): Integer;
begin
  if (S <> nil) then
  begin
    if Offset < 1 then
      Offset := 1;
    Result := System.String(S).IndexOfAny(CharArrayOf(Delimiters), Offset - 1) + 1;
  end
  else
    Result := 0;
end;

end.
