lazarus-ccr/components/fpspreadsheet/source/common/fpsnumformat.pas

4013 lines
136 KiB
ObjectPascal

{@@ ----------------------------------------------------------------------------
Unit fpsNumFormat contains classes and procedures to analyze and process
<b>number formats</b>.
AUTHORS: Werner Pamler
LICENSE: See the file COPYING.modifiedLGPL.txt, included in the Lazarus
distribution, for details about the license.
-------------------------------------------------------------------------------}
unit fpsNumFormat;
{$ifdef fpc}
{$mode objfpc}{$H+}
{$endif}
interface
uses
Classes, SysUtils,
fpstypes;
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;
psErrNoValidTextFormat = 14;
type
{@@ Set of characters }
TsDecsChars = set of char;
{@@ Tokens used by the elements of the number format parser. If, e.g. a
format string is "0.000" then the number format parser detects the following
three tokens
- nftIntZeroDigit with integer value 1 (i.e. 1 zero-digit for the integer part)
- nftDecSep (i.e. decimal separator)
- ntZeroDecs with integer value 2 (i.e. 3 decimal places. }
TsNumFormatToken = (
nftGeneral, // token for "general" number format
nftText, // must be quoted, stored in TextValue
nftThSep, // ',', replaced by FormatSettings.ThousandSeparator
nftDecSep, // '.', replaced by FormatSettings.DecimalSeparator
nftYear, // 'y' or 'Y', count stored in IntValue
nftMonth, // 'm' or 'M', count stored in IntValue
nftDay, // 'd' or 'D', count stored in IntValue
nftHour, // 'h' or 'H', count stored in IntValue
nftMinute, // 'n' or 'N' (or 'm'/'M'), count stored in IntValue
nftSecond, // 's' or 'S', count stored in IntValue
nftMilliseconds, // 'z', 'Z', '0', count stored in IntValue
nftAMPM, //
nftMonthMinute, // 'm'/'M' or 'n'/'N', meaning depending on context
nftDateTimeSep, // '/' or ':', replaced by value from FormatSettings, stored in TextValue
nftSign, // '+' or '-', stored in TextValue
nftSignBracket, // '(' or ')' for negative values, stored in TextValue
nftIntOptDigit, // '#', count stored in IntValue
nftIntZeroDigit, // '0', count stored in IntValue
nftIntSpaceDigit, // '?', count stored in IntValue
nftIntTh, // '#,##0' sequence for nfFixed, count of 0 stored in IntValue
nftZeroDecs, // '0' after dec sep, count stored in IntValue
nftOptDecs, // '#' after dec sep, count stored in IntValue
nftSpaceDecs, // '?' after dec sep, count stored in IntValue
nftExpChar, // 'e' or 'E', stored in TextValue
nftExpSign, // '+' or '-' in exponent
nftExpDigits, // '0' digits in exponent, count stored in IntValue
nftPercent, // '%' percent symbol
nftFactor, // thousand separators at end of format string, each one divides value by 1000
nftFracSymbol, // '/' fraction symbol
nftFracNumOptDigit, // '#' in numerator, count stored in IntValue
nftFracNumSpaceDigit, // '?' in numerator, count stored in IntValue
nftFracNumZeroDigit, // '0' in numerator, count stored in IntValue
nftFracDenomOptDigit, // '#' in denominator, count stored in IntValue
nftFracDenomSpaceDigit,// '?' in denominator, count stored in IntValue
nftFracDenomZeroDigit, // '0' in denominator, count stored in IntValue
nftFracDenom, // specified denominator, value stored in IntValue
nftCurrSymbol, // e.g., '"€"' or '[$€]', stored in TextValue
nftCountry,
nftColor, // e.g., '[red]', Color in IntValue
nftCompareOp,
nftCompareValue,
nftSpace,
nftEscaped, // '\'
nftRepeat,
nftEmptyCharWidth,
nftTextFormat // '@'
);
{@@ Element of the parsed number format sequence. Each element is identified
by a token and has optional parameters stored as integer, float, and/or text. }
TsNumFormatElement = record
{@@ Token identifying the number format element }
Token: TsNumFormatToken;
{@@ Integer value associated with the number format element }
IntValue: Integer;
{@@ Floating point value associated with the number format element }
FloatValue: Double;
{@@ String value associated with the number format element }
TextValue: String;
end;
{@@ Array of parsed number format elements }
TsNumFormatElements = array of TsNumFormatElement;
{@@ Summary information classifying a number format section }
TsNumFormatKind = (nfkPercent, nfkExp, nfkCurrency, nfkFraction,
nfkDate, nfkTime, nfkTimeInterval, nfkText,
nfkHasColor, nfkHasThSep, nfkHasFactor);
{@@ Set of summary elements classifying and describing a number format section }
TsNumFormatKinds = set of TsNumFormatKind;
{@@ Number format string can be composed of several parts separated by a
semicolon. The number format parser extracts the format information into
individual sections for each part }
TsNumFormatSection = record
{@@ Parser number format elements used by this section }
Elements: TsNumFormatElements;
{@@ Summary information describing the section }
Kind: TsNumFormatKinds;
{@@ Reconstructed number format identifier for the built-in fps formats }
NumFormat: TsNumberFormat;
{@@ Number of decimal places used by the format string }
Decimals: Byte;
{@@ Factor by which a number will be multiplied before converting to string }
Factor: Double;
{@@ Digits to be used for the integer part of a fraction }
FracInt: Integer;
{@@ Digits to be used for the numerator part of a fraction }
FracNumerator: Integer;
{@@ Digits to be used for the denominator part of a fraction }
FracDenominator: Integer;
{@@ Currency string to be used in case of currency/accounting formats }
CurrencySymbol: String;
{@@ Color to be used when displaying the converted string }
Color: TsColor;
end;
{@@ Pointer to a parsed number format section }
PsNumFormatSection = ^TsNumFormatSection;
{@@ Array of parsed number format sections }
TsNumFormatSections = array of TsNumFormatSection;
{ TsNumFormatParams }
{@@ Describes a parsed number format and provides all the information to
convert a number to a number or date/time string. These data are created
by the number format parser from a format string. }
TsNumFormatParams = class(TObject)
private
protected
function GetNumFormat: TsNumberFormat; virtual;
function GetNumFormatStr: String; virtual;
public
{@@ Array of the format sections }
Sections: TsNumFormatSections;
procedure DeleteElement(ASectionIndex, AElementIndex: Integer);
procedure InsertElement(ASectionIndex, AElementIndex: Integer;
AToken: TsNumFormatToken);
function SectionsEqualTo(ASections: TsNumFormatSections): Boolean;
procedure SetCurrSymbol(AValue: String);
procedure SetDecimals(AValue: Byte);
procedure SetNegativeRed(AEnable: Boolean);
procedure SetThousandSep(AEnable: Boolean);
property NumFormat: TsNumberFormat read GetNumFormat;
property NumFormatStr: String read GetNumFormatStr;
end;
{ TsNumFormatList }
{@@ Class of number format parameters }
TsNumFormatParamsClass = class of TsNumFormatParams;
{@@ List containing parsed number format parameters }
TsNumFormatList = class(TFPList)
private
FOwnsData: Boolean;
function GetItem(AIndex: Integer): TsNumFormatParams;
procedure SetItem(AIndex: Integer; const AValue: TsNumFormatParams);
protected
FFormatSettings: TFormatSettings;
FClass: TsNumFormatParamsClass;
procedure AddBuiltinFormats; virtual;
public
constructor Create(AFormatSettings: TFormatSettings; AOwnsData: Boolean);
destructor Destroy; override;
function AddFormat(ASections: TsNumFormatSections): Integer; overload;
function AddFormat(AFormatStr: String): Integer; overload;
procedure Clear;
procedure Delete(AIndex: Integer);
function Find(ASections: TsNumFormatSections): Integer; overload;
function Find(AFormatstr: String): Integer; overload;
property Items[AIndex: Integer]: TsNumFormatParams read GetItem write SetItem; default;
end;
{ 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;
{ Utility functions }
function AddAMPM(const ATimeFormatString: String;
const AFormatSettings: TFormatSettings): String;
function AddIntervalBrackets(AFormatString: String): String;
procedure BuildCurrencyFormatList(AList: TStrings;
APositive: Boolean; AValue: Double; const ACurrencySymbol: String);
function BuildCurrencyFormatString(ANumberFormat: TsNumberFormat;
const AFormatSettings: TFormatSettings; ADecimals, APosCurrFmt, ANegCurrFmt: Integer;
ACurrencySymbol: String; Accounting: Boolean = false): String;
function BuildDateTimeFormatString(ANumberFormat: TsNumberFormat;
const AFormatSettings: TFormatSettings; AFormatString: String = ''): String;
function BuildFractionFormatString(AMixedFraction: Boolean;
ANumeratorDigits, ADenominatorDigits: Integer): String;
function BuildNumberFormatString(ANumberFormat: TsNumberFormat;
const AFormatSettings: TFormatSettings; ADecimals: Integer = -1): String;
function BuildFormatStringFromSection(const ASection: TsNumFormatSection): String;
function ApplyTextFormat(AText: String; AParams: TsNumFormatParams): String;
function ConvertFloatToStr(AValue: Double; AParams: TsNumFormatParams;
AFormatSettings: TFormatSettings): String;
function CountDecs(AFormatString: String; ADecChars: TsDecsChars = ['0']): Byte;
function GeneralFormatFloat(AValue: Double;
AFormatSettings: TFormatSettings): String; inline;
function IsBoolValue(const AText, ATrueText, AFalseText: String;
out AValue: Boolean): Boolean;
function IsCurrencyFormat(AFormat: TsNumberFormat): Boolean; overload;
function IsCurrencyFormat(ANumFormat: TsNumFormatParams): Boolean; overload;
function IsDateTimeFormat(AFormat: TsNumberFormat): Boolean; overload;
function IsDateTimeFormat(AFormatStr: String): Boolean; overload;
function IsDateTimeFormat(ANumFormat: TsNumFormatParams): Boolean; overload;
function IsDateTimeValue(AText: String; const AFormatSettings: TFormatSettings;
out ADateTime: TDateTime; out ANumFormat: TsNumberFormat): Boolean;
function IsDateFormat(ANumFormat: TsNumFormatParams): Boolean;
function IsTimeFormat(AFormat: TsNumberFormat): Boolean; overload;
function IsTimeFormat(AFormatStr: String): Boolean; overload;
function IsTimeFormat(ANumFormat: TsNumFormatParams): Boolean; overload;
function IsLongTimeFormat(AFormatStr: String; ATimeSeparator: char): Boolean; overload;
function IsNumberValue(AText: String; AutoDetectNumberFormat: Boolean;
const AFormatSettings: TFormatSettings; out ANumber: Double;
out ANumFormat: TsNumberFormat; out ADecimals: Integer;
out ACurrencySymbol, AWarning: String): Boolean;
function IsTimeIntervalFormat(ANumFormat: TsNumFormatParams): Boolean;
function IsTextFormat(ANumFormat: TsNumFormatParams): Boolean;
function MakeLongDateFormat(ADateFormat: String): String;
function MakeShortDateFormat(ADateFormat: String): String;
procedure MakeTimeIntervalMask(Src: String; var Dest: String);
function StripAMPM(const ATimeFormatString: String): String;
procedure InitFormatSettings(out AFormatSettings: TFormatSettings);
procedure ReplaceFormatSettings(var AFormatSettings: TFormatSettings;
const ADefaultFormats: TFormatSettings);
function CreateNumFormatParams(ANumFormatStr: String;
const AFormatSettings: TFormatSettings): TsNumFormatParams;
function ParamsOfNumFormatStr(ANumFormatStr: String;
const AFormatSettings: TFormatSettings; var AResult: TsNumFormatParams): Integer;
implementation
uses
StrUtils, Math, LazUTF8,
fpsUtils, fpsCurrency;
const
{@@ Array of format strings identifying the order of number and
currency symbol of a positive currency value. The number is expected at
index 0, the currency symbol at index 1 of the parameter array used by the
fpc Format() function. }
POS_CURR_FMT: array[0..3] of string = (
('%1:s%0:s'), // 0: $1
('%0:s%1:s'), // 1: 1$
('%1:s %0:s'), // 2: $ 1
('%0:s %1:s') // 3: 1 $
);
{@@ Array of format strings identifying the order of number and
currency symbol of a negative currency value. The sign is shown
as a dash character ("-") or by means of brackets. The number
is expected at index 0, the currency symbol at index 1 of the
parameter array for the fpc Format() function. }
NEG_CURR_FMT: array[0..15] of string = (
('(%1:s%0:s)'), // 0: ($1)
('-%1:s%0:s'), // 1: -$1
('%1:s-%0:s'), // 2: $-1
('%1:s%0:s-'), // 3: $1-
('(%0:s%1:s)'), // 4: (1$)
('-%0:s%1:s'), // 5: -1$
('%0:s-%1:s'), // 6: 1-$
('%0:s%1:s-'), // 7: 1$-
('-%0:s %1:s'), // 8: -1 $
('-%1:s %0:s'), // 9: -$ 1
('%0:s %1:s-'), // 10: 1 $-
('%1:s %0:s-'), // 11: $ 1-
('%1:s -%0:s'), // 12: $ -1
('%0:s- %1:s'), // 13: 1- $
('(%1:s %0:s)'), // 14: ($ 1)
('(%0:s %1:s)') // 15: (1 $)
);
{==============================================================================}
{ Float-to-string conversion }
{==============================================================================}
type
{@@ Set of parsed number format tokens }
TsNumFormatTokenSet = set of TsNumFormatToken;
const
{@@ Set of tokens which terminate number information in a format string }
TERMINATING_TOKENS: TsNumFormatTokenSet =
[nftSpace, nftText, nftEscaped, nftPercent, nftCurrSymbol, nftSign, nftSignBracket];
{@@ Set of tokens which describe the integer part of a number format }
INT_TOKENS: TsNumFormatTokenSet =
[nftIntOptDigit, nftIntZeroDigit, nftIntSpaceDigit];
{@@ Set of tokens which describe the decimals of a number format }
DECS_TOKENS: TsNumFormatTokenSet =
[nftZeroDecs, nftOptDecs, nftSpaceDecs];
{@@ Set of tokens which describe the numerator of a fraction format }
FRACNUM_TOKENS: TsNumFormatTokenSet =
[nftFracNumOptDigit, nftFracNumZeroDigit, nftFracNumSpaceDigit];
{@@ Set of tokens which describe the denominator of a fraction format }
FRACDENOM_TOKENS: TsNumFormatTokenSet =
[nftFracDenomOptDigit, nftFracDenomZeroDigit, nftFracDenomSpaceDigit, nftFracDenom];
{@@ Set of tokens which describe the exponent in exponential formatting of a number }
EXP_TOKENS: TsNumFormatTokenSet =
[nftExpDigits]; // todo: expand by optional digits (0.00E+#)
{ Helper function which checks whether a sequence of format tokens for
exponential formatting begins at the specified index in the format elements }
function CheckExp(const AElements: TsNumFormatElements; AIndex: Integer): Boolean;
var
numEl: Integer;
i: Integer;
begin
numEl := Length(AElements);
Result := (AIndex < numEl) and (AElements[AIndex].Token in INT_TOKENS);
if not Result then
exit;
numEl := Length(AElements);
i := AIndex + 1;
while (i < numEl) and (AElements[i].Token in INT_TOKENS) do inc(i);
// no decimal places
if (i+2 < numEl) and
(AElements[i].Token = nftExpChar) and
(AElements[i+1].Token = nftExpSign) and
(AElements[i+2].Token in EXP_TOKENS)
then begin
Result := true;
exit;
end;
// with decimal places
if (i < numEl) and (AElements[i].Token = nftDecSep) //and (AElements[i+1].Token in DECS_TOKENS)
then begin
inc(i);
while (i < numEl) and (AElements[i].Token in DECS_TOKENS) do inc(i);
if (i + 2 < numEl) and
(AElements[i].Token = nftExpChar) and
(AElements[i+1].Token = nftExpSign) and
(AElements[i+2].Token in EXP_TOKENS)
then begin
Result := true;
exit;
end;
end;
Result := false;
end;
{ Helper function which checks whether a sequence of format tokens for
fraction formatting begins at the specified index in the format elements }
function CheckFraction(const AElements: TsNumFormatElements; AIndex: Integer;
out digits: Integer): Boolean;
var
numEl: Integer;
i: Integer;
begin
digits := 0;
numEl := Length(AElements);
Result := (AIndex < numEl);
if not Result then
exit;
i := AIndex;
// Check for mixed fraction (integer split off, sample format "# ??/??"
if (AElements[i].Token in (INT_TOKENS + [nftIntTh])) then
begin
inc(i);
while (i < numEl) and (AElements[i].Token in (INT_TOKENS + [nftIntTh])) do inc(i);
while (i < numEl) and (AElements[i].Token in TERMINATING_TOKENS) do inc(i);
end;
if (i = numEl) or not (AElements[i].Token in FRACNUM_TOKENS) then
exit(false);
// Here follows the ordinary fraction (no integer split off); sample format "??/??"
while (i < numEl) and (AElements[i].Token in FRACNUM_TOKENS) do inc(i);
while (i < numEl) and (AElements[i].Token in TERMINATING_TOKENS) do inc(i);
if (i = numEl) or (AElements[i].Token <> nftFracSymbol) then
exit(False);
inc(i);
while (i < numEl) and (AElements[i].Token in TERMINATING_TOKENS) do inc(i);
if (i = numEl) or (not (AElements[i].Token in FRACDENOM_TOKENS)) then
exit(false);
while (i < numEL) and (AElements[i].Token in FRACDENOM_TOKENS) do
begin
case AElements[i].Token of
nftFracDenomZeroDigit : inc(digits, AElements[i].IntValue);
nftFracDenomSpaceDigit: inc(digits, AElements[i].IntValue);
nftFracDenomOptDigit : inc(digits, AElements[i].IntValue);
nftFracDenom : digits := -AElements[i].IntValue; // "-" indicates a literal denominator value!
end;
inc(i);
end;
Result := true;
end;
{ Processes a sequence of #, 0, and ? tokens.
Adds leading (GrowRight=false) or trailing (GrowRight=true) zeros and/or
spaces as specified by the format elements to the number value string.
On exit AIndex points to the first non-integer token. }
function ProcessIntegerFormat(AValue: String; AFormatSettings: TFormatSettings;
const AElements: TsNumFormatElements; var AIndex: Integer;
ATokens: TsNumFormatTokenSet; GrowRight, UseThSep: Boolean): String;
const
OptTokens = [nftIntOptDigit, nftFracNumOptDigit, nftFracDenomOptDigit, nftOptDecs];
ZeroTokens = [nftIntZeroDigit, nftFracNumZeroDigit, nftFracDenomZeroDigit, nftZeroDecs, nftIntTh];
SpaceTokens = [nftIntSpaceDigit, nftFracNumSpaceDigit, nftFracDenomSpaceDigit, nftSpaceDecs];
AllOptTokens = OptTokens + SpaceTokens;
var
fs: TFormatSettings absolute AFormatSettings;
i, j, L: Integer;
numEl: Integer;
begin
Result := AValue;
numEl := Length(AElements);
if GrowRight then
begin
// This branch is intended for decimal places, i.e. there may be trailing zeros.
i := AIndex;
if (AValue = '0') and (AElements[i].Token in AllOptTokens) then
Result := '';
// Remove trailing zeros
while (Result <> '') and (Result[Length(Result)] = '0')
do Delete(Result, Length(Result), 1);
// Add trailing zeros or spaces as required by the elements.
i := AIndex;
L := 0;
while (i < numEl) and (AElements[i].Token in ATokens) do
begin
if AElements[i].Token in ZeroTokens then
begin
inc(L, AElements[i].IntValue);
while Length(Result) < L do Result := Result + '0'
end else
if AElements[i].Token in SpaceTokens then
begin
inc(L, AElements[i].IntValue);
while Length(Result) < L do Result := Result + ' ';
end;
inc(i);
end;
if UseThSep then begin
j := 2;
while (j < Length(Result)) and (Result[j-1] <> ' ') and (Result[j] <> ' ') do
begin
Insert(fs.ThousandSeparator, Result, 1);
inc(j, 3);
end;
end;
AIndex := i;
end else
begin
// This branch is intended for digits (or integer and numerator parts of fractions)
// --> There are no leading zeros.
// Find last digit token of the sequence
i := AIndex;
while (i < numEl) and (AElements[i].Token in ATokens) do
inc(i);
j := i;
if i > 0 then dec(i);
if (AValue = '0') and (AElements[i].Token in AllOptTokens) and (i = AIndex) then
Result := '';
// From the end of the sequence, going backward, add leading zeros or spaces
// as required by the elements of the format.
L := 0;
while (i >= AIndex) do begin
if AElements[i].Token in ZeroTokens then
begin
inc(L, AElements[i].IntValue);
while Length(Result) < L do Result := '0' + Result;
end else
if AElements[i].Token in SpaceTokens then
begin
inc(L, AElements[i].IntValue);
while Length(Result) < L do Result := ' ' + Result;
end;
dec(i);
end;
AIndex := j;
if UseThSep then
begin
// AIndex := j + 1;
j := Length(Result) - 2;
while (j > 1) and (Result[j-1] <> ' ') and (Result[j] <> ' ') do
begin
Insert(fs.ThousandSeparator, Result, j);
dec(j, 3);
end;
end;
end;
end;
{ Converts the floating point number to an exponential number string according
to the format specification in AElements.
It must have been verified before, that the elements in fact are valid for
an exponential format. }
function ProcessExpFormat(AValue: Double; AFormatSettings: TFormatSettings;
const AElements: TsNumFormatElements; var AIndex: Integer): String;
var
fs: TFormatSettings absolute AFormatSettings;
expchar: String;
expSign: String;
se, si, sd: String;
decs, expDigits: Integer;
intZeroDigits, intOptDigits, intSpaceDigits: Integer;
numStr: String;
i, id, p: Integer;
numEl: Integer;
begin
Result := '';
numEl := Length(AElements);
// Determine digits of integer part of mantissa
intZeroDigits := 0;
intOptDigits := 0;
intSpaceDigits := 0;
i := AIndex;
while (AElements[i].Token in INT_TOKENS) do begin
case AElements[i].Token of
nftIntZeroDigit : inc(intZeroDigits, AElements[i].IntValue);
nftIntSpaceDigit: inc(intSpaceDigits, AElements[i].IntValue);
nftIntOptDigit : inc(intOptDigits, AElements[i].IntValue);
end;
inc(i);
end;
// No decimal places
if (i + 2 < numEl) and (AElements[i].Token = nftExpChar) then
begin
expChar := AElements[i].TextValue;
expSign := AElements[i+1].TextValue;
expDigits := 0;
i := i+2;
while (i < numEl) and (AElements[i].Token in EXP_TOKENS) do
begin
inc(expDigits, AElements[i].IntValue); // not exactly what Excel does... Rather exotic case...
inc(i);
end;
numstr := FormatFloat('0'+expChar+expSign+DupeString('0', expDigits), AValue, fs);
p := pos('e', Lowercase(numStr));
se := copy(numStr, p, Length(numStr)); // exp part of the number string, incl "E"
numStr := copy(numstr, 1, p-1); // mantissa of the number string
numStr := ProcessIntegerFormat(numStr, fs, AElements, AIndex, INT_TOKENS, false, false);
Result := numStr + se;
AIndex := i;
end
else
// With decimal places
if (i + 1 < numEl) and (AElements[i].Token = nftDecSep) then
begin
inc(i);
id := i; // index of decimal elements
decs := 0;
while (i < numEl) and (AElements[i].Token in DECS_TOKENS) do
begin
case AElements[i].Token of
nftZeroDecs,
nftSpaceDecs: inc(decs, AElements[i].IntValue);
end;
inc(i);
end;
expChar := AElements[i].TextValue;
expSign := AElements[i+1].TextValue;
expDigits := 0;
inc(i, 2);
while (i < numEl) and (AElements[i].Token in EXP_TOKENS) do
begin
inc(expDigits, AElements[i].IntValue);
inc(i);
end;
if decs=0 then
numstr := FormatFloat('0'+expChar+expSign+DupeString('0', expDigits), AValue, fs)
else
numStr := FloatToStrF(AValue, ffExponent, decs+1, expDigits, fs);
if (abs(AValue) >= 1.0) and (expSign = '-') then
Delete(numStr, pos('+', numStr), 1);
p := pos('e', Lowercase(numStr));
se := copy(numStr, p, Length(numStr)); // exp part of the number string, incl "E"
numStr := copy(numStr, 1, p-1); // mantissa of the number string
p := pos(fs.DecimalSeparator, numStr);
if p = 0 then
begin
si := numstr;
sd := '';
end else
begin
si := ProcessIntegerFormat(copy(numStr, 1, p-1), fs, AElements, AIndex, INT_TOKENS, false, false); // integer part of the mantissa
sd := ProcessIntegerFormat(copy(numStr, p+1, Length(numStr)), fs, AElements, id, DECS_TOKENS, true, false); // fractional part of the mantissa
end;
// Put all parts together...
Result := si + fs.DecimalSeparator + sd + se;
AIndex := i;
end;
end;
function ProcessFracFormat(AValue: Double; const AFormatSettings: TFormatSettings;
ADigits: Integer; const AElements: TsNumFormatElements;
var AIndex: Integer): String;
var
fs: TFormatSettings absolute AFormatSettings;
frint, frnum, frdenom, maxdenom: Int64;
sfrint, sfrnum, sfrdenom: String;
sfrsym, sintnumspace, snumsymspace, ssymdenomspace: String;
i, numEl: Integer;
begin
sintnumspace := '';
snumsymspace := '';
ssymdenomspace := '';
sfrsym := '/';
if ADigits >= 0 then
maxDenom := Round(IntPower(10, ADigits));
numEl := Length(AElements);
i := AIndex;
if AElements[i].Token in (INT_TOKENS + [nftIntTh]) then begin
// Split-off integer
if (AValue >= 1) then
begin
frint := trunc(AValue);
AValue := frac(AValue);
end else
frint := 0;
if ADigits >= 0 then
FloatToFraction(AValue, maxdenom, frnum, frdenom)
else
begin
frdenom := -ADigits;
frnum := round(AValue*frdenom);
end;
sfrint := ProcessIntegerFormat(IntToStr(frint), fs, AElements, i,
INT_TOKENS + [nftIntTh], false, (AElements[i].Token = nftIntTh));
while (i < numEl) and (AElements[i].Token in TERMINATING_TOKENS) do
begin
sintnumspace := sintnumspace + AElements[i].TextValue;
inc(i);
end;
end else
begin
// "normal" fraction
sfrint := '';
if ADigits > 0 then
FloatToFraction(AValue, maxdenom, frnum, frdenom)
else
begin
frdenom := -ADigits;
frnum := round(AValue*frdenom);
end;
sintnumspace := '';
end;
// numerator and denominator
sfrnum := ProcessIntegerFormat(IntToStr(frnum), fs, AElements, i,
FRACNUM_TOKENS, false, false);
while (i < numEl) and (AElements[i].Token in TERMINATING_TOKENS) do
begin
snumsymspace := snumsymspace + AElements[i].TextValue;
inc(i);
end;
inc(i); // fraction symbol
while (i < numEl) and (AElements[i].Token in TERMINATING_TOKENS) do
begin
ssymdenomspace := ssymdenomspace + AElements[i].TextValue;
inc(i);
end;
sfrdenom := ProcessIntegerFormat(IntToStr(frdenom), fs, AElements, i,
FRACDENOM_TOKENS, false, false);
AIndex := i+1;
// Special cases
if (frnum = 0) then
begin
if sfrnum = '' then begin
sintnumspace := '';
snumsymspace := '';
ssymdenomspace := '';
sfrdenom := '';
sfrsym := '';
end else
if trim(sfrnum) = '' then begin
sfrdenom := DupeString(' ', Length(sfrdenom));
sfrsym := ' ';
end;
end;
if sfrint = '' then sintnumspace := '';
// Compose result string
Result := sfrnum + snumsymspace + sfrsym + ssymdenomspace + sfrdenom;
if (Trim(Result) = '') and (sfrint = '') then
sfrint := '0';
if sfrint <> '' then
Result := sfrint + sintnumSpace + result;
end;
function ProcessFloatFormat(AValue: Double; AFormatSettings: TFormatSettings;
const AElements: TsNumFormatElements; var AIndex: Integer): String;
var
fs: TFormatSettings absolute AFormatSettings; // just to ease typing...
numEl: Integer;
numStr, s: String;
p, i: Integer;
decs: Integer;
useThSep: Boolean;
decsIndex: Integer;
begin
Result := '';
numEl := Length(AElements);
useThSep := AElements[AIndex].Token = nftIntTh;
// Find the element index of the decimal separator
i := AIndex;
while (i < numEl) and (AElements[i].Token <> nftDecSep) do
inc(i);
// No decimal separator --> format as integer
if i >= numEl then begin
Result := ProcessIntegerFormat(IntToStr(round(AValue)), fs, AElements, AIndex,
(INT_TOKENS + [nftIntTh]), false, useThSep);
exit;
end;
// There is a decimal separator. Get the count of decimal places.
decs := 0;
inc(i);
decsIndex := i;
while (i < numEl) and (AElements[i].Token in DECS_TOKENS) do begin
inc(decs, AElements[i].IntValue);
inc(i);
end;
// Convert value to string; this will do some rounding if required.
numstr := FloatToStrF(AValue, ffFixed, MaxInt, decs, fs);
// Process the integer part of the rounded number string
p := pos(fs.DecimalSeparator, numstr);
if p > 0 then s := copy(numstr, 1, p-1) else s := numstr;
Result := ProcessIntegerFormat(s, fs, AElements, AIndex,
(INT_TOKENS + [nftIntTh]), false, UseThSep);
// Process the fractional part of the rounded number string
if p > 0 then begin
s := Copy(numstr, p+1, Length(numstr));
AIndex := decsIndex;
s := ProcessIntegerFormat(s, fs, AElements, AIndex, DECS_TOKENS, true, false);
if s <> '' then
Result := Result + fs.DecimalSeparator + s;
end;
end;
{@@ ----------------------------------------------------------------------------
Converts a floating point number to a string as determined by the specified
number format parameters
@param AValue Value to be converted to a string
@param AParams Number format parameters which will be applied in the
conversion. The number format params are obtained
by the number format parser from the number format
string.
@param AFormatSettings Format settings needed by the number format parser for
the conversion
@return Converted string
-------------------------------------------------------------------------------}
function ConvertFloatToStr(AValue: Double; AParams: TsNumFormatParams;
AFormatSettings: TFormatSettings): String;
var
fs: TFormatSettings absolute AFormatSettings;
sidx: Integer;
section: TsNumFormatSection;
i, el, numEl: Integer;
isNeg: Boolean;
yr, mon, day, hr, min, sec, ms: Word;
s: String;
digits: Integer;
begin
Result := '';
if IsNaN(AValue) then
exit;
if AParams = nil then
begin
Result := GeneralFormatFloat(AValue, fs);
exit;
end;
sidx := 0;
if (AValue < 0) and (Length(AParams.Sections) > 1) then
sidx := 1;
if (AValue = 0) and (Length(AParams.Sections) > 2) then
sidx := 2;
isNeg := (AValue < 0);
AValue := abs(AValue); // section 0 adds the sign back, section 1 has the sign in the elements
section := AParams.Sections[sidx];
numEl := Length(section.Elements);
if nfkPercent in section.Kind then
AValue := AValue * 100.0;
if nfkHasFactor in section.Kind then
AValue := AValue * section.Factor;
if nfkTime in section.Kind then
DecodeTime(AValue, hr, min, sec, ms);
if nfkDate in section.Kind then
DecodeDate(AValue, yr, mon, day);
el := 0;
while (el < numEl) do begin
if section.Elements[el].Token = nftGeneral then
begin
s := GeneralFormatFloat(AValue, fs);
if (sidx=0) and isNeg then s := '-' + s;
Result := Result + s;
end
else
// Integer token: can be the start of a number, exp, or mixed fraction format
// Cases with thousand separator are handled here as well.
if section.Elements[el].Token in (INT_TOKENS + [nftIntTh]) then begin
// Check for exponential format
if CheckExp(section.Elements, el) then
s := ProcessExpFormat(AValue, fs, section.Elements, el)
else
// Check for fraction format
if CheckFraction(section.Elements, el, digits) then
s := ProcessFracFormat(AValue, fs, digits, section.Elements, el)
else
// Floating-point or integer
s := ProcessFloatFormat(AValue, fs, section.Elements, el);
if (sidx = 0) and isNeg then s := '-' + s;
Result := Result + s;
Continue;
end
else
// Regular fraction (without integer being split off)
if (section.Elements[el].Token in FRACNUM_TOKENS) and
CheckFraction(section.Elements, el, digits) then
begin
s := ProcessFracFormat(AValue, fs, digits, section.Elements, el);
if (sidx = 0) and isNeg then s := '-' + s;
Result := Result + s;
Continue;
end
else
case section.Elements[el].Token of
nftSpace, nftText, nftEscaped, nftCurrSymbol,
nftSign, nftSignBracket, nftPercent:
Result := Result + section.Elements[el].TextValue;
nftEmptyCharWidth:
Result := Result + ' ';
nftDateTimeSep:
case section.Elements[el].TextValue of
'/': Result := Result + fs.DateSeparator;
':': Result := Result + fs.TimeSeparator;
else Result := Result + section.Elements[el].TextValue;
end;
nftDecSep:
Result := Result + fs.DecimalSeparator;
nftThSep:
Result := Result + fs.ThousandSeparator;
nftYear:
case section.Elements[el].IntValue of
1,
2: Result := Result + IfThen(yr mod 100 < 10, '0'+IntToStr(yr mod 100), IntToStr(yr mod 100));
4: Result := Result + IntToStr(yr);
end;
nftMonth:
case section.Elements[el].IntValue of
1: Result := Result + IntToStr(mon);
2: Result := Result + IfThen(mon < 10, '0'+IntToStr(mon), IntToStr(mon));
3: Result := Result + fs.ShortMonthNames[mon];
4: Result := Result + fs.LongMonthNames[mon];
end;
nftDay:
case section.Elements[el].IntValue of
1: result := result + IntToStr(day);
2: result := Result + IfThen(day < 10, '0'+IntToStr(day), IntToStr(day));
3: Result := Result + fs.ShortDayNames[DayOfWeek(AValue)];
4: Result := Result + fs.LongDayNames[DayOfWeek(AValue)];
end;
nftHour:
begin
if section.Elements[el].IntValue < 0 then // This case is for nfTimeInterval
s := IntToStr(Int64(hr) + trunc(AValue) * 24)
else
if section.Elements[el].TextValue = 'AM' then // This tag is set in case of AM/FM format
begin
hr := hr mod 12;
if hr = 0 then hr := 12;
s := IntToStr(hr)
end else
s := IntToStr(hr);
if (abs(section.Elements[el].IntValue) = 2) and (Length(s) = 1) then
s := '0' + s;
Result := Result + s;
end;
nftMinute:
begin
if section.Elements[el].IntValue < 0 then // case for nfTimeInterval
s := IntToStr(int64(min) + trunc(AValue) * 24 * 60)
else
s := IntToStr(min);
if (abs(section.Elements[el].IntValue) = 2) and (Length(s) = 1) then
s := '0' + s;
Result := Result + s;
end;
nftSecond:
begin
if section.Elements[el].IntValue < 0 then // case for nfTimeInterval
s := IntToStr(Int64(sec) + trunc(AValue) * 24 * 60 * 60)
else
s := IntToStr(sec);
if (abs(section.Elements[el].IntValue) = 2) and (Length(s) = 1) then
s := '0' + s;
Result := Result + s;
end;
nftMilliseconds:
case section.Elements[el].IntValue of
1: Result := Result + IntToStr(ms div 100);
2: Result := Result + Format('%02d', [ms div 10]);
3: Result := Result + Format('%03d', [ms]);
end;
nftAMPM:
begin
s := section.Elements[el].TextValue;
if lowercase(s) = 'ampm' then
s := IfThen(frac(AValue) < 0.5, fs.TimeAMString, fs.TimePMString)
else
begin
i := pos('/', s);
if i > 0 then
s := IfThen(frac(AValue) < 0.5, copy(s, 1, i-1), copy(s, i+1, Length(s)))
else
s := IfThen(frac(AValue) < 0.5, 'AM', 'PM');
end;
Result := Result + s;
end;
end; // case
inc(el);
end; // while
end;
function GeneralFormatFloat(AValue: Double;
AFormatSettings: TFormatSettings): String;
begin
Result := FloatToStrF(AValue, ffGeneral, 16, 16, AFormatSettings);
// 16 is for best rounding results
end;
{==============================================================================}
{ Utility functions }
{==============================================================================}
{@@ ----------------------------------------------------------------------------
Adds an AM/PM format code to a pre-built time formatting string. The strings
replacing "AM" or "PM" in the final formatted number are taken from the
TimeAMString or TimePMString of the specified FormatSettings.
@param ATimeFormatString String of time formatting codes (such as 'hh:nn')
@param AFormatSettings FormatSettings for locale-dependent information
@result Formatting string with AM/PM option activated.
Example: ATimeFormatString = 'hh:nn' ==> 'hh:nn AM/PM'
-------------------------------------------------------------------------------}
function AddAMPM(const ATimeFormatString: String;
const AFormatSettings: TFormatSettings): String;
var
am, pm: String;
fs: TFormatSettings absolute AFormatSettings;
begin
am := IfThen(fs.TimeAMString <> '', fs.TimeAMString, 'AM');
pm := IfThen(fs.TimePMString <> '', fs.TimePMString, 'PM');
Result := Format('%s %s/%s', [StripAMPM(ATimeFormatString), am, pm]);
end;
{@@ ----------------------------------------------------------------------------
The given format string is assumed to represent a time interval, i.e. its
first time symbol must be enclosed by square brackets. Checks if this is true,
and adds the brackes if not.
@param AFormatString String with time formatting codes
@return Unchanged format string if its first time code is in square brackets
(as in '[h]:nn:ss'). If not, the first time code is enclosed in
square brackets.
-------------------------------------------------------------------------------}
function AddIntervalBrackets(AFormatString: String): String;
var
p: Integer;
s1, s2: String;
begin
if AFormatString[1] = '[' then
Result := AFormatString
else begin
p := pos(':', AFormatString);
if p <> 0 then begin
s1 := copy(AFormatString, 1, p-1);
s2 := copy(AFormatString, p, Length(AFormatString));
Result := Format('[%s]%s', [s1, s2]);
end else
Result := Format('[%s]', [AFormatString]);
end;
end;
{@@ ----------------------------------------------------------------------------
Builds a string list with samples of the predefined currency formats
@param AList String list in which the format samples are stored
@param APositive If true, samples are built for positive currency
values, otherwise for negative values
@param AValue Currency value to be used when calculating the sample
strings
@param ACurrencySymbol Currency symbol string to be used in the samples
-------------------------------------------------------------------------------}
procedure BuildCurrencyFormatList(AList: TStrings;
APositive: Boolean; AValue: Double; const ACurrencySymbol: String);
var
valueStr: String;
i: Integer;
begin
valueStr := Format('%.0n', [AValue]);
AList.BeginUpdate;
try
if AList.Count = 0 then
begin
if APositive then
for i:=0 to High(POS_CURR_FMT) do
AList.Add(Format(POS_CURR_FMT[i], [valueStr, ACurrencySymbol]))
else
for i:=0 to High(NEG_CURR_FMT) do
AList.Add(Format(NEG_CURR_FMT[i], [valueStr, ACurrencySymbol]));
end else
begin
if APositive then
for i:=0 to High(POS_CURR_FMT) do
AList[i] := Format(POS_CURR_FMT[i], [valueStr, ACurrencySymbol])
else
for i:=0 to High(NEG_CURR_FMT) do
AList[i] := Format(NEG_CURR_FMT[i], [valueStr, ACurrencySymbol]);
end;
finally
AList.EndUpdate;
end;
end;
{@@ ----------------------------------------------------------------------------
Builds a currency format string. The presentation of negative values (brackets,
or minus signs) is taken from the provided format settings. The format string
consists of three sections, separated by semicolons.
@param ANumberFormat Identifier of the built-in number format for which the
format string is to be generated.
@param AFormatSettings FormatSettings to be applied (used to extract default
values for the parameters following)
@param ADecimals number of decimal places. If < 0, the CurrencyDecimals
of the FormatSettings is used.
@param APosCurrFmt Identifier for the order of currency symbol, value and
spaces of positive values
- see pcfXXXX constants in fpsTypes.pas.
If < 0, the CurrencyFormat of the FormatSettings is used.
@param ANegCurrFmt Identifier for the order of currency symbol, value and
spaces of negative values. Specifies also usage of ().
- see ncfXXXX constants in fpsTypes.pas.
If < 0, the NegCurrFormat of the FormatSettings is used.
@param ACurrencySymbol String to identify the currency, like $ or USD.
If ? the CurrencyString of the FormatSettings is used.
@param Accounting If true, adds spaces for alignment of decimals
@return String of formatting codes
@example '"$"#,##0.00;("$"#,##0.00);"$"0.00'
-------------------------------------------------------------------------------}
function BuildCurrencyFormatString(ANumberFormat: TsNumberFormat;
const AFormatSettings: TFormatSettings;
ADecimals, APosCurrFmt, ANegCurrFmt: Integer; ACurrencySymbol: String;
Accounting: Boolean = false): String;
var
decs: String;
pcf, ncf: Byte;
p, n: String;
negRed: Boolean;
begin
pcf := IfThen(APosCurrFmt < 0, AFormatSettings.CurrencyFormat, APosCurrFmt);
ncf := IfThen(ANegCurrFmt < 0, AFormatSettings.NegCurrFormat, ANegCurrFmt);
if (ADecimals < 0) then
ADecimals := AFormatSettings.CurrencyDecimals;
if ACurrencySymbol = '?' then
ACurrencySymbol := AFormatSettings.CurrencyString;
if ACurrencySymbol <> '' then
ACurrencySymbol := '[$' + ACurrencySymbol + ']';
// ACurrencySymbol := '"' + ACurrencySymbol + '"'; // <-- not good for biff2
decs := DupeString('0', ADecimals);
if ADecimals > 0 then decs := '.' + decs;
negRed := (ANumberFormat = nfCurrencyRed);
p := POS_CURR_FMT[pcf]; // Format mask for positive values
n := NEG_CURR_FMT[ncf]; // Format mask for negative values
// add extra space for the sign of the number for perfect alignment in Excel
if Accounting then
case ncf of
0, 14: p := p + '_)';
3, 11: p := p + '_-';
4, 15: p := '_(' + p;
5, 8 : p := '_-' + p;
end;
if ACurrencySymbol <> '' then begin
Result := Format(p, ['#,##0' + decs, ACurrencySymbol]) + ';'
+ IfThen(negRed, '[red]', '')
+ Format(n, ['#,##0' + decs, ACurrencySymbol]) + ';'
+ Format(p, ['0'+decs, ACurrencySymbol]);
end
else begin
Result := '#,##0' + decs;
if negRed then
Result := Result +';[red]'
else
Result := Result +';';
case ncf of
0, 14, 15 : Result := Result + '(#,##0' + decs + ')';
1, 2, 5, 6, 8, 9, 12: Result := Result + '-#,##0' + decs;
else Result := Result + '#,##0' + decs + '-';
end;
Result := Result + ';0' + decs;
end;
end;
{@@ ----------------------------------------------------------------------------
Builds a date/time format string from the number format code.
@param ANumberFormat built-in number format identifier
@param AFormatSettings Format settings from which locale-dependent
information like day-month-year order is taken.
@param AFormatString Optional pre-built formatting string. It is used
only for the format nfInterval where square brackets
are added to the first time code field.
@return String of date/time formatting code constructed from the built-in
format information.
-------------------------------------------------------------------------------}
function BuildDateTimeFormatString(ANumberFormat: TsNumberFormat;
const AFormatSettings: TFormatSettings; AFormatString: String = '') : string;
var
i, j: Integer;
Unwanted: set of ansichar;
begin
case ANumberFormat of
nfShortDateTime:
Result := AFormatSettings.ShortDateFormat + ' ' + AFormatSettings.ShortTimeFormat;
// In the DefaultFormatSettings this is: d/m/y hh:nn
nfShortDate:
Result := AFormatSettings.ShortDateFormat; // --> d/m/y
nfLongDate:
Result := AFormatSettings.LongDateFormat; // --> dd mm yyyy
nfShortTime:
Result := StripAMPM(AFormatSettings.ShortTimeFormat); // --> hh:nn
nfLongTime:
Result := StripAMPM(AFormatSettings.LongTimeFormat); // --> hh:nn:ss
nfShortTimeAM:
begin // --> hh:nn AM/PM
Result := AFormatSettings.ShortTimeFormat;
if (pos('a', lowercase(AFormatSettings.ShortTimeFormat)) = 0) then
Result := AddAMPM(Result, AFormatSettings);
end;
nfLongTimeAM: // --> hh:nn:ss AM/PM
begin
Result := AFormatSettings.LongTimeFormat;
if pos('a', lowercase(AFormatSettings.LongTimeFormat)) = 0 then
Result := AddAMPM(Result, AFormatSettings);
end;
nfDayMonth, // --> dd/mmm
nfMonthYear: // --> mmm/yy
begin
Result := AFormatSettings.ShortDateFormat;
case ANumberFormat of
nfDayMonth:
unwanted := ['y', 'Y'];
nfMonthYear:
unwanted := ['d', 'D'];
end;
for i:=Length(Result) downto 1 do
if Result[i] in unwanted then Delete(Result, i, 1);
while not (Result[1] in (['m', 'M', 'd', 'D', 'y', 'Y'] - unwanted)) do
Delete(Result, 1, 1);
while not (Result[Length(Result)] in (['m', 'M', 'd', 'D', 'y', 'Y'] - unwanted)) do
Delete(Result, Length(Result), 1);
i := 1;
while not (Result[i] in ['m', 'M']) do inc(i);
j := i;
while (j <= Length(Result)) and (Result[j] in ['m', 'M']) do inc(j);
while (j - i < 3) do begin
Insert(Result[i], Result, j);
inc(j);
end;
end;
nfTimeInterval: // --> [h]:nn:ss
if AFormatString = '' then
Result := '[h]:nn:ss'
else
Result := AddIntervalBrackets(AFormatString);
end;
end;
{@@ ----------------------------------------------------------------------------
Builds a number format string for fraction formatting from the number format
code and the count of numerator and denominator digits.
@param AMixedFraction If TRUE, fraction is presented as mixed fraction
@param ANumeratorDigits Count of numerator digits
@param ADenominatorDigits Count of denominator digits. If the value is negative
then its absolute value is inserted literally as
as denominator.
@return String of formatting code, here something like: '##/##' or '# ##/##'
-------------------------------------------------------------------------------}
function BuildFractionFormatString(AMixedFraction: Boolean;
ANumeratorDigits, ADenominatorDigits: Integer): String;
begin
if ADenominatorDigits < 0 then // a negative value indicates a fixed denominator value
Result := Format('%s/%d', [
DupeString('?', ANumeratorDigits), -ADenominatorDigits
])
else
Result := Format('%s/%s', [
DupeString('?', ANumeratorDigits), DupeString('?', ADenominatorDigits)
]);
if AMixedFraction then
Result := '# ' + Result;
end;
{@@ ----------------------------------------------------------------------------
Builds a number format string from the number format code and the count of
decimal places.
@param ANumberFormat Identifier of the built-in numberformat for which a
format string is to be generated
@param AFormatSettings FormatSettings for default parameters
@param ADecimals Number of decimal places. If < 0 the CurrencyDecimals
value of the FormatSettings is used. In case of a
fraction format "ADecimals" refers to the maximum count
digits of the denominator.
@return String of formatting codes
@example ANumberFormat = nfFixedTh, ADecimals = 2 --> '#,##0.00'
-------------------------------------------------------------------------------}
function BuildNumberFormatString(ANumberFormat: TsNumberFormat;
const AFormatSettings: TFormatSettings; ADecimals: Integer = -1): String;
var
decs: String;
begin
Result := '';
if ADecimals = -1 then
ADecimals := AFormatSettings.CurrencyDecimals;
decs := DupeString('0', ADecimals);
if ADecimals > 0 then decs := '.' + decs;
case ANumberFormat of
nfText:
Result := '@';
nfFixed:
Result := '0' + decs;
nfFixedTh:
Result := '#,##0' + decs;
nfExp:
Result := '0' + decs + 'E+00';
nfPercentage:
Result := '0' + decs + '%';
nfFraction:
if ADecimals = 0 then // "ADecimals" has a different meaning here...
Result := '# ??/??' // This is the default fraction format
else
begin
decs := DupeString('?', ADecimals);
Result := '# ' + decs + '/' + decs;
end;
nfCurrency, nfCurrencyRed:
Result := BuildCurrencyFormatString(ANumberFormat, AFormatSettings,
ADecimals, AFormatSettings.CurrencyFormat, AFormatSettings.NegCurrFormat,
AFormatSettings.CurrencyString);
nfShortDateTime, nfShortDate, nfLongDate, nfShortTime, nfLongTime,
nfShortTimeAM, nfLongTimeAM, nfDayMonth, nfMonthYear, nfTimeInterval:
raise Exception.Create('BuildNumberFormatString: Use BuildDateTimeFormatSstring '+
'to create a format string for date/time values.');
end;
end;
{@@ ----------------------------------------------------------------------------
Creates a format string for the specified parsed number format section.
The format string is created according to Excel convention (which is understood
by ODS as well).
@param ASection Parsed section of number format elements as created by the
number format parser
@return Excel-compatible format string
-------------------------------------------------------------------------------}
function BuildFormatStringFromSection(const ASection: TsNumFormatSection): String;
var
element: TsNumFormatElement;
i, n: Integer;
begin
Result := '';
for i := 0 to High(ASection.Elements) do begin
element := ASection.Elements[i];
case element.Token of
nftGeneral:
Result := Result + 'General';
nftIntOptDigit, nftOptDecs, nftFracNumOptDigit, nftFracDenomOptDigit:
if element.IntValue > 0 then
Result := Result + DupeString('#', element.IntValue);
nftIntZeroDigit, nftZeroDecs, nftFracNumZeroDigit, nftFracDenomZeroDigit, nftExpDigits:
if element.IntValue > 0 then
Result := result + DupeString('0', element.IntValue);
nftIntSpaceDigit, nftSpaceDecs, nftFracNumSpaceDigit, nftFracDenomSpaceDigit:
if element.Intvalue > 0 then
Result := result + DupeString('?', element.IntValue);
nftFracDenom:
Result := Result + IntToStr(element.IntValue);
nftIntTh:
case element.Intvalue of
0: Result := Result + '#,###';
1: Result := Result + '#,##0';
2: Result := Result + '#,#00';
3: Result := Result + '#,000';
end;
nftDecSep, nftThSep:
Result := Result + element.TextValue;
nftFracSymbol:
Result := Result + '/';
nftPercent:
Result := Result + '%';
nftFactor:
if element.IntValue <> 0 then
begin
n := element.IntValue;
while (n > 0) do
begin
Result := Result + element.TextValue;
dec(n);
end;
end;
nftSpace:
Result := Result + ' ';
nftText:
if element.TextValue <> '' then result := Result + '"' + element.TextValue + '"';
nftYear:
Result := Result + DupeString('Y', element.IntValue);
nftMonth:
Result := Result + DupeString('M', element.IntValue);
nftDay:
Result := Result + DupeString('D', element.IntValue);
nftHour:
if element.IntValue < 0
then Result := Result + '[' + DupeString('h', -element.IntValue) + ']'
else Result := Result + DupeString('h', element.IntValue);
nftMinute:
if element.IntValue < 0
then Result := result + '[' + DupeString('m', -element.IntValue) + ']'
else Result := Result + DupeString('m', element.IntValue);
nftSecond:
if element.IntValue < 0
then Result := Result + '[' + DupeString('s', -element.IntValue) + ']'
else Result := Result + DupeString('s', element.IntValue);
nftMilliseconds:
Result := Result + DupeString('0', element.IntValue);
nftSign, nftSignBracket, nftExpChar, nftExpSign, nftAMPM, nftDateTimeSep:
if element.TextValue <> '' then Result := Result + element.TextValue;
nftCurrSymbol:
if element.TextValue <> '' then
Result := Result + '[$' + element.TextValue + ']';
nftEscaped:
if element.TextValue <> '' then
Result := Result + '\' + element.TextValue;
nftRepeat:
if element.TextValue <> '' then Result := Result + '*' + element.TextValue;
nftColor:
case element.IntValue of
scBlack : Result := '[black]';
scWhite : Result := '[white]';
scRed : Result := '[red]';
scBlue : Result := '[blue]';
scGreen : Result := '[green]';
scYellow : Result := '[yellow]';
scMagenta: Result := '[magenta]';
scCyan : Result := '[cyan]';
else Result := Format('[Color%d]', [element.IntValue]);
end;
nftTextFormat:
Result := '@';
end;
end;
end;
{@@ ----------------------------------------------------------------------------
Counts how many decimal places are coded into a given number format string.
@param AFormatString String with number format codes, such as '0.000'
@param ADecChars Characters which are considered as symbols for decimals.
For the fixed decimals, this is the '0'. Optional
decimals are encoced as '#'.
@return Count of decimal places found (3 in above example).
-------------------------------------------------------------------------------}
function CountDecs(AFormatString: String; ADecChars: TsDecsChars = ['0']): Byte;
var
i: Integer;
begin
Result := 0;
i := 1;
while (i <= Length(AFormatString)) do begin
if AFormatString[i] = '.' then begin
inc(i);
while (i <= Length(AFormatString)) and (AFormatString[i] in ADecChars) do begin
inc(i);
inc(Result);
end;
exit;
end else
inc(i);
end;
end;
{@@ ----------------------------------------------------------------------------
Applies a text format to a text. The text placeholder is @. Supports
appending and prepending text.
-------------------------------------------------------------------------------}
function ApplyTextFormat(AText: String; AParams: TsNumFormatParams): String;
var
sct: TsNumFormatSection;
element: TsNumFormatElement;
i: Integer;
begin
Result := '';
for sct in AParams.Sections do
for i := 0 to High(sct.Elements) do begin
element := sct.Elements[i];
case element.Token of
nftTextFormat:
Result := Result + AText;
nftText:
Result := Result + element.TextValue;
end;
end;
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified text corresponds to a boolean value. For this,
it must match the specified TRUE and FALSE text phrases.
-------------------------------------------------------------------------------}
function IsBoolValue(const AText, ATrueText, AFalseText: String;
out AValue: Boolean): Boolean;
begin
if SameText(AText, ATrueText) then
begin
AValue := true;
Result := true;
end else
if SameText(AText, AFalseText) then
begin
AValue := false;
Result := true;
end else
Result := false;
end;
{@@ ----------------------------------------------------------------------------
Checks whether the given number format code is for currency,
i.e. requires a currency symbol.
@param AFormat Built-in number format identifier to be checked
@return True if AFormat is nfCurrency or nfCurrencyRed, false otherwise.
-------------------------------------------------------------------------------}
function IsCurrencyFormat(AFormat: TsNumberFormat): Boolean;
begin
Result := AFormat in [nfCurrency, nfCurrencyRed];
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified number format parameters apply to currency values.
@param ANumFormat Number format parameters
@return True if Kind of the 1st format parameter section contains the
nfkCurrency elements; false otherwise
-------------------------------------------------------------------------------}
function IsCurrencyFormat(ANumFormat: TsNumFormatParams): Boolean;
begin
Result := (ANumFormat <> nil) and
(ANumFormat.Sections[0].Kind * [nfkCurrency] <> []);
end;
{@@ ----------------------------------------------------------------------------
Checks whether the given number format code is for date/time values.
@param AFormat Built-in number format identifier to be checked
@return True if AFormat is a date/time format (such as nfShortTime),
false otherwise
-------------------------------------------------------------------------------}
function IsDateTimeFormat(AFormat: TsNumberFormat): Boolean;
begin
Result := AFormat in [nfShortDateTime, nfShortDate, nfLongDate,
nfShortTime, nfLongTime, nfShortTimeAM, nfLongTimeAM,
nfDayMonth, nfMonthYear, nfTimeInterval];
end;
{@@ ----------------------------------------------------------------------------
Checks whether the given string with formatting codes is for date/time values.
@param AFormatStr String with formatting codes to be checked.
@return True if AFormatStr is a date/time format string (such as 'hh:nn'),
false otherwise
-------------------------------------------------------------------------------}
function IsDateTimeFormat(AFormatStr: string): Boolean;
var
parser: TsNumFormatParser;
begin
parser := TsNumFormatParser.Create(AFormatStr, DefaultFormatSettings);
try
Result := parser.IsDateTimeFormat;
finally
parser.Free;
end;
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified number format parameters apply to date/time values.
@param ANumFormat Number format parameters
@return True if Kind of the 1st format parameter section contains the
nfkDate or nfkTime elements; false otherwise
-------------------------------------------------------------------------------}
function IsDateTimeFormat(ANumFormat: TsNumFormatParams): Boolean;
begin
Result := (ANumFormat <> nil) and
(ANumFormat.Sections[0].Kind * [nfkDate, nfkTime] <> []);
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified text corresponds to a date/time value and returns
true, its numerical value and its built-in numberformat if it is.
-------------------------------------------------------------------------------}
function IsDateTimeValue(AText: String; const AFormatSettings: TFormatSettings;
out ADateTime: TDateTime; out ANumFormat: TsNumberFormat): Boolean;
{ Test whether the text is formatted according to a built-in date/time format.
Converts the obtained date/time value back to a string and compares. }
function TestFormat(lNumFmt: TsNumberFormat): Boolean;
var
fmt: string;
begin
fmt := BuildDateTimeFormatString(lNumFmt, AFormatSettings);
Result := FormatDateTime(fmt, ADateTime, AFormatSettings) = AText;
if Result then ANumFormat := lNumFmt;
end;
begin
Result := TryStrToDateTime(AText, ADateTime, AFormatSettings);
if Result then
begin
ANumFormat := nfCustom;
if abs(ADateTime) > 1 then // this is most probably a date
begin
if TestFormat(nfShortDateTime) then
exit;
if TestFormat(nfLongDate) then
exit;
if TestFormat(nfShortDate) then
exit;
if TestFormat(nfMonthYear) then
exit;
if TestFormat(nfDayMonth) then
exit;
end else
begin // this case is time-only
if TestFormat(nfLongTimeAM) then
exit;
if TestFormat(nfLongTime) then
exit;
if TestFormat(nfShortTimeAM) then
exit;
if TestFormat(nfShortTime) then
exit;
end;
end;
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified number format parameters apply to a date value.
@param ANumFormat Number format parameters
@return True if Kind of the 1st format parameter section contains the
nfkDate, but no nfkTime tags; false otherwise
-------------------------------------------------------------------------------}
function IsDateFormat(ANumFormat: TsNumFormatParams): Boolean;
begin
Result := (ANumFormat <> nil) and
(ANumFormat.Sections[0].Kind * [nfkDate, nfkTime] = [nfkDate]);
end;
{@@ ----------------------------------------------------------------------------
Checks whether the given built-in number format code is for time values.
@param AFormat Built-in number format identifier to be checked
@return True if AFormat represents to a time-format, false otherwise
-------------------------------------------------------------------------------}
function IsTimeFormat(AFormat: TsNumberFormat): boolean;
begin
Result := AFormat in [nfShortTime, nfLongTime, nfShortTimeAM, nfLongTimeAM,
nfTimeInterval];
end;
{@@ ----------------------------------------------------------------------------
Checks whether the given string with formatting codes is for time values.
@param AFormatStr String with formatting codes to be checked
@return True if AFormatStr represents a time-format, false otherwise
-------------------------------------------------------------------------------}
function IsTimeFormat(AFormatStr: String): Boolean;
var
parser: TsNumFormatParser;
begin
parser := TsNumFormatParser.Create(AFormatStr, DefaultFormatSettings);
try
Result := parser.IsTimeFormat;
finally
parser.Free;
end;
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified number format parameters apply to time values.
@param ANumFormat Number format parameters
@return True if Kind of the 1st format parameter section contains the
nfkTime, but no nfkDate elements; false otherwise
-------------------------------------------------------------------------------}
function IsTimeFormat(ANumFormat: TsNumFormatParams): Boolean;
begin
Result := (ANumFormat <> nil) and
(ANumFormat.Sections[0].Kind * [nfkTime, nfkDate] = [nfkTime]);
end;
{@@ ----------------------------------------------------------------------------
Returns TRUE if the specified format string represents a long time format, i.e.
it contains two TimeSeparators.
-------------------------------------------------------------------------------}
function IsLongTimeFormat(AFormatStr: String; ATimeSeparator: Char): Boolean;
var
i, n: Integer;
begin
n := 0;
for i:=1 to Length(AFormatStr) do
if AFormatStr[i] = ATimeSeparator then inc(n);
Result := (n=2);
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified text corresponds to a numerical value. If it is
then the function result is TRUE, and the number value and its formatting
parameters are returned.
-------------------------------------------------------------------------------}
function IsNumberValue(AText: String; AutoDetectNumberFormat: Boolean;
const AFormatSettings: TFormatSettings;
out ANumber: Double; out ANumFormat: TsNumberFormat; out ADecimals: Integer;
out ACurrencySymbol, AWarning: String): Boolean;
var
p: Integer;
DecSep, ThousSep: Char;
begin
Result := false;
AWarning := '';
// To detect whether the text is a currency value we look for the currency
// string. If we find it, we delete it and convert the remaining string to
// a number.
ACurrencySymbol := AFormatSettings.CurrencyString;
if RemoveCurrencySymbol(ACurrencySymbol, AText) then
begin
if IsNegative(AText) then
begin
if AText = '' then
exit;
AText := '-' + AText;
end;
end else
ACurrencySymbol := '';
if AutoDetectNumberFormat then
Result := TryStrToFloatAuto(AText, ANumber, DecSep, ThousSep, AWarning)
else begin
Result := TryStrToFloat(AText, ANumber, AFormatSettings);
if Result then
begin
if pos(AFormatSettings.DecimalSeparator, AText) = 0
then DecSep := #0
else DecSep := AFormatSettings.DecimalSeparator;
if pos(AFormatSettings.ThousandSeparator, AText) = 0
then ThousSep := #0
else ThousSep := AFormatSettings.ThousandSeparator;
end;
end;
// Try to determine the number format
if Result then
begin
if ThousSep <> #0 then
ANumFormat := nfFixedTh
else
ANumFormat := nfGeneral;
// count number of decimal places and try to catch special formats
ADecimals := 0;
if DecSep <> #0 then
begin
// Go to the decimal separator and search towards the end of the string
p := pos(DecSep, AText) + 1;
while (p <= Length(AText)) do begin
// exponential format
if AText[p] in ['+', '-', 'E', 'e'] then
begin
ANumFormat := nfExp;
break;
end else
// percent format
if AText[p] = '%' then
begin
ANumFormat := nfPercentage;
break;
end else
begin
inc(p);
inc(ADecimals);
end;
end;
if (ADecimals > 0) and (ADecimals < 9) and (ANumFormat = nfGeneral) then
// "no formatting" assumed if there are "many" decimals
ANumFormat := nfFixed;
end else
begin
p := Length(AText);
while (p > 0) do begin
case AText[p] of
'%' : ANumFormat := nfPercentage;
'e', 'E': ANumFormat := nfExp;
else dec(p);
end;
break;
end;
end;
end else
ACurrencySymbol := '';
end;
{@@ ----------------------------------------------------------------------------
Checks whether the specified number format parameters is a time interval
format.
@param ANumFormat Number format parameters
@return True if Kind of the 1st format parameter section contains the
nfkTimeInterval elements; false otherwise
-------------------------------------------------------------------------------}
function IsTimeIntervalFormat(ANumFormat: TsNumFormatParams): Boolean;
begin
Result := (ANumFormat <> nil) and
(ANumFormat.Sections[0].Kind * [nfkTimeInterval] <> []);
end;
function IsTextFormat(ANumFormat: TsNumFormatParams): Boolean;
begin
Result := (ANumFormat <> nil) and
(ANumFormat.Sections[0].Kind = [nfkText]);
end;
{@@ ----------------------------------------------------------------------------
Creates a long date format string out of a short date format string.
Retains the order of year-month-day and the separators, but uses 4 digits
for year and 3 digits of month.
@param ADateFormat String with date formatting code representing a
"short" date, such as 'dd/mm/yy'
@return Format string modified to represent a "long" date, such as 'dd/mmm/yyyy'
-------------------------------------------------------------------------------}
function MakeLongDateFormat(ADateFormat: String): String;
var
i: Integer;
begin
Result := '';
i := 1;
while i < Length(ADateFormat) do begin
case ADateFormat[i] of
'y', 'Y':
begin
Result := Result + DupeString(ADateFormat[i], 4);
while (i < Length(ADateFormat)) and (ADateFormat[i] in ['y','Y']) do
inc(i);
end;
'm', 'M':
begin
result := Result + DupeString(ADateFormat[i], 3);
while (i < Length(ADateFormat)) and (ADateFormat[i] in ['m','M']) do
inc(i);
end;
else
Result := Result + ADateFormat[i];
inc(i);
end;
end;
end;
{@@ ----------------------------------------------------------------------------
Modifies the short date format such that it has a two-digit year and a two-digit
month. Retains the order of year-month-day and the separators.
@param ADateFormat String with date formatting codes representing a
"long" date, such as 'dd/mmm/yyyy'
@return Format string modified to represent a "short" date, such as 'dd/mm/yy'
-------------------------------------------------------------------------------}
function MakeShortDateFormat(ADateFormat: String): String;
var
i: Integer;
begin
Result := '';
i := 1;
while i < Length(ADateFormat) do begin
case ADateFormat[i] of
'y', 'Y':
begin
Result := Result + DupeString(ADateFormat[i], 2);
while (i < Length(ADateFormat)) and (ADateFormat[i] in ['y','Y']) do
inc(i);
end;
'm', 'M':
begin
result := Result + DupeString(ADateFormat[i], 2);
while (i < Length(ADateFormat)) and (ADateFormat[i] in ['m','M']) do
inc(i);
end;
else
Result := Result + ADateFormat[i];
inc(i);
end;
end;
end;
{@@ ----------------------------------------------------------------------------
Creates a "time interval" format string having the first time code identifier
in square brackets.
@param Src Source format string, must be a time format string, like 'hh:nn'
@param Dest Destination format string, will have the first time code element
of the src format string in square brackets, like '[hh]:nn'.
-------------------------------------------------------------------------------}
procedure MakeTimeIntervalMask(Src: String; var Dest: String);
var
L: TStrings;
begin
L := TStringList.Create;
try
L.StrictDelimiter := true;
L.Delimiter := ':';
L.DelimitedText := Src;
if L[0][1] <> '[' then L[0] := '[' + L[0];
if L[0][Length(L[0])] <> ']' then L[0] := L[0] + ']';
Dest := L.DelimitedText;
finally
L.Free;
end;
end;
{@@ ----------------------------------------------------------------------------
Removes an AM/PM formatting code from a given time formatting string. Variants
of "AM/PM" are considered as well. The string is left unchanged if it does not
contain AM/PM codes.
@param ATimeFormatString String of time formatting codes (such as 'hh:nn AM/PM')
@return Formatting string with AM/PM being removed (--> 'hh:nn')
-------------------------------------------------------------------------------}
function StripAMPM(const ATimeFormatString: String): String;
var
i: Integer;
begin
Result := '';
i := 1;
while i <= Length(ATimeFormatString) do begin
if ATimeFormatString[i] in ['a', 'A'] then begin
inc(i);
while (i <= Length(ATimeFormatString)) and (ATimeFormatString[i] in ['p', 'P', 'm', 'M', '/']) do
inc(i);
end else
Result := Result + ATimeFormatString[i];
inc(i);
end;
end;
{@@ ----------------------------------------------------------------------------
Initializes the FormatSettings of file a import/export parameters record to
default values which can be replaced by the FormatSettings of the
workbook's FormatSettings
-------------------------------------------------------------------------------}
procedure InitFormatSettings(out AFormatSettings: TFormatSettings);
var
i: Integer;
begin
with AFormatSettings do
begin
CurrencyFormat := Byte(-1);
NegCurrFormat := Byte(-1);
ThousandSeparator := #0;
DecimalSeparator := #0;
CurrencyDecimals := Byte(-1);
DateSeparator := #0;
TimeSeparator := #0;
ListSeparator := #0;
CurrencyString := '';
ShortDateFormat := '';
LongDateFormat := '';
TimeAMString := '';
TimePMString := '';
ShortTimeFormat := '';
LongTimeFormat := '';
for i:=1 to 12 do
begin
ShortMonthNames[i] := '';
LongMonthNames[i] := '';
end;
for i:=1 to 7 do
begin
ShortDayNames[i] := '';
LongDayNames[i] := '';
end;
TwoDigitYearCenturyWindow := Word(-1);
end;
end;
{@@ ----------------------------------------------------------------------------
Replaces in AFormatSettings all members marked as having default values (#0,
-1, '') by the corresponding values of the ADefaultFormats record
-------------------------------------------------------------------------------}
procedure ReplaceFormatSettings(var AFormatSettings: TFormatSettings;
const ADefaultFormats: TFormatSettings);
var
i: Integer;
begin
if AFormatSettings.CurrencyFormat = Byte(-1) then
AFormatSettings.CurrencyFormat := ADefaultFormats.CurrencyFormat;
if AFormatSettings.NegCurrFormat = Byte(-1) then
AFormatSettings.NegCurrFormat := ADefaultFormats.NegCurrFormat;
if AFormatSettings.ThousandSeparator = #0 then
AFormatSettings.ThousandSeparator := ADefaultFormats.ThousandSeparator;
if AFormatSettings.DecimalSeparator = #0 then
AFormatSettings.DecimalSeparator := ADefaultFormats.DecimalSeparator;
if AFormatSettings.CurrencyDecimals = Byte(-1) then
AFormatSettings.CurrencyDecimals := ADefaultFormats.CurrencyDecimals;
if AFormatSettings.DateSeparator = #0 then
AFormatSettings.DateSeparator := ADefaultFormats.DateSeparator;
if AFormatSettings.TimeSeparator = #0 then
AFormatSettings.TimeSeparator := ADefaultFormats.TimeSeparator;
if AFormatSettings.ListSeparator = #0 then
AFormatSettings.ListSeparator := ADefaultFormats.ListSeparator;
if AFormatSettings.CurrencyString = '' then
AFormatSettings.CurrencyString := ADefaultFormats.CurrencyString;
if AFormatSettings.ShortDateFormat = '' then
AFormatSettings.ShortDateFormat := ADefaultFormats.ShortDateFormat;
if AFormatSettings.LongDateFormat = '' then
AFormatSettings.LongDateFormat := ADefaultFormats.LongDateFormat;
if AFormatSettings.ShortTimeFormat = '' then
AFormatSettings.ShortTimeFormat := ADefaultFormats.ShortTimeFormat;
if AFormatSettings.LongTimeFormat = '' then
AFormatSettings.LongTimeFormat := ADefaultFormats.LongTimeFormat;
for i:=1 to 12 do
begin
if AFormatSettings.ShortMonthNames[i] = '' then
AFormatSettings.ShortMonthNames[i] := ADefaultFormats.ShortMonthNames[i];
if AFormatSettings.LongMonthNames[i] = '' then
AFormatSettings.LongMonthNames[i] := ADefaultFormats.LongMonthNames[i];
end;
for i:=1 to 7 do
begin
if AFormatSettings.ShortDayNames[i] = '' then
AFormatSettings.ShortDayNames[i] := ADefaultFormats.ShortDayNames[i];
if AFormatSettings.LongDayNames[i] = '' then
AFormatSettings.LongDayNames[i] := ADefaultFormats.LongDayNames[i];
end;
if AFormatSettings.TwoDigitYearCenturyWindow = Word(-1) then
AFormatSettings.TwoDigitYearCenturyWindow := ADefaultFormats.TwoDigitYearCenturyWindow;
end;
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;
{==============================================================================}
{ TsNumFormatParams }
{==============================================================================}
{@@ ----------------------------------------------------------------------------
Deletes a parsed number format element from the specified format section.
@param ASectionIndex Index of the format section containing the element to
be deleted
@param AElementIndex Index of the format element to be deleted
-------------------------------------------------------------------------------}
procedure TsNumFormatParams.DeleteElement(ASectionIndex, AElementIndex: Integer);
var
i, n: Integer;
begin
with Sections[ASectionIndex] do
begin
n := Length(Elements);
for i := AElementIndex+1 to n-1 do
Elements[i-1] := Elements[i];
SetLength(Elements, n-1);
end;
end;
{@@ ----------------------------------------------------------------------------
Creates the built-in number format identifier from the parsed number format
sections and elements
@return Built-in number format identifer if the format is built into
fpspreadsheet, or nfCustom otherwise
@see TsNumFormat
-------------------------------------------------------------------------------}
function TsNumFormatParams.GetNumFormat: TsNumberFormat;
begin
Result := nfCustom;
case Length(Sections) of
0: Result := nfGeneral;
1: Result := Sections[0].NumFormat;
2: if (Sections[0].NumFormat = Sections[1].NumFormat) and
(Sections[0].NumFormat in [nfCurrency, nfCurrencyRed])
then
Result := Sections[0].NumFormat;
3: if (Sections[0].NumFormat = Sections[1].NumFormat) and
(Sections[1].NumFormat = Sections[2].NumFormat) and
(Sections[0].NumFormat in [nfCurrency, nfCurrencyRed])
then
Result := Sections[0].NumFormat;
end;
end;
{@@ ----------------------------------------------------------------------------
Constructs the number format string from the parsed sections and elements.
The format symbols are selected according to Excel syntax.
@return Excel-compatible number format string.
-------------------------------------------------------------------------------}
function TsNumFormatParams.GetNumFormatStr: String;
var
i: Integer;
begin
if Length(Sections) > 0 then begin
Result := BuildFormatStringFromSection(Sections[0]);
for i := 1 to High(Sections) do
Result := Result + ';' + BuildFormatStringFromSection(Sections[i]);
end else
Result := '';
end;
{@@ ----------------------------------------------------------------------------
Inserts a parsed format token into the specified format section before the
specified element.
@param ASectionIndex Index of the parsed format section into which the
token is to be inserted
@param AElementIndex Index of the format element before which the token
is to be inserted
@param AToken Parsed format token to be inserted
@see TsNumFormatToken
-------------------------------------------------------------------------------}
procedure TsNumFormatParams.InsertElement(ASectionIndex, AElementIndex: Integer;
AToken: TsNumFormatToken);
var
i, n: Integer;
begin
with Sections[ASectionIndex] do
begin
n := Length(Elements);
SetLength(Elements, n+1);
for i:=n-1 downto AElementIndex do
Elements[i+1] := Elements[i];
Elements[AElementIndex].Token := AToken;
end;
end;
{@@ ----------------------------------------------------------------------------
Checks whether the parsed format sections passed as a parameter are identical
to the interal section array.
@param ASections Array of parsed format sections to be compared with the
internal format sections
-------------------------------------------------------------------------------}
function TsNumFormatParams.SectionsEqualTo(ASections: TsNumFormatSections): Boolean;
var
i, j: Integer;
begin
Result := false;
if Length(ASections) <> Length(Sections) then
exit;
for i := 0 to High(Sections) do begin
if Length(Sections[i].Elements) <> Length(ASections[i].Elements) then
exit;
for j:=0 to High(Sections[i].Elements) do
begin
if Sections[i].Elements[j].Token <> ASections[i].Elements[j].Token then
exit;
if Sections[i].NumFormat <> ASections[i].NumFormat then
exit;
if Sections[i].Decimals <> ASections[i].Decimals then
exit;
{
if Sections[i].Factor <> ASections[i].Factor then
exit;
}
if Sections[i].FracInt <> ASections[i].FracInt then
exit;
if Sections[i].FracNumerator <> ASections[i].FracNumerator then
exit;
if Sections[i].FracDenominator <> ASections[i].FracDenominator then
exit;
if Sections[i].CurrencySymbol <> ASections[i].CurrencySymbol then
exit;
if Sections[i].Color <> ASections[i].Color then
exit;
case Sections[i].Elements[j].Token of
nftText, nftThSep, nftDecSep, nftDateTimeSep,
nftAMPM, nftSign, nftSignBracket,
nftExpChar, nftExpSign, nftPercent, nftFracSymbol, nftCurrSymbol,
nftCountry, nftSpace, nftEscaped, nftRepeat, nftEmptyCharWidth,
nftTextFormat:
if Sections[i].Elements[j].TextValue <> ASections[i].Elements[j].TextValue
then exit;
nftYear, nftMonth, nftDay,
nftHour, nftMinute, nftSecond, nftMilliseconds,
nftMonthMinute,
nftIntOptDigit, nftIntZeroDigit, nftIntSpaceDigit, nftIntTh,
nftZeroDecs, nftOptDecs, nftSpaceDecs, nftExpDigits, nftFactor,
nftFracNumOptDigit, nftFracNumSpaceDigit, nftFracNumZeroDigit,
nftFracDenomOptDigit, nftFracDenomSpaceDigit, nftFracDenomZeroDigit,
nftColor:
if Sections[i].Elements[j].IntValue <> ASections[i].Elements[j].IntValue
then exit;
nftCompareOp, nftCompareValue:
if Sections[i].Elements[j].FloatValue <> ASections[i].Elements[j].FloatValue
then exit;
end;
end;
end;
Result := true;
end;
{@@ ----------------------------------------------------------------------------
Defines the currency symbol used in the format params sequence
@param AValue String containing the currency symbol to be used in the
converted numbers
-------------------------------------------------------------------------------}
procedure TsNumFormatParams.SetCurrSymbol(AValue: String);
var
section: TsNumFormatSection;
s, el: Integer;
begin
for s:=0 to High(Sections) do
begin
section := Sections[s];
if (nfkCurrency in section.Kind) then
begin
section.CurrencySymbol := AValue;
for el := 0 to High(section.Elements) do
if section.Elements[el].Token = nftCurrSymbol then
section.Elements[el].Textvalue := AValue;
end;
end;
end;
{@@ ----------------------------------------------------------------------------
Adds or modifies parsed format tokens such that the specified number of
decimal places is displayed
@param AValue Number of decimal places to be shown
-------------------------------------------------------------------------------}
procedure TsNumFormatParams.SetDecimals(AValue: byte);
var
section: TsNumFormatSection;
s, el: Integer;
begin
for s := 0 to High(Sections) do
begin
section := Sections[s];
if section.Kind * [nfkFraction, nfkDate, nfkTime] <> [] then
Continue;
section.Decimals := AValue;
for el := High(section.Elements) downto 0 do
case section.Elements[el].Token of
nftZeroDecs:
section.Elements[el].Intvalue := AValue;
nftOptDecs, nftSpaceDecs:
DeleteElement(s, el);
end;
end;
end;
{@@ ----------------------------------------------------------------------------
If AEnable is true a format section for negative numbers is added (or an
existing one is modified) such that negative numbers are displayed in red.
If AEnable is false the format tokens are modified such that negative values
are displayed in default color.
@param AEnable The format tokens are modified such as to display negative
values in red if AEnable is true.
-------------------------------------------------------------------------------}
procedure TsNumFormatParams.SetNegativeRed(AEnable: Boolean);
var
el: Integer;
begin
// Enable negative-value color
if AEnable then
begin
if Length(Sections) = 1 then begin
SetLength(Sections, 2);
Sections[1] := Sections[0];
InsertElement(1, 0, nftColor);
Sections[1].Elements[0].Intvalue := scRed;
InsertElement(1, 1, nftSign);
Sections[1].Elements[1].TextValue := '-';
end else
begin
if not (nfkHasColor in Sections[1].Kind) then
InsertElement(1, 0, nftColor);
for el := 0 to High(Sections[1].Elements) do
if Sections[1].Elements[el].Token = nftColor then
Sections[1].Elements[el].IntValue := scRed;
end;
Sections[1].Kind := Sections[1].Kind + [nfkHasColor];
Sections[1].Color := scRed;
end else
// Disable negative-value color
if Length(Sections) >= 2 then
begin
Sections[1].Kind := Sections[1].Kind - [nfkHasColor];
Sections[1].Color := scBlack;
for el := High(Sections[1].Elements) downto 0 do
if Sections[1].Elements[el].Token = nftColor then
DeleteElement(1, el);
end;
end;
{@@ ----------------------------------------------------------------------------
Inserts a thousand separator token into the format elements at the
appropriate position, or removes it
@param AEnable A thousand separator is inserted if AEnable is true, or else
deleted.
-------------------------------------------------------------------------------}
procedure TsNumFormatParams.SetThousandSep(AEnable: Boolean);
var
section: TsNumFormatSection;
s, el: Integer;
replaced: Boolean;
begin
for s := 0 to High(Sections) do
begin
section := Sections[s];
replaced := false;
for el := High(section.Elements) downto 0 do
begin
if AEnable then
begin
if section.Elements[el].Token in [nftIntOptDigit, nftIntSpaceDigit, nftIntZeroDigit] then
begin
if replaced then
DeleteElement(s, el)
else begin
section.Elements[el].Token := nftIntTh;
Include(section.Kind, nfkHasThSep);
replaced := true;
end;
end;
end else
begin
if section.Elements[el].Token = nftIntTh then begin
section.Elements[el].Token := nftIntZeroDigit;
Exclude(section.Kind, nfkHasThSep);
break;
end;
end;
end;
end;
end;
{==============================================================================}
{ TsNumFormatList }
{==============================================================================}
{@@ ----------------------------------------------------------------------------
Constructor of the number format list class.
@param AFormatSettings Format settings needed internally by the number
format parser (currency symbol, etc.)
@param AOwnsData If true then the list is responsible to destroy
the list items
-------------------------------------------------------------------------------}
constructor TsNumFormatList.Create(AFormatSettings: TFormatSettings;
AOwnsData: Boolean);
begin
inherited Create;
FClass := TsNumFormatParams;
FFormatSettings := AFormatSettings;
FOwnsData := AOwnsData;
end;
{@@ ----------------------------------------------------------------------------
Destructor of the number format list class.
Clears the list items if the list "owns" the data.
-------------------------------------------------------------------------------}
destructor TsNumFormatList.Destroy;
begin
Clear;
inherited;
end;
{@@ ----------------------------------------------------------------------------
Adds the specified sections of a parsed number format to the list.
Duplicates are not checked before adding the format item.
@param ASections Array of number format sections as obtained by the
number format parser for a given format string
@return Index of the format item in the list.
-------------------------------------------------------------------------------}
function TsNumFormatList.AddFormat(ASections: TsNumFormatSections): Integer;
var
nfp: TsNumFormatParams;
begin
Result := Find(ASections);
if Result = -1 then begin
nfp := FClass.Create;
nfp.Sections := ASections;
Result := inherited Add(nfp);
end;
end;
{@@ ----------------------------------------------------------------------------
Adds a number format as specified by a format string to the list
Uses the number format parser to convert the format string to format sections
and elements.
Duplicates are not checked before adding the format item.
@param AFormatStr Excel-like format string describing the format to be added
@return Index of the format item in the list
-------------------------------------------------------------------------------}
function TsNumFormatList.AddFormat(AFormatStr: String): Integer;
var
parser: TsNumFormatParser;
newSections: TsNumFormatSections;
i: Integer;
begin
parser := TsNumFormatParser.Create(AFormatStr, FFormatSettings);
try
SetLength(newSections, parser.ParsedSectionCount);
for i:=0 to High(newSections) do
newSections[i] := parser.ParsedSections[i];
Result := AddFormat(newSections);
finally
parser.Free;
end;
end;
{@@ ----------------------------------------------------------------------------
Adds the number formats to the list which are built into the file format.
Does nothing here. Must be overridden by derived classes for each file format.
-------------------------------------------------------------------------------}
procedure TsNumFormatList.AddBuiltinFormats;
begin
end;
{@@ ----------------------------------------------------------------------------
Clears the list.
If the list "owns" the format items they are destroyed.
@see TsNumFormatList.Create
-------------------------------------------------------------------------------}
procedure TsNumFormatList.Clear;
var
i: Integer;
begin
for i := Count-1 downto 0 do Delete(i);
inherited;
end;
{@@ ----------------------------------------------------------------------------
Deletes the number format item having the specified index in the list.
If the list "owns" the format items, the item is destroyed.
@param AIndex Index of the format item to be deleted
@see TsNumformatList.Create
-------------------------------------------------------------------------------}
procedure TsNumFormatList.Delete(AIndex: Integer);
var
p: TsNumFormatParams;
begin
if FOwnsData then
begin
p := GetItem(AIndex);
if p <> nil then p.Free;
end;
inherited Delete(AIndex);
end;
{@@ ----------------------------------------------------------------------------
Checks whether a parsed format item having the specified format sections is
contained in the list and returns its index if found, or -1 if not found.
@param ASections Array of number format sections as obtained by the
number format parser for a given format string
@return Index of the found format item, or -1 if not found
-------------------------------------------------------------------------------}
function TsNumFormatList.Find(ASections: TsNumFormatSections): Integer;
var
nfp: TsNumFormatParams;
begin
for Result := 0 to Count-1 do begin
nfp := GetItem(Result);
if nfp.SectionsEqualTo(ASections) then
exit;
end;
Result := -1;
end;
{@@ ----------------------------------------------------------------------------
Checks whether a format item corresponding to the specified format string is
contained in the list and returns its index if found, or -1 if not.
Should be called before adding a format to the list to avoid duplicates.
@param AFormatStr Number format string of the format item which is seeked
@return Index of the found format item, or -1 if not found
@see TsNumFormatList.Add
-------------------------------------------------------------------------------}
function TsNumFormatList.Find(AFormatStr: String): Integer;
var
nfp: TsNumFormatParams;
begin
nfp := CreateNumFormatParams(AFormatStr, FFormatSettings);
if nfp = nil then
Result := -1
else
Result := Find(nfp.Sections);
end;
{@@ ----------------------------------------------------------------------------
Getter function returning the correct type of the list items
(i.e., TsNumFormatParams which are parsed format descriptions).
@param AIndex Index of the format item
@return Pointer to the list item at the specified index, cast to the type
TsNumFormatParams
-------------------------------------------------------------------------------}
function TsNumFormatList.GetItem(AIndex: Integer): TsNumFormatParams;
begin
Result := TsNumFormatParams(inherited Items[AIndex]);
end;
{@@ ----------------------------------------------------------------------------
Setter function for the list items
@param AIndex Index of the format item
@param AValue Pointer to the parsed format description to be stored in the
list at the specified index.
-------------------------------------------------------------------------------}
procedure TsNumFormatList.SetItem(AIndex: Integer;
const AValue: TsNumFormatParams);
begin
inherited Items[AIndex] := AValue;
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
begin
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];
nftTextFormat:
section^.Kind := section^.Kind + [nfkText];
end;
end; // for
if FStatus <> psOK then
exit;
if (section^.Kind * [nfkDate, nfkTime] <> []) and
(section^.Kind * [nfkPercent, nfkExp, nfkCurrency, nfkFraction] <> []) then
begin
FStatus := psErrNoValidDateTimeFormat;
exit;
end;
if (Length(FSections) = 1) and (section^.Kind = [nfkText]) then begin
section^.NumFormat := nfText;
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;
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;
n: Integer;
uch: Cardinal;
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;
uch := UTF8CharacterToUnicode(FCurrent, n);
if n > 1 then
begin
AddElement(nftEmptyCharWidth, UnicodeToUTF8(uch));
inc(FCurrent, n-1);
FToken := NextToken;
Continue;
end else
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
uch := UTF8CharacterToUnicode(FCurrent, n);
if n > 1 then
begin
AddElement(nftText, UnicodeToUTF8(uch));
inc(FCurrent, n-1);
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.