233 lines
5.8 KiB
ObjectPascal
233 lines
5.8 KiB
ObjectPascal
{
|
|
:: X2UtNamedFormat implements Format-style functionality using named
|
|
:: instead of indexed parameters.
|
|
::
|
|
:: Last changed: $Date$
|
|
:: Revision: $Rev$
|
|
:: Author: $Author$
|
|
}
|
|
unit X2UtNamedFormat;
|
|
|
|
interface
|
|
uses
|
|
Classes;
|
|
|
|
|
|
type
|
|
TNamedFormatStringList = class(TStringList)
|
|
public
|
|
procedure AddLn();
|
|
|
|
function Format(AParams: array of const): String;
|
|
end;
|
|
|
|
|
|
{
|
|
AFormat uses the same format strings as SysUtils.Format, where each
|
|
format specifier may use a named instead of a numeric index, surrounded by
|
|
<>, eg:
|
|
|
|
%<Value1>:s %<Value2>:.2d
|
|
|
|
|
|
AParams contains alternating the parameter name and it's value.
|
|
|
|
Note: NamedFormat works by mapping names to indices and passing the result
|
|
to SysUtils.Format. Unnamed or existing indexed specifiers will therefore
|
|
be affected by named specifiers! It is strongly recommended to name all
|
|
specifiers.
|
|
}
|
|
function NamedFormat(const AFormat: String; AParams: array of const): String;
|
|
|
|
|
|
implementation
|
|
uses
|
|
SysUtils;
|
|
|
|
|
|
type
|
|
TProtectedMemoryStream = class(TMemoryStream);
|
|
|
|
|
|
const
|
|
SpecifierChar = '%';
|
|
SpecifierNameStart = '<';
|
|
SpecifierNameEnd = '>';
|
|
ValidNameChars = ['A'..'Z', 'a'..'z', '0'..'9', '_'];
|
|
|
|
|
|
procedure StreamWriteChar(const AStream: TStream; const AValue: Char);
|
|
begin
|
|
AStream.WriteBuffer(AValue, SizeOf(Char));
|
|
end;
|
|
|
|
|
|
procedure StreamWriteString(const AStream: TStream; const AValue: String);
|
|
begin
|
|
AStream.WriteBuffer(PChar(AValue)^, Length(AValue));
|
|
end;
|
|
|
|
|
|
function FindNameEnd(const APosition: PChar; const AEnd: PChar): PChar;
|
|
var
|
|
position: PChar;
|
|
|
|
begin
|
|
Result := nil;
|
|
position := APosition;
|
|
|
|
while position < AEnd do
|
|
begin
|
|
if position^ = SpecifierNameEnd then
|
|
begin
|
|
Result := position;
|
|
break;
|
|
end;
|
|
|
|
if not (position^ in ValidNameChars) then
|
|
break;
|
|
|
|
Inc(position);
|
|
end;
|
|
end;
|
|
|
|
|
|
function NamedFormat(const AFormat: String; AParams: array of const): String;
|
|
var
|
|
currentPos: PChar;
|
|
formatEnd: PChar;
|
|
formatStream: TMemoryStream;
|
|
formatString: String;
|
|
name: String;
|
|
nameEnd: PChar;
|
|
nameStart: PChar;
|
|
param: TVarRec;
|
|
paramIndex: Integer;
|
|
paramNames: TStringList;
|
|
paramValues: array of TVarRec;
|
|
specifierIndex: Integer;
|
|
errorMsg: String;
|
|
|
|
begin
|
|
if Length(AParams) mod 2 = 1 then
|
|
raise Exception.Create('AParams must contains a multiple of 2 number of items');
|
|
|
|
currentPos := PChar(AFormat);
|
|
SetLength(paramValues, 0);
|
|
|
|
formatEnd := currentPos;
|
|
Inc(formatEnd, Length(AFormat));
|
|
|
|
paramNames := TStringList.Create();
|
|
try
|
|
paramNames.CaseSensitive := False;
|
|
|
|
formatStream := TMemoryStream.Create();
|
|
try
|
|
{ Most likely scenario; the names are longer than the replacement
|
|
indexes. }
|
|
TProtectedMemoryStream(formatStream).Capacity := Length(AFormat);
|
|
|
|
while currentPos < formatEnd do
|
|
begin
|
|
{ Search for % }
|
|
if currentPos^ = SpecifierChar then
|
|
begin
|
|
StreamWriteChar(formatStream, currentPos^);
|
|
Inc(currentPos);
|
|
|
|
{ Check if this is indeed a named specifier }
|
|
if (currentPos < formatEnd) and (currentPos^ = SpecifierNameStart) then
|
|
begin
|
|
Inc(currentPos);
|
|
|
|
nameStart := currentPos;
|
|
nameEnd := FindNameEnd(currentPos, formatEnd);
|
|
|
|
if Assigned(nameEnd) then
|
|
begin
|
|
SetString(name, nameStart, nameEnd - nameStart);
|
|
|
|
specifierIndex := paramNames.IndexOf(name);
|
|
if specifierIndex = -1 then
|
|
specifierIndex := paramNames.Add(name);
|
|
|
|
StreamWriteString(formatStream, IntToStr(specifierIndex));
|
|
|
|
currentPos := nameEnd;
|
|
end;
|
|
end;
|
|
end else
|
|
StreamWriteChar(formatStream, currentPos^);
|
|
|
|
Inc(currentPos);
|
|
end;
|
|
|
|
SetString(formatString, PChar(formatStream.Memory), formatStream.Size);
|
|
finally
|
|
FreeAndNil(formatStream);
|
|
end;
|
|
|
|
SetLength(paramValues, paramNames.Count);
|
|
paramIndex := 0;
|
|
|
|
while paramIndex < High(AParams) do
|
|
begin
|
|
param := AParams[paramIndex];
|
|
|
|
case param.VType of
|
|
vtChar: name := param.VChar;
|
|
vtString: name := param.VString^;
|
|
vtPChar: name := param.VPChar;
|
|
vtAnsiString: name := PChar(param.VAnsiString);
|
|
else
|
|
raise Exception.CreateFmt('Parameter name at index %d is not a string value',
|
|
[paramIndex div 2]);
|
|
end;
|
|
|
|
Inc(paramIndex);
|
|
|
|
specifierIndex := paramNames.IndexOf(name);
|
|
if specifierIndex > -1 then
|
|
paramValues[specifierIndex] := AParams[paramIndex];
|
|
|
|
Inc(paramIndex);
|
|
end;
|
|
|
|
try
|
|
Result := Format(formatString, paramValues);
|
|
except
|
|
on E:EConvertError do
|
|
begin
|
|
errorMsg := E.Message;
|
|
|
|
{ Translate specifiers in error messages back to names }
|
|
for paramIndex := 0 to Pred(paramNames.Count) do
|
|
errorMsg := StringReplace(errorMsg, SpecifierChar + IntToStr(paramIndex) + ':',
|
|
SpecifierChar + SpecifierNameStart +
|
|
paramNames[paramIndex] + SpecifierNameEnd + ':',
|
|
[rfReplaceAll]);
|
|
|
|
raise EConvertError.Create(errorMsg);
|
|
end;
|
|
end;
|
|
finally
|
|
FreeAndNil(paramNames);
|
|
end;
|
|
end;
|
|
|
|
|
|
{ TNamedFormatStringList }
|
|
procedure TNamedFormatStringList.AddLn;
|
|
begin
|
|
Add('');
|
|
end;
|
|
|
|
|
|
function TNamedFormatStringList.Format(AParams: array of const): String;
|
|
begin
|
|
Result := NamedFormat(Text, AParams);
|
|
end;
|
|
|
|
end.
|