Files
lazarus-ccr/components/fpspreadsheet/fpsnumformatparser.pas

1564 lines
46 KiB
ObjectPascal
Raw Normal View History

unit fpsNumFormatParser;
{$ifdef fpc}
{$mode objfpc}{$H+}
{$endif}
interface
uses
Classes, SysUtils, fpstypes, fpsNumFormat;
const
psOK = 0;
psErrNoValidColorIndex = 1;
psErrNoValidCompareNumber = 2;
psErrUnknownInfoInBrackets = 3;
psErrConditionalFormattingNotSupported = 4;
psErrNoUsableFormat = 5;
psErrNoValidNumberFormat = 6;
psErrNoValidDateTimeFormat = 7;
psErrQuoteExpected = 8;
psErrMultipleCurrSymbols = 9;
psErrMultipleFracSymbols = 10;
psErrMultipleExpChars = 11;
psErrGeneralExpected = 12;
psAmbiguousSymbol = 13;
type
{ TsNumFormatParser }
TsNumFormatParser = class
private
FToken: Char;
FCurrent: PChar;
FStart: PChar;
FEnd: PChar;
FCurrSection: Integer;
FStatus: Integer;
function GetCurrencySymbol: String;
function GetDecimals: byte;
function GetFracDenominator: Integer;
function GetFracInt: Integer;
function GetFracNumerator: Integer;
function GetFormatString: String;
function GetNumFormat: TsNumberFormat;
function GetParsedSectionCount: Integer;
function GetParsedSections(AIndex: Integer): TsNumFormatSection;
procedure SetDecimals(AValue: Byte);
protected
FFormatSettings: TFormatSettings;
FSections: TsNumFormatSections;
{ Administration while scanning }
procedure AddElement(AToken: TsNumFormatToken; AText: String); overload;
procedure AddElement(AToken: TsNumFormatToken; AIntValue: Integer=0; AText: String = ''); overload;
procedure AddElement(AToken: TsNumFormatToken; AFloatValue: Double); overload;
procedure AddSection;
procedure DeleteElement(ASection, AIndex: Integer);
procedure InsertElement(ASection, AIndex: Integer; AToken: TsNumFormatToken; AText: String); overload;
procedure InsertElement(ASection, AIndex: Integer; AToken: TsNumFormatToken; AIntValue: Integer); overload;
procedure InsertElement(ASection, AIndex: Integer; AToken: TsNumFormatToken; AFloatValue: Double); overload;
function NextToken: Char;
function PrevToken: Char;
{ Scanning/parsing }
procedure ScanAMPM;
procedure ScanAndCount(ATestChar: Char; out ACount: Integer);
procedure ScanBrackets;
procedure ScanCondition(AFirstChar: Char);
procedure ScanCurrSymbol;
procedure ScanDateTime;
procedure ScanFormat;
procedure ScanGeneral;
procedure ScanNumber;
procedure ScanQuotedText;
// Main scanner
procedure Parse(const AFormatString: String);
{ Analysis while scanning }
procedure AnalyzeColor(AValue: String);
function AnalyzeCurrency(const AValue: String): Boolean;
{ Analysis after scanning }
// General
procedure CheckSections;
procedure CheckSection(ASection: Integer);
procedure FixMonthMinuteToken(var ASection: TsNumFormatSection);
// Format string
function BuildFormatString: String; virtual;
public
constructor Create(const AFormatString: String;
const AFormatSettings: TFormatSettings);
destructor Destroy; override;
procedure ClearAll;
function GetDateTimeCode(ASection: Integer): String;
function IsDateTimeFormat: Boolean;
function IsTimeFormat: Boolean;
procedure LimitDecimals;
property CurrencySymbol: String read GetCurrencySymbol;
property Decimals: Byte read GetDecimals write SetDecimals;
property FormatString: String read GetFormatString;
property FracDenominator: Integer read GetFracDenominator;
property FracInt: Integer read GetFracInt;
property FracNumerator: Integer read GetFracNumerator;
property NumFormat: TsNumberFormat read GetNumFormat;
property ParsedSectionCount: Integer read GetParsedSectionCount;
property ParsedSections[AIndex: Integer]: TsNumFormatSection read GetParsedSections;
property Status: Integer read FStatus;
end;
function CreateNumFormatParams(ANumFormatStr: String;
const AFormatSettings: TFormatSettings): TsNumFormatParams;
function ParamsOfNumFormatStr(ANumFormatStr: String;
const AFormatSettings: TFormatSettings; var AResult: TsNumFormatParams): Integer;
implementation
uses
TypInfo, Math, LazUTF8, fpsutils, fpsCurrency;
function CreateNumFormatParams(ANumFormatStr: String;
const AFormatSettings: TFormatSettings): TsNumFormatParams;
begin
Result := TsNumFormatParams.Create;
ParamsOfNumFormatStr(ANumFormatStr, AFormatSettings, result);
end;
function ParamsOfNumFormatStr(ANumFormatStr: String;
const AFormatSettings: TFormatSettings; var AResult: TsNumFormatParams): Integer;
var
parser: TsNumFormatParser;
begin
Assert(AResult <> nil);
if ANumFormatstr = 'General' then ANumFormatStr := '';
parser := TsNumFormatParser.Create(ANumFormatStr, AFormatSettings);
try
Result := parser.Status;
AResult.Sections := parser.FSections;
finally
parser.Free;
end;
end;
{------------------------------------------------------------------------------}
{ TsNumFormatParser }
{------------------------------------------------------------------------------}
{@@ ----------------------------------------------------------------------------
Creates a number format parser for analyzing a formatstring that has been
read from a spreadsheet file.
If ALocalized is true then the formatstring contains localized decimal
separator etc.
-------------------------------------------------------------------------------}
constructor TsNumFormatParser.Create(const AFormatString: String;
const AFormatSettings: TFormatSettings);
begin
inherited Create;
FFormatSettings := AFormatSettings;
Parse(AFormatString);
CheckSections;
if AFormatString = '' then FSections[0].NumFormat := nfGeneral;
end;
destructor TsNumFormatParser.Destroy;
begin
FSections := nil;
inherited Destroy;
end;
procedure TsNumFormatParser.AddElement(AToken: TsNumFormatToken; AText: String);
var
n: Integer;
begin
n := Length(FSections[FCurrSection].Elements);
SetLength(FSections[FCurrSection].Elements, n+1);
FSections[FCurrSection].Elements[n].Token := AToken;
FSections[FCurrSection].Elements[n].TextValue := AText;
end;
procedure TsNumFormatParser.AddElement(AToken: TsNumFormatToken;
AIntValue: Integer=0; AText: String = '');
var
n: Integer;
begin
n := Length(FSections[FCurrSection].Elements);
SetLength(FSections[FCurrSection].Elements, n+1);
FSections[FCurrSection].Elements[n].Token := AToken;
FSections[FCurrSection].Elements[n].IntValue := AIntValue;
FSections[FCurrSection].Elements[n].TextValue := AText;
end;
procedure TsNumFormatParser.AddElement(AToken: TsNumFormatToken; AFloatValue: Double); overload;
var
n: Integer;
begin
n := Length(FSections[FCurrSection].Elements);
SetLength(FSections[FCurrSection].Elements, n+1);
FSections[FCurrSection].Elements[n].Token := AToken;
FSections[FCurrSection].Elements[n].FloatValue := AFloatValue;
end;
procedure TsNumFormatParser.AddSection;
begin
FCurrSection := Length(FSections);
SetLength(FSections, FCurrSection + 1);
with FSections[FCurrSection] do
SetLength(Elements, 0);
end;
procedure TsNumFormatParser.AnalyzeColor(AValue: String);
var
n: Integer;
begin
AValue := lowercase(AValue);
// Colors
if AValue = 'red' then
AddElement(nftColor, ord(scRed))
else
if AValue = 'black' then
AddElement(nftColor, ord(scBlack))
else
if AValue = 'blue' then
AddElement(nftColor, ord(scBlue))
else
if AValue = 'white' then
AddElement(nftColor, ord(scWhite))
else
if AValue = 'green' then
AddElement(nftColor, ord(scGreen))
else
if AValue = 'cyan' then
AddElement(nftColor, ord(scCyan))
else
if AValue = 'magenta' then
AddElement(nftColor, ord(scMagenta))
else
if copy(AValue, 1, 5) = 'color' then begin
AValue := copy(AValue, 6, Length(AValue));
if not TryStrToInt(trim(AValue), n) then begin
FStatus := psErrNoValidColorIndex;
exit;
end;
AddElement(nftColor, n);
end else
FStatus := psErrUnknownInfoInBrackets;
end;
function TsNumFormatParser.AnalyzeCurrency(const AValue: String): Boolean;
begin
if (FFormatSettings.CurrencyString = '') then
Result := false
else
Result := CurrencyRegistered(AValue);
end;
{ Creates a formatstring for all sections.
Note: this implementation is only valid for the fpc and Excel dialects of
format string. }
function TsNumFormatParser.BuildFormatString: String;
var
i: Integer;
begin
if Length(FSections) > 0 then begin
Result := BuildFormatStringFromSection(FSections[0]);
for i:=1 to High(FSections) do
Result := Result + ';' + BuildFormatStringFromSection(FSections[i]);
end;
end;
procedure TsNumFormatParser.CheckSections;
var
i: Integer;
begin
for i:=0 to High(FSections) do
CheckSection(i);
if (Length(FSections) > 1) and (FSections[1].NumFormat = nfCurrencyRed) then
for i:=0 to High(FSections) do
if FSections[i].NumFormat = nfCurrency then
FSections[i].NumFormat := nfCurrencyRed;
end;
procedure TsNumFormatParser.CheckSection(ASection: Integer);
var
el, i: Integer;
section: PsNumFormatSection;
nfs, nfsTest: String;
nf: TsNumberFormat;
formats: set of TsNumberFormat;
isMonthMinute: Boolean;
begin
if FStatus <> psOK then
exit;
section := @FSections[ASection];
section^.Kind := [];
if (ASection = 0) and (Length(FSections) = 1) and (Length(section^.Elements) = 1)
and (section^.Elements[0].Token = nftGeneral)
then begin
section^.NumFormat := nfGeneral;
exit;
end;
i := 0;
isMonthMinute := false;
for el := 0 to High(section^.Elements) do
case section^.Elements[el].Token of
nftZeroDecs:
section^.Decimals := section^.Elements[el].IntValue;
nftIntZeroDigit, nftIntOptDigit, nftIntSpaceDigit:
i := section^.Elements[el].IntValue;
nftFracNumSpaceDigit, nftFracNumZeroDigit:
section^.FracNumerator := section^.Elements[el].IntValue;
nftFracDenomSpaceDigit, nftFracDenomZeroDigit:
section^.FracDenominator := section^.Elements[el].IntValue;
nftFracDenom:
section^.FracDenominator := -section^.Elements[el].IntValue;
nftPercent:
section^.Kind := section^.Kind + [nfkPercent];
nftExpChar:
if (nfkExp in section^.Kind) then
FStatus := psErrMultipleExpChars
else
section^.Kind := section^.Kind + [nfkExp];
nftFactor:
if section^.Elements[el].IntValue <> 0 then
begin
section^.Elements[el].FloatValue := IntPower(10, -3*section^.Elements[el].IntValue);
section^.Factor := section^.Elements[el].FloatValue;
section^.Kind := section^.Kind + [nfkHasFactor];
end;
nftFracSymbol:
if (nfkFraction in section^.Kind) then
FStatus := psErrMultipleFracSymbols
else
begin
section^.Kind := section^.Kind + [nfkFraction];
section^.FracInt := i;
end;
nftCurrSymbol:
begin
if (nfkCurrency in section^.Kind) then
FStatus := psErrMultipleCurrSymbols
else begin
section^.Kind := section^.Kind + [nfkCurrency];
section^.CurrencySymbol := section^.Elements[el].TextValue;
end;
end;
nftYear, nftMonth, nftDay:
section^.Kind := section^.Kind + [nfkDate];
nftHour, nftMinute, nftSecond, nftMilliseconds:
begin
section^.Kind := section^.Kind + [nfkTime];
if section^.Elements[el].IntValue < 0 then
section^.Kind := section^.Kind + [nfkTimeInterval];
end;
nftMonthMinute:
isMonthMinute := true;
nftColor:
begin
section^.Kind := section^.Kind + [nfkHasColor];
section^.Color := section^.Elements[el].IntValue;
end;
nftIntTh:
section^.Kind := section^.Kind + [nfkHasThSep];
end;
if FStatus <> psOK then
exit;
if (section^.Kind * [nfkDate, nfkTime] <> []) and
(section^.Kind * [nfkPercent, nfkExp, nfkCurrency, nfkFraction] <> []) then
begin
FStatus := psErrNoValidDateTimeFormat;
exit;
end;
section^.NumFormat := nfCustom;
if (section^.Kind * [nfkDate, nfkTime] <> []) or isMonthMinute then
begin
FixMonthMinuteToken(section^);
nfs := GetFormatString;
if (nfkTimeInterval in section^.Kind) then
section^.NumFormat := nfTimeInterval
else
begin
formats := [nfShortDateTime, nfLongDate, nfShortDate, nfLongTime,
nfShortTime, nfLongTimeAM, nfShortTimeAM, nfDayMonth, nfMonthYear];
for nf in formats do
begin
nfsTest := BuildDateTimeFormatString(nf, FFormatSettings);
if Length(nfsTest) = Length(nfs) then
begin
if SameText(nfs, nfsTest) then
begin
section^.NumFormat := nf;
break;
end;
for i := 1 to Length(nfsTest) do
case nfsTest[i] of
'/': if not (nf in [nfLongTimeAM, nfShortTimeAM]) then
nfsTest[i] := FFormatSettings.DateSeparator;
':': nfsTest[i] := FFormatSettings.TimeSeparator;
'n': nfsTest[i] := 'm';
end;
if SameText(nfs, nfsTest) then
begin
section^.NumFormat := nf;
break;
end;
end;
end;
end;
end else
begin
nfs := GetFormatString;
nfsTest := BuildFractionFormatString(section^.FracInt > 0, section^.FracNumerator, section^.FracDenominator);
if sameText(nfs, nfsTest) then
section^.NumFormat := nfFraction
else
begin
formats := [nfFixed, nfFixedTh, nfPercentage, nfExp];
for nf in formats do begin
nfsTest := BuildNumberFormatString(nf, FFormatSettings, section^.Decimals);
if SameText(nfs, nfsTest) then
begin
section^.NumFormat := nf;
break;
end;
end;
end;
if (section^.NumFormat = nfCustom) and (nfkCurrency in section^.Kind) then
begin
section^.NumFormat := nfCurrency;
if section^.Color = scRed then
section^.NumFormat := nfCurrencyRed;
end;
{
el := 0;
while el < Length(section^.Elements) do
begin
if IsNumberAt(ASection, el, nf, decs, next) then begin
section^.Decimals := decs;
if nf = nfFixedTh then begin
if (nfkCurrency in section^.Kind) then
section^.NumFormat := nfCurrency
else
section^.NumFormat := nfFixedTh
end else
begin
section^.NumFormat := nf;
if (nfkPercent in section^.Kind) then
section^.NumFormat := nfPercentage
else
if (nfkExp in section^.Kind) then
section^.NumFormat := nfExp
else
if (nfkCurrency in section^.Kind) then
section^.NumFormat := nfCurrency
else
if (nfkFraction in section^.Kind) and (decs = 0) then begin
f1 := section^.Elements[el].IntValue; // int part or numerator
el := next;
while IsTokenAt(nftSpace, ASection, el) or IsTextAt(' ', ASection, el) do
inc(el);
if IsTokenAt(nftFracSymbol, ASection, el) then begin
inc(el);
while IsTokenAt(nftSpace, ASection, el) or IsTextAt(' ', aSection, el) do
inc(el);
if IsNumberAt(ASection, el, nf, decs, next) and (nf in [nfFixed, nfFraction]) and (decs = 0) then
begin
section^.FracInt := 0;
section^.FracNumerator := f1;
section^.FracDenominator := section^.Elements[el].IntValue;
section^.NumFormat := nfFraction;
end;
end else
if IsNumberAt(ASection, el, nf, decs, next) and (nf in [nfFixed, nfFraction]) and (decs = 0) then
begin
f2 := section^.Elements[el].IntValue;
el := next;
while IsTokenAt(nftSpace, ASection, el) or IsTextAt(' ', ASection, el) do
inc(el);
if IsTokenAt(nftFracSymbol, ASection, el) then
begin
inc(el);
while IsTokenAt(nftSpace, ASection, el) or IsTextAt(' ', ASection, el) do
inc(el);
if IsNumberAt(ASection, el, nf, decs, next) and (nf in [nfFixed, nfFraction]) and (decs=0) then
begin
section^.FracInt := f1;
section^.FracNumerator := f2;
section^.FracDenominator := section^.Elements[el].IntValue;
section^.NumFormat := nfFraction;
end;
end;
end;
end;
end;
break;
end else
if IsTokenAt(nftColor, ASection, el) then
section^.Color := section^.Elements[el].IntValue;
inc(el);
end;
if (section^.NumFormat = nfCurrency) and (section^.Color = scRed) then
section^.NumFormat := nfCurrencyRed;
}
end;
end;
procedure TsNumFormatParser.ClearAll;
var
i, j: Integer;
begin
for i:=0 to Length(FSections)-1 do begin
for j:=0 to Length(FSections[i].Elements) do
if FSections[i].Elements <> nil then
FSections[i].Elements[j].TextValue := '';
FSections[i].Elements := nil;
FSections[i].CurrencySymbol := '';
end;
FSections := nil;
end;
procedure TsNumFormatParser.DeleteElement(ASection, AIndex: Integer);
var
i, n: Integer;
begin
n := Length(FSections[ASection].Elements);
for i:= AIndex+1 to n-1 do
FSections[ASection].Elements[i-1] := FSections[ASection].Elements[i];
SetLength(FSections[ASection].Elements, n-1);
end;
{ Identify the ambiguous "m" token ("month" or "minute") }
procedure TsNumFormatParser.FixMonthMinuteToken(var ASection: TsNumFormatSection);
var
i, j: Integer;
// Finds the previous date/time element skipping spaces, date/time sep etc.
function PrevDateTimeElement(j: Integer): Integer;
begin
Result := -1;
dec(j);
while (j >= 0) do begin
with ASection.Elements[j] do
if Token in [nftYear, nftMonth, nftDay, nftHour, nftMinute, nftSecond] then
begin
Result := j;
exit;
end;
dec(j);
end;
end;
// Finds the next date/time element skipping spaces, date/time sep etc.
function NextDateTimeElement(j: Integer): Integer;
begin
Result := -1;
inc(j);
while (j < Length(ASection.Elements)) do begin
with ASection.Elements[j] do
if Token in [nftYear, nftMonth, nftDay, nftHour, nftMinute, nftSecond] then
begin
Result := j;
exit;
end;
inc(j);
end;
end;
begin
for i:=0 to High(ASection.Elements) do
begin
// Find index of nftMonthMinute token...
if ASection.Elements[i].Token = nftMonthMinute then begin
// ... and, using its neighbors, decide whether it is a month or a minute.
j := NextDateTimeElement(i);
if j <> -1 then
case ASection.Elements[j].Token of
nftDay, nftYear:
begin
ASection.Elements[i].Token := nftMonth;
Continue;
end;
nftSecond:
begin
ASection.Elements[i].Token := nftMinute;
Continue;
end;
end;
j := PrevDateTimeElement(i);
if j <> -1 then
case ASection.Elements[j].Token of
nftDay, nftYear:
begin
ASection.Elements[i].Token := nftMonth;
Continue;
end;
nftHour:
begin
ASection.Elements[i].Token := nftMinute;
Continue;
end;
end;
// If we get here the token is isolated. In this case we assume
// that it is a month - that's the way Excel does it when reading files
// (for editing of a worksheet, however, Excel distinguishes between
// uppercase "M" for "month" and lowercase "m" for "minute".)
ASection.Elements[i].Token := nftMonth;
Include(ASection.Kind, nfkDate);
end;
end;
end;
procedure TsNumFormatParser.InsertElement(ASection, AIndex: Integer;
AToken: TsNumFormatToken; AText: String);
var
i, n: Integer;
begin
n := Length(FSections[ASection].Elements);
SetLength(FSections[ASection].Elements, n+1);
for i:= n-1 downto AIndex+1 do
FSections[ASection].Elements[i+1] := FSections[ASection].Elements[i];
FSections[ASection].Elements[AIndex+1].Token := AToken;
FSections[ASection].Elements[AIndex+1].TextValue := AText;
end;
procedure TsNumFormatParser.InsertElement(ASection, AIndex: Integer;
AToken: TsNumFormatToken; AIntValue: Integer);
var
i, n: Integer;
begin
n := Length(FSections[ASection].Elements);
SetLength(FSections[ASection].Elements, n+1);
for i:= n-1 downto AIndex+1 do
FSections[ASection].Elements[i+1] := FSections[ASection].Elements[i];
FSections[ASection].Elements[AIndex+1].Token := AToken;
FSections[ASection].Elements[AIndex+1].IntValue := AIntValue;
end;
procedure TsNumFormatParser.InsertElement(ASection, AIndex: Integer;
AToken: TsNumFormatToken; AFloatValue: Double);
var
i, n: Integer;
begin
n := Length(FSections[ASection].Elements);
SetLength(FSections[ASection].Elements, n+1);
for i:= n-1 downto AIndex+1 do
FSections[ASection].Elements[i+1] := FSections[ASection].Elements[i];
FSections[ASection].Elements[AIndex+1].Token := AToken;
FSections[ASection].Elements[AIndex+1].FloatValue := AFloatValue;
end;
function TsNumFormatParser.GetFormatString: String;
begin
Result := BuildFormatString;
end;
{ Extracts the currency symbol form the formatting sections. It is assumed that
all two or three sections of the currency/accounting format use the same
currency symbol, otherwise it would be custom format anyway which ignores
the currencysymbol value. }
function TsNumFormatParser.GetCurrencySymbol: String;
begin
if Length(FSections) > 0 then
Result := FSections[0].CurrencySymbol
else
Result := '';
end;
{ Creates a string which summarizes the date/time formats in the given section.
The string contains a 'y' for a nftYear, a 'm' for a nftMonth, a
'd' for a nftDay, a 'h' for a nftHour, a 'n' for a nftMinute, a 's' for a
nftSeconds, and a 'z' for a nftMilliseconds token. The order is retained.
Needed for biff2 }
function TsNumFormatParser.GetDateTimeCode(ASection: Integer): String;
var
i: Integer;
begin
Result := '';
if ASection < Length(FSections) then
with FSections[ASection] do begin
i := 0;
while i < Length(Elements) do begin
case Elements[i].Token of
nftYear : Result := Result + 'y';
nftMonth : Result := Result + 'm';
nftDay : Result := Result + 'd';
nftHour : Result := Result + 'h';
nftMinute : Result := Result + 'n';
nftSecond : Result := Result + 's';
nftMilliSeconds: Result := Result + 'z';
end;
inc(i);
end;
end;
end;
{ Extracts the number of decimals from the sections. Since they are needed only
for default formats having only a single section, only the first section is
considered. In case of currency/accounting having two or three sections, it is
assumed that all sections have the same decimals count, otherwise it would not
be a standard format. }
function TsNumFormatParser.GetDecimals: Byte;
begin
if Length(FSections) > 0 then
Result := FSections[0].Decimals
else
Result := 0;
end;
function TsNumFormatParser.GetFracDenominator: Integer;
begin
if Length(FSections) > 0 then
Result := FSections[0].FracDenominator
else
Result := 0;
end;
function TsNumFormatParser.GetFracInt: Integer;
begin
if Length(FSections) > 0 then
Result := FSections[0].FracInt
else
Result := 0;
end;
function TsNumFormatParser.GetFracNumerator: Integer;
begin
if Length(FSections) > 0 then
Result := FSections[0].FracNumerator
else
Result := 0;
end;
{ Tries to extract a common builtin number format from the sections. If there
are multiple sections, it is always a custom format, except for Currency and
Accounting. }
function TsNumFormatParser.GetNumFormat: TsNumberFormat;
begin
if Length(FSections) = 0 then
result := nfGeneral
else begin
Result := FSections[0].NumFormat;
if (Result = nfCurrency) then begin
if Length(FSections) = 2 then begin
Result := FSections[1].NumFormat;
if FSections[1].CurrencySymbol <> FSections[0].CurrencySymbol then begin
Result := nfCustom;
exit;
end;
if (FSections[0].NumFormat in [nfCurrency, nfCurrencyRed]) and
(FSections[1].NumFormat in [nfCurrency, nfCurrencyRed])
then
exit;
end else
if Length(FSections) = 3 then begin
Result := FSections[1].NumFormat;
if (FSections[0].CurrencySymbol <> FSections[1].CurrencySymbol) or
(FSections[1].CurrencySymbol <> FSections[2].CurrencySymbol)
then begin
Result := nfCustom;
exit;
end;
if (FSections[0].NumFormat in [nfCurrency, nfCurrencyRed]) and
(FSections[1].NumFormat in [nfCurrency, nfCurrencyRed]) and
(FSections[2].NumFormat in [nfCurrency, nfCurrencyRed])
then
exit;
end;
Result := nfCustom;
exit;
end;
if Length(FSections) > 1 then
Result := nfCustom;
end;
end;
function TsNumFormatParser.GetParsedSectionCount: Integer;
begin
Result := Length(FSections);
end;
function TsNumFormatParser.GetParsedSections(AIndex: Integer): TsNumFormatSection;
begin
Result := FSections[AIndex];
end;
{
function TsNumFormatParser.GetTokenIntValueAt(AToken: TsNumFormatToken;
ASection, AIndex: Integer): Integer;
begin
if IsTokenAt(AToken, ASection, AIndex) then
Result := FSections[ASection].Elements[AIndex].IntValue
else
Result := -1;
end;
}
{ Returns true if the format elements contain at least one date/time token }
function TsNumFormatParser.IsDateTimeFormat: Boolean;
var
section: TsNumFormatSection;
begin
for section in FSections do
if section.Kind * [nfkDate, nfkTime] <> [] then
begin
Result := true;
exit;
end;
Result := false;
end;
{
function TsNumFormatParser.IsNumberAt(ASection, AIndex: Integer;
out ANumFormat: TsNumberFormat; out ADecimals: Byte;
out ANextIndex: Integer): Boolean;
var
token: TsNumFormatToken;
begin
if (ASection > High(FSections)) or (AIndex > High(FSections[ASection].Elements))
then begin
Result := false;
ANextIndex := AIndex;
exit;
end;
Result := true;
ANumFormat := nfCustom;
ADecimals := 0;
token := FSections[ASection].Elements[AIndex].Token;
if token in [nftFracNumOptDigit, nftFracNumZeroDigit, nftFracNumSpaceDigit,
nftFracDenomOptDigit, nftFracDenomZeroDigit, nftFracDenomSpaceDigit] then
begin
ANumFormat := nfFraction;
ANextIndex := AIndex + 1;
exit;
end;
if (token = nftIntTh) and (FSections[ASection].Elements[AIndex].IntValue = 1) then // '#,##0'
ANumFormat := nfFixedTh
else
if (token = nftIntZeroDigit) and (FSections[ASection].Elements[AIndex].IntValue = 1) then // '0'
ANumFormat := nfFixed;
if (token in [nftIntTh, nftIntZeroDigit, nftIntOptDigit, nftIntSpaceDigit]) then
begin
if IsTokenAt(nftDecSep, ASection, AIndex+1) then
begin
if AIndex + 2 < Length(FSections[ASection].Elements) then
begin
token := FSections[ASection].Elements[AIndex+2].Token;
if (token in [nftZeroDecs, nftOptDecs, nftSpaceDecs]) then
begin
ANextIndex := AIndex + 3;
ADecimals := FSections[ASection].Elements[AIndex+2].IntValue;
if (token <> nftZeroDecs) then
ANumFormat := nfCustom;
exit;
end;
end;
end else
if IsTokenAt(nftSpace, ASection, AIndex+1) then
begin
ANumFormat := nfFraction;
ANextIndex := AIndex + 1;
exit;
end else
begin
ANextIndex := AIndex + 1;
exit;
end;
end;
ANextIndex := AIndex;
Result := false;
end;
function TsNumFormatParser.IsTextAt(AText: String; ASection, AIndex: Integer): Boolean;
begin
Result := IsTokenAt(nftText, ASection, AIndex) and
(FSections[ASection].Elements[AIndex].TextValue = AText);
end;
}
{ Returns true if the format elements contain only time, no date tokens. }
function TsNumFormatParser.IsTimeFormat: Boolean;
var
section: TsNumFormatSection;
begin
for section in FSections do
if (nfkTime in section.Kind) then
begin
Result := true;
exit;
end;
Result := false;
end;
{
function TsNumFormatParser.IsTokenAt(AToken: TsNumFormatToken;
ASection, AIndex: Integer): Boolean;
begin
Result := (ASection < Length(FSections)) and
(AIndex < Length(FSections[ASection].Elements)) and
(FSections[ASection].Elements[AIndex].Token = AToken);
end;
}
{ Limits the decimals to 0 or 2, as required by Excel2. }
procedure TsNumFormatParser.LimitDecimals;
var
i, j: Integer;
begin
for j:=0 to High(FSections) do
for i:=0 to High(FSections[j].Elements) do
if FSections[j].Elements[i].Token = nftZeroDecs then
if FSections[j].Elements[i].IntValue > 0 then
FSections[j].Elements[i].IntValue := 2;
end;
function TsNumFormatParser.NextToken: Char;
begin
if FCurrent < FEnd then begin
inc(FCurrent);
Result := FCurrent^;
end else
Result := #0;
end;
function TsNumFormatParser.PrevToken: Char;
begin
if FCurrent > nil then begin
dec(FCurrent);
Result := FCurrent^;
end else
Result := #0;
end;
procedure TsNumFormatParser.Parse(const AFormatString: String);
begin
FStatus := psOK;
AddSection;
if (AFormatString = '') then
begin
AddElement(nftGeneral);
exit;
end;
FStart := @AFormatString[1];
FEnd := FStart + Length(AFormatString);
FCurrent := FStart;
FToken := FCurrent^;
while (FCurrent < FEnd) and (FStatus = psOK) do begin
case FToken of
'G','g': ScanGeneral;
'[': ScanBrackets;
'"': ScanQuotedText;
':': AddElement(nftDateTimeSep, ':');
';': AddSection;
else ScanFormat;
end;
FToken := NextToken;
end;
end;
{ Scans an AM/PM sequence (or AMPM or A/P).
At exit, cursor is a next character }
procedure TsNumFormatParser.ScanAMPM;
var
s: String;
el: Integer;
begin
s := '';
while (FCurrent < FEnd) do begin
if (FToken in ['A', 'a', 'P', 'p', 'm', 'M', '/']) then
s := s + FToken
else
break;
FToken := NextToken;
end;
if s <> '' then
begin
AddElement(nftAMPM, s);
// Tag the hour element for AM/PM format needed
el := High(FSections[FCurrSection].Elements)-1;
for el := High(FSections[FCurrSection].Elements)-1 downto 0 do
if FSections[FCurrSection].Elements[el].Token = nftHour then
begin
FSections[FCurrSection].Elements[el].TextValue := 'AM';
break;
end;
end;
end;
{ Counts the number of characters equal to ATestChar. Stops at the next
different character. This is also where the cursor is at exit. }
procedure TsNumFormatParser.ScanAndCount(ATestChar: Char; out ACount: Integer);
begin
ACount := 0;
if FToken <> ATestChar then
exit;
repeat
inc(ACount);
FToken := NextToken;
until (FToken <> ATestChar) or (FCurrent >= FEnd);
end;
{ Extracts the text between square brackets. This can be
- a time duration like [hh]
- a condition, like [>= 2.0]
- a currency symbol like [$-409]
- a color like [red] or [color25]
The procedure is left with the cursor at ']' }
procedure TsNumFormatParser.ScanBrackets;
var
s: String;
n: Integer;
prevtok: Char;
isText: Boolean;
begin
s := '';
isText := false;
FToken := NextToken; // Cursor was at '['
while (FCurrent < FEnd) and (FStatus = psOK) do begin
case FToken of
'h', 'H', 'm', 'M', 'n', 'N', 's', 'S':
if isText then
s := s + FToken
else
begin
prevtok := FToken;
ScanAndCount(FToken, n);
if (FToken in [']', #0]) then begin
case prevtok of
'h', 'H' : AddElement(nftHour, -n);
'm', 'M', 'n', 'N': AddElement(nftMinute, -n);
's', 'S' : AddElement(nftSecond, -n);
end;
break;
end else
FStatus := psErrUnknownInfoInBrackets;
end;
'<', '>', '=':
begin
ScanCondition(FToken);
if FToken = ']' then
break
else
FStatus := psErrUnknownInfoInBrackets;
end;
'$':
begin
ScanCurrSymbol;
if FToken = ']' then
break
else
FStatus := psErrUnknownInfoInBrackets;
end;
']':
begin
AnalyzeColor(s);
break;
end;
else
s := s + FToken;
isText := true;
end;
FToken := NextToken;
end;
end;
{ Scans a condition like [>=2.0]. Starts after the "[" and ends before at "]".
Returns first character after the number (spaces allowed). }
procedure TsNumFormatParser.ScanCondition(AFirstChar: Char);
var
s: String;
// op: TsCompareOperation;
value: Double;
res: Integer;
begin
s := AFirstChar;
FToken := NextToken;
if FToken in ['>', '<', '='] then s := s + FToken else FToken := PrevToken;
{
if s = '=' then op := coEqual else
if s = '<>' then op := coNotEqual else
if s = '<' then op := coLess else
if s = '>' then op := coGreater else
if s = '<=' then op := coLessEqual else
if s = '>=' then op := coGreaterEqual
else begin
FStatus := psErrUnknownInfoInBrackets;
FToken := #0;
exit;
end;
}
while (FToken = ' ') and (FCurrent < FEnd) do
FToken := NextToken;
if FCurrent >= FEnd then begin
FStatus := psErrUnknownInfoInBrackets;
FToken := #0;
exit;
end;
s := FToken;
while (FCurrent < FEnd) and (FToken in ['+', '-', '.', '0'..'9']) do begin
FToken := NextToken;
s := s + FToken;
end;
val(s, value, res);
if res <> 0 then begin
FStatus := psErrUnknownInfoInBrackets;
FToken := #0;
exit;
end;
while (FCurrent < FEnd) and (FToken = ' ') do
FToken := NextToken;
if FToken = ']' then
AddElement(nftCompareOp, value)
else begin
FStatus := psErrUnknownInfoInBrackets;
FToken := #0;
end;
end;
{ Scans to end of a symbol like [$EUR-409], starting after the $ and ending at
the "]".
After the "$" follows the currency symbol, after the "-" country information }
procedure TsNumFormatParser.ScanCurrSymbol;
var
s: String;
begin
s := '';
FToken := NextToken;
while (FCurrent < FEnd) and not (FToken in ['-', ']']) do begin
s := s + FToken;
FToken := NextToken;
end;
if s <> '' then
AddElement(nftCurrSymbol, s);
if FToken <> ']' then begin
FToken := NextToken;
while (FCurrent < FEnd) and (FToken <> ']') do begin
s := s + FToken;
FToken := NextToken;
end;
if s <> '' then
AddElement(nftCountry, s);
end;
end;
{ Scans a date/time format. Procedure is left with the cursor at the last char
of the date/time format. }
procedure TsNumFormatParser.ScanDateTime;
var
n: Integer;
token: Char;
begin
while (FCurrent < FEnd) and (FStatus = psOK) do begin
case FToken of
'\': // means that the next character is taken literally
begin
FToken := NextToken; // skip the "\"...
AddElement(nftEscaped, FToken);
FToken := NextToken;
end;
'Y', 'y':
begin
ScanAndCount(FToken, n);
AddElement(nftYear, n);
end;
'm', 'M', 'n', 'N':
begin
token := FToken;
ScanAndCount(FToken, n);
AddElement(nftMonthMinute, n, token); // Decide on minute or month later
end;
'D', 'd':
begin
ScanAndCount(FToken, n);
AddElement(nftDay, n);
end;
'H', 'h':
begin
ScanAndCount(FToken, n);
AddElement(nftHour, n);
end;
'S', 's':
begin
ScanAndCount(FToken, n);
AddElement(nftSecond, n);
end;
'/', ':':
begin
AddElement(nftDateTimeSep, FToken);
FToken := NextToken;
end;
'.':
begin
token := NextToken;
if token in ['z', '0'] then begin
AddElement(nftDecSep, FToken);
FToken := NextToken;
ScanAndCount(FToken, n);
AddElement(nftMilliseconds, n);
end else begin
AddElement(nftDateTimeSep, FToken);
FToken := token;
end;
end;
'[':
begin
ScanBrackets;
FToken := NextToken;
end;
'A', 'a':
ScanAMPM;
',', '-':
begin
AddElement(nftText, FToken);
FToken := NextToken;
end
else
// char pointer must be at end of date/time mask.
FToken := PrevToken;
Exit;
end;
end;
end;
procedure TsNumFormatParser.ScanFormat;
var
done: Boolean;
begin
done := false;
while (FCurrent < FEnd) and (FStatus = psOK) and (not done) do begin
case FToken of
'\': // Excel: add next character literally
begin
FToken := NextToken;
AddElement(nftText, FToken);
end;
'*': // Excel: repeat next character to fill cell. For accounting format.
begin
FToken := NextToken;
AddElement(nftRepeat, FToken);
end;
'_': // Excel: Leave width of next character empty
begin
FToken := NextToken;
AddElement(nftEmptyCharWidth, FToken);
end;
'@': // Excel: Indicates text format
begin
AddElement(nftTextFormat, FToken);
end;
'"':
ScanQuotedText;
'(', ')':
AddElement(nftSignBracket, FToken);
'0', '#', '?', '.', ',', '-':
ScanNumber;
'y', 'Y', 'm', 'M', 'd', 'D', 'h', 'N', 'n', 's':
ScanDateTime;
'[':
ScanBrackets;
' ':
AddElement(nftSpace, FToken);
'A', 'a':
begin
ScanAMPM;
FToken := PrevToken;
end;
'G', 'g':
ScanGeneral;
';': // End of the section. Important: Cursor must stay on ';'
begin
AddSection;
Exit;
end;
else
AddElement(nftText, FToken);
end;
FToken := NextToken;
end;
end;
{ Scans for the word "General", it may be used like other tokens }
procedure TsNumFormatParser.ScanGeneral;
begin
FStatus := psErrGeneralExpected;
FToken := NextToken;
if not (FToken in ['e', 'E']) then exit;
FToken := NextToken;
if not (FToken in ['n', 'N']) then exit;
FToken := NextToken;
if not (FToken in ['e', 'E']) then exit;
FToken := NextToken;
if not (FToken in ['r', 'R']) then exit;
FToken := NextToken;
if not (FToken in ['a', 'A']) then exit;
FToken := NextToken;
if not (FToken in ['l', 'L']) then exit;
AddElement(nftGeneral);
FStatus := psOK;
end;
{ Scans a floating point format. Procedure is left with the cursor at the last
character of the format. }
procedure TsNumFormatParser.ScanNumber;
var
hasDecSep: Boolean;
isFrac: Boolean;
n, m: Integer;
el: Integer;
savedCurrent: PChar;
thSep: Char;
begin
hasDecSep := false;
isFrac := false;
thSep := ',';
while (FCurrent < FEnd) and (FStatus = psOK) do begin
case FToken of
',': AddElement(nftThSep, ',');
'.': begin
AddElement(nftDecSep, '.');
hasDecSep := true;
end;
'#': begin
ScanAndCount('#', n);
savedCurrent := FCurrent;
if not (hasDecSep or isFrac) and (n = 1) and (FToken = thSep) then
begin
m := 0;
FToken := NextToken;
ScanAndCount('#', n);
case n of
0: begin
ScanAndCount('0', n);
ScanAndCount(thSep, m);
FToken := prevToken;
if n = 3 then
AddElement(nftIntTh, 3, ',')
else
FCurrent := savedCurrent;
end;
1: begin
ScanAndCount('0', n);
ScanAndCount(thSep, m);
FToken := prevToken;
if n = 2 then
AddElement(nftIntTh, 2, ',')
else
FCurrent := savedCurrent;
end;
2: begin
ScanAndCount('0', n);
ScanAndCount(thSep, m);
FToken := prevToken;
if (n = 1) then
AddElement(nftIntTh, 1, ',')
else
FCurrent := savedCurrent;
end;
end;
if m > 0 then
AddElement(nftFactor, m, thSep);
end else
begin
FToken := PrevToken;
if isFrac then
AddElement(nftFracDenomOptDigit, n)
else
if hasDecSep then
AddElement(nftOptDecs, n)
else
AddElement(nftIntOptDigit, n);
end;
end;
'0': begin
ScanAndCount('0', n);
ScanAndCount(thSep, m);
FToken := PrevToken;
if hasDecSep then
AddElement(nftZeroDecs, n)
else
if isFrac then
AddElement(nftFracDenomZeroDigit, n)
else
AddElement(nftIntZeroDigit, n);
if m > 0 then
AddElement(nftFactor, m, thSep);
end;
'1'..'9':
begin
if isFrac then
begin
n := 0;
while (FToken in ['1'..'9','0']) do
begin
n := n*10 + StrToInt(FToken);
FToken := nextToken;
end;
AddElement(nftFracDenom, n);
end else
AddElement(nftText, FToken);
end;
'?': begin
ScanAndCount('?', n);
FToken := PrevToken;
if hasDecSep then
AddElement(nftSpaceDecs, n)
else
if isFrac then
AddElement(nftFracDenomSpaceDigit, n)
else
AddElement(nftIntSpaceDigit, n);
end;
'E', 'e':
begin
AddElement(nftExpChar, FToken);
FToken := NextToken;
if FToken in ['+', '-'] then
AddElement(nftExpSign, FToken);
FToken := NextToken;
if FToken = '0' then begin
ScanAndCount('0', n);
FToken := PrevToken;
AddElement(nftExpDigits, n);
end;
end;
'+', '-':
AddElement(nftSign, FToken);
'%': AddElement(nftPercent, FToken);
'/': begin
isFrac := true;
AddElement(nftFracSymbol, FToken);
// go back and replace correct token for numerator
el := High(FSections[FCurrSection].Elements);
while el > 0 do begin
dec(el);
case FSections[FCurrSection].Elements[el].Token of
nftIntOptDigit:
begin
FSections[FCurrSection].Elements[el].Token := nftFracNumOptDigit;
break;
end;
nftIntSpaceDigit:
begin
FSections[FCurrSection].Elements[el].Token := nftFracNumSpaceDigit;
break;
end;
nftIntZeroDigit:
begin
FSections[FCurrSection].Elements[el].Token := nftFracNumZeroDigit;
break;
end;
end;
end;
end;
'G', 'g':
ScanGeneral;
else
FToken := PrevToken;
Exit;
end;
FToken := NextToken;
end;
end;
{ Scans a text in quotation marks. Tries to interpret the text as a currency
symbol (--> AnalyzeText).
The procedure is entered and left with the cursor at a quotation mark. }
procedure TsNumFormatParser.ScanQuotedText;
var
s: String;
begin
s := '';
FToken := NextToken; // Cursor war at '"'
while (FCurrent < FEnd) and (FStatus = psOK) do begin
if FToken = '"' then begin
if AnalyzeCurrency(s) then
AddElement(nftCurrSymbol, s)
else
AddElement(nftText, s);
exit;
end else begin
s := s + FToken;
FToken := NextToken;
end;
end;
// When the procedure gets here the final quotation mark is missing
FStatus := psErrQuoteExpected;
end;
procedure TsNumFormatParser.SetDecimals(AValue: Byte);
var
i, j, n: Integer;
foundDecs: Boolean;
begin
foundDecs := false;
for j := 0 to High(FSections) do begin
n := Length(FSections[j].Elements);
i := n-1;
while (i > -1) do begin
case FSections[j].Elements[i].Token of
nftDecSep: // this happens, e.g., for "0.E+00"
if (AValue > 0) and not foundDecs then begin
InsertElement(j, i, nftZeroDecs, AValue);
break;
end;
nftIntOptDigit, nftIntZeroDigit, nftIntSpaceDigit, nftIntTh:
// no decimals so far --> add decimal separator and decimals element
if (AValue > 0) then begin
// Don't use "AddElements" because nfCurrency etc have elements after the number.
InsertElement(j, i, nftDecSep, '.');
InsertElement(j, i+1, nftZeroDecs, AValue);
break;
end;
nftZeroDecs, nftOptDecs, nftSpaceDecs:
begin
foundDecs := true;
if AValue > 0 then begin
// decimals are already used, just replace value of decimal places
FSections[j].Elements[i].IntValue := AValue;
FSections[j].Elements[i].Token := nftZeroDecs;
break;
end else begin
// No decimals any more: delete decs and decsep elements
DeleteElement(j, i);
DeleteElement(j, i-1);
break;
end;
end;
end;
dec(i);
end;
end;
end;
end.