|
@@ -0,0 +1,4028 @@
|
|
|
+{@@ ----------------------------------------------------------------------------
|
|
|
+ Unit @bold(fpsNumFormat) contains classes and procedures to analyze and process
|
|
|
+ @bold(number formats).
|
|
|
+
|
|
|
+ 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,
|
|
|
+ fpscommon;
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+ @member Token Identifies the number format element
|
|
|
+ @member IntValue Integer value associated with the number format element
|
|
|
+ @member FloatValue Floating point value associated with the number format element
|
|
|
+ @member TextValue String value associated with the number format element }
|
|
|
+ TsNumFormatElement = record
|
|
|
+ Token: TsNumFormatToken;
|
|
|
+ IntValue: Integer;
|
|
|
+ FloatValue: Double;
|
|
|
+ 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;
|
|
|
+ {@@ Minimum number of digits before the decimal separator }
|
|
|
+ MinIntDigits: 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 value to a number or date/time string. These data are created
|
|
|
+ by the number format parser from a format string. }
|
|
|
+ TsNumFormatParams = class(TObject)
|
|
|
+ private
|
|
|
+ FAllowLocalizedAMPM: Boolean;
|
|
|
+ protected
|
|
|
+ function GetNumFormat: TsNumberFormat; virtual;
|
|
|
+ function GetNumFormatStr: String; virtual;
|
|
|
+ public
|
|
|
+ {@@ Array of the format sections }
|
|
|
+ Sections: TsNumFormatSections;
|
|
|
+ constructor Create;
|
|
|
+ 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 AllowLocalizedAMPM: boolean read FAllowLocalizedAMPM write FAllowLocalizedAMPM;
|
|
|
+ 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;
|
|
|
+ AMinIntDigits: Integer = 1): String;
|
|
|
+
|
|
|
+function BuildFormatStringFromSection(const ASection: TsNumFormatSection;
|
|
|
+ AllowLocalizedAMPM: Boolean = true): 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;
|
|
|
+
|
|
|
+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
|
|
|
+ // fpsUtils.Round() avoids Banker's rounding
|
|
|
+ Result := ProcessIntegerFormat(IntToStr(fpsCommon.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
|
|
|
+ @returns Converted string
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+function ConvertFloatToStr(AValue: Double; AParams: TsNumFormatParams;
|
|
|
+ AFormatSettings: TFormatSettings): String;
|
|
|
+
|
|
|
+ { Returns true if s represent the value 0; it can be written in various
|
|
|
+ ways: '0', '0.00', '0,000.0', '0.00E+10' etc. }
|
|
|
+ function IsZeroStr(s: String): Boolean;
|
|
|
+ var
|
|
|
+ i: Integer;
|
|
|
+ begin
|
|
|
+ Result := false;
|
|
|
+ for i:=1 to Length(s) do
|
|
|
+ case s[i] of
|
|
|
+ 'e', 'E': break;
|
|
|
+ '1'..'9': exit;
|
|
|
+ end;
|
|
|
+ Result := true;
|
|
|
+ end;
|
|
|
+
|
|
|
+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 and not IsZeroStr(s) 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(round(ms/100));
|
|
|
+ 2: Result := Result + Format('%.2d', [round(ms/10)]);
|
|
|
+ 3: Result := Result + Format('%.3d', [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, 15, 15, AFormatSettings);
|
|
|
+ // 15 is for best rounding results.
|
|
|
+ // Note: Still more than Excel whichrounds pi to 9 digits only.
|
|
|
+end;
|
|
|
+
|
|
|
+
|
|
|
+{==============================================================================}
|
|
|
+{ Utility functions }
|
|
|
+{==============================================================================}
|
|
|
+
|
|
|
+{@@ ----------------------------------------------------------------------------
|
|
|
+ Adds an AM/PM format code to a pre-built time formatting string.
|
|
|
+
|
|
|
+ Example: ATimeFormatString = 'hh:nn' ==> 'hh:nn AM/PM'
|
|
|
+
|
|
|
+ @param ATimeFormatString String of time formatting codes (such as 'hh:nn')
|
|
|
+ @param AFormatSettings FormatSettings for locale-dependent information
|
|
|
+ @returns Formatting string with AM/PM option activated.
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+function AddAMPM(const ATimeFormatString: String;
|
|
|
+ const AFormatSettings: TFormatSettings): String;
|
|
|
+begin
|
|
|
+ Result := Format('%s AM/PM', [StripAMPM(ATimeFormatString)]);
|
|
|
+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
|
|
|
+ @returns 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.
|
|
|
+
|
|
|
+ Example: '"$"#,##0.00;("$"#,##0.00);"$"0.00'
|
|
|
+
|
|
|
+ @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
|
|
|
+
|
|
|
+ @returns String of formatting codes
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+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.
|
|
|
+ @returns 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.
|
|
|
+
|
|
|
+ @returns 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.
|
|
|
+
|
|
|
+ Example: ANumberFormat = nfFixedTh, ADecimals = 2 --> '#,##0.00'
|
|
|
+
|
|
|
+ @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.
|
|
|
+ @param AMinIntDigits Minimum count of integer digits, i.e. count of '0' in the format string before the decimal separator
|
|
|
+
|
|
|
+ @returns String of formatting codes
|
|
|
+
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+function BuildNumberFormatString(ANumberFormat: TsNumberFormat;
|
|
|
+ const AFormatSettings: TFormatSettings; ADecimals: Integer = -1;
|
|
|
+ AMinIntDigits: Integer = 1): String;
|
|
|
+var
|
|
|
+ decdigits: String;
|
|
|
+ intdigits: String;
|
|
|
+begin
|
|
|
+ Result := '';
|
|
|
+ if AMinIntDigits > 0 then
|
|
|
+ intdigits := DupeString('0', AMinIntDigits)
|
|
|
+ else
|
|
|
+ intdigits := '#';
|
|
|
+ if ADecimals = -1 then
|
|
|
+ ADecimals := AFormatSettings.CurrencyDecimals;
|
|
|
+ if ADecimals > 0 then
|
|
|
+ decdigits := '.' + DupeString('0', ADecimals)
|
|
|
+ else
|
|
|
+ decdigits := '';
|
|
|
+ case ANumberFormat of
|
|
|
+ nfText:
|
|
|
+ Result := '@';
|
|
|
+ nfFixed:
|
|
|
+ Result := intdigits + decdigits;
|
|
|
+ nfFixedTh:
|
|
|
+ begin
|
|
|
+ while Length(IntDigits) < 4 do intDigits := '#' + intdigits;
|
|
|
+ System.Insert(',', intdigits, Length(intdigits)-2);
|
|
|
+ Result := intdigits + decdigits;
|
|
|
+ end;
|
|
|
+ nfExp:
|
|
|
+ Result := intdigits + decdigits + 'E+00';
|
|
|
+ nfPercentage:
|
|
|
+ Result := intdigits + decdigits + '%';
|
|
|
+ nfFraction:
|
|
|
+ if ADecimals = 0 then // "ADecimals" has a different meaning here...
|
|
|
+ Result := '# ??/??' // This is the default fraction format
|
|
|
+ else
|
|
|
+ begin
|
|
|
+ decdigits := DupeString('?', ADecimals);
|
|
|
+ Result := '# ' + decdigits + '/' + decdigits;
|
|
|
+ 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 EFPSpreadsheet.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
|
|
|
+ @param AllowLocalizedAMPM Replaces "AMPM" in a time format string by "AM/PM". "AMPM" is allowed by FPS, but not by Excel. When converting a time to string it is replaced by the localized strings FormatSettings.TimeAMString/.TimePMString.
|
|
|
+
|
|
|
+ @returns Excel-compatible format string
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+function BuildFormatStringFromSection(const ASection: TsNumFormatSection;
|
|
|
+ AllowLocalizedAMPM: Boolean = true): 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);
|
|
|
+ nftAMPM:
|
|
|
+ if Lowercase(element.TextValue) = 'ampm' then
|
|
|
+ Result := Result + 'AM/PM'
|
|
|
+ else if element.TextValue <> '' then
|
|
|
+ Result := Result + element.TextValue;
|
|
|
+ nftSign, nftSignBracket, nftExpChar, nftExpSign, 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 '#'.
|
|
|
+ @returns Count of decimal places found
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+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
|
|
|
+ @returns @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
|
|
|
+ @returns @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
|
|
|
+ @returns @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.
|
|
|
+ @returns @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
|
|
|
+ @returns @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
|
|
|
+ @returns @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
|
|
|
+ @returns @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
|
|
|
+ @returns @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
|
|
|
+ @returns @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'
|
|
|
+ @returns 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'
|
|
|
+ @returns 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')
|
|
|
+ @returns 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 }
|
|
|
+{==============================================================================}
|
|
|
+
|
|
|
+constructor TsNumFormatParams.Create;
|
|
|
+begin
|
|
|
+ inherited;
|
|
|
+ FAllowLocalizedAMPM := true;
|
|
|
+end;
|
|
|
+
|
|
|
+{@@ ----------------------------------------------------------------------------
|
|
|
+ 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
|
|
|
+
|
|
|
+ @returns Built-in number format identifer if the format is built into fpspreadsheet, or nfCustom otherwise
|
|
|
+ @seeAlso TsNumberFormat
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+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.
|
|
|
+
|
|
|
+ @returns 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], FAllowLocalizedAMPM);
|
|
|
+ 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
|
|
|
+
|
|
|
+ @seeAlso 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
|
|
|
+ @returns 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
|
|
|
+ @returns Index of the format item in the list
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+function TsNumFormatList.AddFormat(AFormatStr: String): Integer;
|
|
|
+var
|
|
|
+ parser: TsNumFormatParser;
|
|
|
+ newSections: TsNumFormatSections = nil;
|
|
|
+ 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.
|
|
|
+
|
|
|
+ @seeAlso 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
|
|
|
+ @seeAlso 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
|
|
|
+ @returns 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
|
|
|
+ @returns Index of the found format item, or -1 if not found
|
|
|
+ @seeAlso TsNumFormatList.AddFormat
|
|
|
+-------------------------------------------------------------------------------}
|
|
|
+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., @link(TsNumFormatParams) which are parsed format descriptions).
|
|
|
+
|
|
|
+ @param AIndex Index of the format item
|
|
|
+ @returns Pointer to the list item at the specified index, cast to the type @link(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:
|
|
|
+ begin
|
|
|
+ section^.MinIntDigits := section^.Elements[el].IntValue;
|
|
|
+ i := section^.Elements[el].IntValue;
|
|
|
+ end;
|
|
|
+ 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];
|
|
|
+ if section^.Elements[el].Token = nftMilliseconds then
|
|
|
+ section^.Decimals := section^.Elements[el].IntValue
|
|
|
+ else
|
|
|
+ section^.Decimals := 0;
|
|
|
+ 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 built-in 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
|
|
|
+ {
|
|
|
+ AddElement(nftDecSep, FToken);
|
|
|
+ FToken := NextToken;
|
|
|
+ if FToken in ['z', 'Z', '0'] then
|
|
|
+ begin
|
|
|
+ ScanAndCount(FToken, n);
|
|
|
+ AddElement(nftMilliseconds, n);
|
|
|
+ end;
|
|
|
+ }
|
|
|
+
|
|
|
+ token := NextToken;
|
|
|
+ if token in ['z', 'Z', '0'] then begin
|
|
|
+ AddElement(nftDecSep, FToken);
|
|
|
+ FToken := NextToken;
|
|
|
+ if FToken in ['z', 'Z', '0'] then
|
|
|
+ ScanAndCount(FToken, n)
|
|
|
+ else
|
|
|
+ n := 0;
|
|
|
+ AddElement(nftMilliseconds, n+1);
|
|
|
+ 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); // wp: Why Unicode ???
|
|
|
+ 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', 'H', 'N', 'n', 's':
|
|
|
+ ScanDateTime;
|
|
|
+ '[':
|
|
|
+ ScanBrackets;
|
|
|
+ '%':
|
|
|
+ AddElement(nftPercent, FToken);
|
|
|
+ ' ':
|
|
|
+ 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.
|