mirror of
https://github.com/doublecmd/doublecmd.git
synced 2026-06-21 09:58:13 +00:00
572 lines
20 KiB
ObjectPascal
572 lines
20 KiB
ObjectPascal
unit fpsCommon;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils;
|
|
|
|
const
|
|
{@@ These are some basic rgb color volues. FPSpreadsheet will support
|
|
only those built-in color constants originating in the EGA palette.
|
|
}
|
|
{@@ rgb value of @bold(black) color, BIFF2 palette index 0, BIFF8 index 8}
|
|
scBlack = $00000000;
|
|
{@@ rgb value of @bold(white) color, BIFF2 palette index 1, BIFF8 index 9 }
|
|
scWhite = $00FFFFFF;
|
|
{@@ rgb value of @bold(red) color, BIFF2 palette index 2, BIFF8 index 10 }
|
|
scRed = $000000FF;
|
|
{@@ rgb value of @bold(green) color, BIFF2 palette index 3, BIFF8 index 11 }
|
|
scGreen = $0000FF00;
|
|
{@@ rgb value of @bold(blue) color, BIFF2 palette index 4, BIFF8 indexes 12 and 39}
|
|
scBlue = $00FF0000;
|
|
{@@ rgb value of @bold(yellow) color, BIFF2 palette index 5, BIFF8 indexes 13 and 34}
|
|
scYellow = $0000FFFF;
|
|
{@@ rgb value of @bold(magenta) color, BIFF2 palette index 6, BIFF8 index 14 and 33}
|
|
scMagenta = $00FF00FF;
|
|
{@@ rgb value of @bold(cyan) color, BIFF2 palette index 7, BIFF8 indexes 15}
|
|
scCyan = $00FFFF00;
|
|
|
|
type
|
|
{@@ Colors in fpspreadsheet are given as rgb values in little-endian notation
|
|
(i.e. "r" is the low-value byte). The highest-value byte, if not zero,
|
|
indicates special colors.
|
|
|
|
@note(This byte order in TsColor is opposite to that in HTML colors.) }
|
|
TsColor = DWord;
|
|
|
|
{@@ Builtin number formats. Only uses a subset of the default formats,
|
|
enough to be able to read/write date/time values.
|
|
nfCustom allows to apply a format string directly. }
|
|
TsNumberFormat = (
|
|
// general-purpose for all numbers
|
|
nfGeneral,
|
|
// numbers
|
|
nfFixed, nfFixedTh, nfExp, nfPercentage, nfFraction,
|
|
// currency
|
|
nfCurrency, nfCurrencyRed,
|
|
// dates and times
|
|
nfShortDateTime, nfShortDate, nfLongDate, nfShortTime, nfLongTime,
|
|
nfShortTimeAM, nfLongTimeAM, nfDayMonth, nfMonthYear, nfTimeInterval,
|
|
// text
|
|
nfText,
|
|
// other (format string goes directly into the file)
|
|
nfCustom);
|
|
|
|
{@@ Ancestor of the fpSpreadsheet exceptions }
|
|
EFpSpreadsheet = class(Exception);
|
|
|
|
resourcestring
|
|
// Format
|
|
rsAmbiguousDecThouSeparator = 'Assuming usage of decimal separator in "%s".';
|
|
|
|
function Round(AValue: Double): Int64;
|
|
|
|
procedure FloatToFraction(AValue: Double; AMaxDenominator: Int64;
|
|
out ANumerator, ADenominator: Int64);
|
|
function TryStrToFloatAuto(AText: String; out ANumber: Double;
|
|
out ADecimalSeparator, AThousandSeparator: Char; out AWarning: String): Boolean;
|
|
|
|
procedure AddBuiltinBiffFormats(AList: TStringList;
|
|
AFormatSettings: TFormatSettings; ALastIndex: Integer);
|
|
|
|
procedure RegisterCurrency(ACurrencySymbol: String);
|
|
procedure RegisterCurrencies(AList: TStrings; AReplace: Boolean);
|
|
procedure UnregisterCurrency(ACurrencySymbol: String);
|
|
function CurrencyRegistered(ACurrencySymbol: String): Boolean;
|
|
procedure GetRegisteredCurrencies(AList: TStrings);
|
|
|
|
function IsNegative(var AText: String): Boolean;
|
|
function RemoveCurrencySymbol(ACurrencySymbol: String;
|
|
var AText: String): Boolean;
|
|
function TryStrToCurrency(AText: String; out ANumber: Double;
|
|
out ACurrencySymbol:String; const AFormatSettings: TFormatSettings): boolean;
|
|
|
|
|
|
implementation
|
|
|
|
uses
|
|
Math, fpsNumFormat;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Special rounding function which avoids banker's rounding
|
|
-------------------------------------------------------------------------------}
|
|
function Round(AValue: Double): Int64;
|
|
begin
|
|
if AValue > 0 then
|
|
Result := trunc(AValue + 0.5)
|
|
else
|
|
Result := trunc(AValue - 0.5);
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Approximates a floating point value as a fraction and returns the values of
|
|
numerator and denominator.
|
|
|
|
@param AValue Floating point value to be analyzed
|
|
@param AMaxDenominator Maximum value of the denominator allowed
|
|
@param ANumerator (out) Numerator of the best approximating fraction
|
|
@param ADenominator (out) Denominator of the best approximating fraction
|
|
-------------------------------------------------------------------------------}
|
|
procedure FloatToFraction(AValue: Double; AMaxDenominator: Int64;
|
|
out ANumerator, ADenominator: Int64);
|
|
// Uses method of continued fractions, adapted version from a function in
|
|
// Bart Broersma's fractions.pp unit:
|
|
// http://svn.code.sf.net/p/flyingsheep/code/trunk/ConsoleProjecten/fractions/
|
|
const
|
|
MaxInt64 = High(Int64);
|
|
MinInt64 = Low(Int64);
|
|
var
|
|
H1, H2, K1, K2, A, NewA, tmp, prevH1, prevK1: Int64;
|
|
B, test, diff, prevdiff: Double;
|
|
PendingOverflow: Boolean;
|
|
i: Integer = 0;
|
|
begin
|
|
if (AValue > MaxInt64) or (AValue < MinInt64) then
|
|
raise EFPSpreadsheet.Create('Range error');
|
|
|
|
if abs(AValue) < 0.5 / AMaxDenominator then
|
|
begin
|
|
ANumerator := 0;
|
|
ADenominator := AMaxDenominator;
|
|
exit;
|
|
end;
|
|
|
|
H1 := 1;
|
|
H2 := 0;
|
|
K1 := 0;
|
|
K2 := 1;
|
|
B := AValue;
|
|
NewA := Round(Floor(B));
|
|
prevH1 := H1;
|
|
prevK1 := K1;
|
|
prevdiff := 1E308;
|
|
repeat
|
|
inc(i);
|
|
A := NewA;
|
|
tmp := H1;
|
|
H1 := A * H1 + H2;
|
|
H2 := tmp;
|
|
tmp := K1;
|
|
K1 := A * K1 + K2;
|
|
K2 := tmp;
|
|
test := H1/K1;
|
|
diff := test - AValue;
|
|
{ Use the previous result if the denominator becomes larger than the allowed
|
|
value, or if the difference becomes worse because the "best" result has
|
|
been missed due to rounding error - this is more stable than using a
|
|
predefined precision in comparing diff with zero. }
|
|
if (abs(K1) >= AMaxDenominator) or (abs(diff) > abs(prevdiff)) then
|
|
begin
|
|
H1 := prevH1;
|
|
K1 := prevK1;
|
|
break;
|
|
end;
|
|
if (Abs(B - A) < 1E-30) then
|
|
B := 1E30 //happens when H1/K1 exactly matches Value
|
|
else
|
|
B := 1 / (B - A);
|
|
PendingOverFlow := (B * H1 + H2 > MaxInt64) or
|
|
(B * K1 + K2 > MaxInt64) or
|
|
(B > MaxInt64);
|
|
if not PendingOverflow then
|
|
NewA := Round(Floor(B));
|
|
prevH1 := H1;
|
|
prevK1 := K1;
|
|
prevdiff := diff;
|
|
until PendingOverflow;
|
|
ANumerator := H1;
|
|
ADenominator := K1;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Converts a string to a floating point number. No assumption on decimal and
|
|
thousand separator are made.
|
|
|
|
Is needed for reading CSV files.
|
|
-------------------------------------------------------------------------------}
|
|
function TryStrToFloatAuto(AText: String; out ANumber: Double;
|
|
out ADecimalSeparator, AThousandSeparator: Char; out AWarning: String): Boolean;
|
|
var
|
|
i: Integer;
|
|
testSep: Char;
|
|
testSepPos: Integer;
|
|
lastDigitPos: Integer;
|
|
isPercent: Boolean;
|
|
fs: TFormatSettings;
|
|
done: Boolean;
|
|
begin
|
|
Result := false;
|
|
AWarning := '';
|
|
ADecimalSeparator := #0;
|
|
AThousandSeparator := #0;
|
|
if AText = '' then
|
|
exit;
|
|
|
|
fs := DefaultFormatSettings;
|
|
|
|
// We scan the string starting from its end. If we find a point or a comma,
|
|
// we have a candidate for the decimal or thousand separator. If we find
|
|
// the same character again it was a thousand separator, if not it was
|
|
// a decimal separator.
|
|
|
|
// There is one amgiguity: Using a thousand separator for number < 1.000.000,
|
|
// but no decimal separator misinterprets the thousand separator as a
|
|
// decimal separator.
|
|
|
|
done := false; // Indicates that both decimal and thousand separators are found
|
|
testSep := #0; // Separator candidate to be tested
|
|
testSepPos := 0; // Position of this separator candidate in the string
|
|
lastDigitPos := 0; // Position of the last numerical digit
|
|
isPercent := false; // Flag for percentage format
|
|
|
|
i := Length(AText); // Start at end...
|
|
while i >= 1 do // ...and search towards start
|
|
begin
|
|
case AText[i] of
|
|
'0'..'9':
|
|
if (lastDigitPos = 0) and (AText[i] in ['0'..'9']) then
|
|
lastDigitPos := i;
|
|
|
|
'e', 'E':
|
|
;
|
|
|
|
'%':
|
|
begin
|
|
isPercent := true;
|
|
// There may be spaces before the % sign which we don't want
|
|
dec(i);
|
|
while (i >= 1) do
|
|
if AText[i] = ' ' then
|
|
dec(i)
|
|
else
|
|
begin
|
|
inc(i);
|
|
break;
|
|
end;
|
|
end;
|
|
|
|
'+', '-':
|
|
;
|
|
|
|
'.', ',':
|
|
begin
|
|
if testSep = #0 then begin
|
|
testSep := AText[i];
|
|
testSepPos := i;
|
|
end;
|
|
// This is the right-most separator candidate in the text
|
|
// It can be a decimal or a thousand separator.
|
|
// Therefore, we continue searching from here.
|
|
dec(i);
|
|
while i >= 1 do
|
|
begin
|
|
if not (AText[i] in ['0'..'9', '+', '-', '.', ',']) then
|
|
exit;
|
|
|
|
// If we find the testSep character again it must be a thousand separator,
|
|
// and there are no decimals.
|
|
if (AText[i] = testSep) then
|
|
begin
|
|
// ... but only if there are 3 numerical digits in between
|
|
if (testSepPos - i = 4) then
|
|
begin
|
|
fs.ThousandSeparator := testSep;
|
|
// The decimal separator is the "other" character.
|
|
if testSep = '.' then
|
|
fs.DecimalSeparator := ','
|
|
else
|
|
fs.DecimalSeparator := '.';
|
|
AThousandSeparator := fs.ThousandSeparator;
|
|
ADecimalSeparator := #0; // this indicates that there are no decimals
|
|
done := true;
|
|
i := 0;
|
|
end else
|
|
begin
|
|
Result := false;
|
|
exit;
|
|
end;
|
|
end
|
|
else
|
|
// If we find the "other" separator character, then testSep was a
|
|
// decimal separator and the current character is a thousand separator.
|
|
// But there must be 3 digits in between.
|
|
if AText[i] in ['.', ','] then
|
|
begin
|
|
if testSepPos - i <> 4 then // no 3 digits in between --> no number, maybe a date.
|
|
exit;
|
|
fs.DecimalSeparator := testSep;
|
|
fs.ThousandSeparator := AText[i];
|
|
ADecimalSeparator := fs.DecimalSeparator;
|
|
AThousandSeparator := fs.ThousandSeparator;
|
|
done := true;
|
|
i := 0;
|
|
end;
|
|
dec(i);
|
|
end;
|
|
end;
|
|
|
|
else
|
|
exit; // Non-numeric character found, no need to continue
|
|
|
|
end;
|
|
dec(i);
|
|
end;
|
|
|
|
// Only one separator candicate found, we assume it is a decimal separator
|
|
if (testSep <> #0) and not done then
|
|
begin
|
|
// Warning in case of ambiguous detection of separator. If only one separator
|
|
// type is found and it is at the third position from the string's end it
|
|
// might by a thousand separator or a decimal separator. We assume the
|
|
// latter case, but create a warning.
|
|
if (lastDigitPos - testSepPos = 3) and not isPercent then
|
|
AWarning := Format(rsAmbiguousDecThouSeparator, [AText]);
|
|
fs.DecimalSeparator := testSep;
|
|
ADecimalSeparator := fs.DecimalSeparator;
|
|
// Make sure that the thousand separator is different from the decimal sep.
|
|
if testSep = '.' then fs.ThousandSeparator := ',' else fs.ThousandSeparator := '.';
|
|
end;
|
|
|
|
// Delete all thousand separators from the string - StrToFloat does not like them...
|
|
AText := StringReplace(AText, fs.ThousandSeparator, '', [rfReplaceAll]);
|
|
|
|
// Is the last character a percent sign?
|
|
if isPercent then
|
|
while (Length(AText) > 0) and (AText[Length(AText)] in ['%', ' ']) do
|
|
Delete(AText, Length(AText), 1);
|
|
|
|
// Try string-to-number conversion
|
|
Result := TryStrToFloat(AText, ANumber, fs);
|
|
|
|
// If successful ...
|
|
if Result then
|
|
begin
|
|
// ... take care of the percentage sign
|
|
if isPercent then
|
|
ANumber := ANumber * 0.01;
|
|
end;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
These are the built-in number formats as expected in the biff spreadsheet file.
|
|
In BIFF5+ they are not written to file but they are used for lookup of the
|
|
number format that Excel used.
|
|
-------------------------------------------------------------------------------}
|
|
procedure AddBuiltinBiffFormats(AList: TStringList;
|
|
AFormatSettings: TFormatSettings; ALastIndex: Integer);
|
|
var
|
|
fs: TFormatSettings absolute AFormatSettings;
|
|
cs: String;
|
|
i: Integer;
|
|
begin
|
|
cs := fs.CurrencyString;
|
|
AList.Clear;
|
|
AList.Add(''); // 0
|
|
AList.Add('0'); // 1
|
|
AList.Add('0.00'); // 2
|
|
AList.Add('#,##0'); // 3
|
|
AList.Add('#,##0.00'); // 4
|
|
AList.Add(BuildCurrencyFormatString(nfCurrency, fs, 0, fs.CurrencyFormat, fs.NegCurrFormat, cs)); // 5
|
|
AList.Add(BuildCurrencyFormatString(nfCurrencyRed, fs, 0, fs.CurrencyFormat, fs.NegCurrFormat, cs)); // 6
|
|
AList.Add(BuildCurrencyFormatString(nfCurrency, fs, 2, fs.CurrencyFormat, fs.NegCurrFormat, cs)); // 7
|
|
AList.Add(BuildCurrencyFormatString(nfCurrencyRed, fs, 2, fs.CurrencyFormat, fs.NegCurrFormat, cs)); // 8
|
|
AList.Add('0%'); // 9
|
|
AList.Add('0.00%'); // 10
|
|
AList.Add('0.00E+00'); // 11
|
|
AList.Add('# ?/?'); // 12
|
|
AList.Add('# ??/??'); // 13
|
|
AList.Add(BuildDateTimeFormatString(nfShortDate, fs)); // 14
|
|
AList.Add(BuildDateTimeFormatString(nfLongdate, fs)); // 15
|
|
AList.Add(BuildDateTimeFormatString(nfDayMonth, fs)); // 16: 'd/mmm'
|
|
AList.Add(BuildDateTimeFormatString(nfMonthYear, fs)); // 17: 'mmm/yy'
|
|
AList.Add(BuildDateTimeFormatString(nfShortTimeAM, fs)); // 18
|
|
AList.Add(BuildDateTimeFormatString(nfLongTimeAM, fs)); // 19
|
|
AList.Add(BuildDateTimeFormatString(nfShortTime, fs)); // 20
|
|
AList.Add(BuildDateTimeFormatString(nfLongTime, fs)); // 21
|
|
AList.Add(BuildDateTimeFormatString(nfShortDateTime, fs)); // 22
|
|
for i:=23 to 36 do
|
|
AList.Add(''); // not supported
|
|
AList.Add('_(#,##0_);(#,##0)'); // 37
|
|
AList.Add('_(#,##0_);[Red](#,##0)'); // 38
|
|
AList.Add('_(#,##0.00_);(#,##0.00)'); // 39
|
|
AList.Add('_(#,##0.00_);[Red](#,##0.00)'); // 40
|
|
AList.Add('_("'+cs+'"* #,##0_);_("'+cs+'"* (#,##0);_("'+cs+'"* "-"_);_(@_)'); // 41
|
|
AList.Add('_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)'); // 42
|
|
AList.Add('_("'+cs+'"* #,##0.00_);_("'+cs+'"* (#,##0.00);_("'+cs+'"* "-"??_);_(@_)'); // 43
|
|
AList.Add('_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)'); // 44
|
|
AList.Add('nn:ss'); // 45
|
|
AList.Add('[h]:nn:ss'); // 46
|
|
AList.Add('nn:ss.z'); // 47
|
|
AList.Add('##0.0E+00'); // 48
|
|
AList.Add('@'); // 49 "Text" format
|
|
for i:=50 to ALastIndex do AList.Add(''); // not supported/used
|
|
end;
|
|
|
|
var
|
|
CurrencyList: TStrings = nil;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Registers a currency symbol UTF8 string for usage by fpspreadsheet
|
|
|
|
Currency symbols are the key for detection of currency values. In order to
|
|
reckognize strings are currency symbols they have to be registered in the
|
|
internal CurrencyList.
|
|
|
|
Registration occurs automatically for USD, "$", the currencystring defined
|
|
in the DefaultFormatSettings and for the currency symbols used explicitly
|
|
when calling WriteCurrency or WriteNumerFormat.
|
|
-------------------------------------------------------------------------------}
|
|
procedure RegisterCurrency(ACurrencySymbol: String);
|
|
begin
|
|
if not CurrencyRegistered(ACurrencySymbol) and (ACurrencySymbol <> '') then
|
|
CurrencyList.Add(ACurrencySymbol);
|
|
end;
|
|
|
|
{@@ RegisterCurrencies registers the currency strings contained in the string list
|
|
If AReplace is true, the list replaces the currently registered list.
|
|
-------------------------------------------------------------------------------}
|
|
procedure RegisterCurrencies(AList: TStrings; AReplace: Boolean);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if AList = nil then
|
|
exit;
|
|
|
|
if AReplace then CurrencyList.Clear;
|
|
for i:=0 to AList.Count-1 do
|
|
RegisterCurrency(AList[i]);
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Removes registration of a currency symbol string for usage by fpspreadsheet
|
|
-------------------------------------------------------------------------------}
|
|
procedure UnregisterCurrency(ACurrencySymbol: String);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
i := CurrencyList.IndexOf(ACurrencySymbol);
|
|
if i <> -1 then CurrencyList.Delete(i);
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Checks whether a string is registered as valid currency symbol string
|
|
-------------------------------------------------------------------------------}
|
|
function CurrencyRegistered(ACurrencySymbol: String): Boolean;
|
|
begin
|
|
Result := CurrencyList.IndexOf(ACurrencySymbol) <> -1;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes all registered currency symbols to a string list
|
|
-------------------------------------------------------------------------------}
|
|
procedure GetRegisteredCurrencies(AList: TStrings);
|
|
begin
|
|
AList.Clear;
|
|
AList.Assign(CurrencyList);
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Checks whether the given number string is a negative value. In case of
|
|
currency value, this can be indicated by brackets, or a minus sign at string
|
|
start or end.
|
|
-------------------------------------------------------------------------------}
|
|
function IsNegative(var AText: String): Boolean;
|
|
begin
|
|
Result := false;
|
|
if AText = '' then
|
|
exit;
|
|
if (AText[1] = '(') and (AText[Length(AText)] = ')') then
|
|
begin
|
|
Result := true;
|
|
Delete(AText, 1, 1);
|
|
Delete(AText, Length(AText), 1);
|
|
AText := Trim(AText);
|
|
end else
|
|
if (AText[1] = '-') then
|
|
begin
|
|
Result := true;
|
|
Delete(AText, 1, 1);
|
|
AText := Trim(AText);
|
|
end else
|
|
if (AText[Length(AText)] = '-') then
|
|
begin
|
|
Result := true;
|
|
Delete(AText, Length(AText), 1);
|
|
AText := Trim(AText);
|
|
end;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Checks wheter a specified currency symbol is contained in a string, removes
|
|
the currency symbol and returns the remaining string.
|
|
-------------------------------------------------------------------------------}
|
|
function RemoveCurrencySymbol(ACurrencySymbol: String; var AText: String): Boolean;
|
|
var
|
|
p: Integer;
|
|
begin
|
|
p := pos(ACurrencySymbol, AText);
|
|
if p > 0 then
|
|
begin
|
|
Delete(AText, p, Length(ACurrencySymbol));
|
|
AText := Trim(AText);
|
|
Result := true;
|
|
end else
|
|
Result := false;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Checks whether a string is a number with attached currency symbol. Looks also
|
|
for negative values in brackets.
|
|
-------------------------------------------------------------------------------}
|
|
function TryStrToCurrency(AText: String; out ANumber: Double;
|
|
out ACurrencySymbol:String; const AFormatSettings: TFormatSettings): boolean;
|
|
var
|
|
i: Integer;
|
|
s: String;
|
|
isNeg: Boolean;
|
|
begin
|
|
Result := false;
|
|
ANumber := 0.0;
|
|
ACurrencySymbol := '';
|
|
|
|
// Check the text for the presence of each known curreny symbol
|
|
for i:= 0 to CurrencyList.Count-1 do
|
|
begin
|
|
// Store string in temporary variable since it will be modified
|
|
s := AText;
|
|
// Check for this currency sign being contained in the string, remove it if found.
|
|
if RemoveCurrencySymbol(CurrencyList[i], s) then
|
|
begin
|
|
// Check for negative signs and remove them, but keep this information
|
|
isNeg := IsNegative(s);
|
|
// Try to convert remaining string to number
|
|
if TryStrToFloat(s, ANumber, AFormatSettings) then begin
|
|
// if successful: take care of negative values
|
|
if isNeg then ANumber := -ANumber;
|
|
ACurrencySymbol := CurrencyList[i];
|
|
Result := true;
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
initialization
|
|
// Known currency symbols
|
|
CurrencyList := TStringList.Create;
|
|
with TStringList(CurrencyList) do
|
|
begin
|
|
CaseSensitive := false;
|
|
Duplicates := dupIgnore;
|
|
end;
|
|
RegisterCurrency('USD');
|
|
RegisterCurrency('$');
|
|
RegisterCurrency(AnsiToUTF8(DefaultFormatSettings.CurrencyString));
|
|
|
|
finalization
|
|
FreeAndNil(CurrencyList);
|
|
|
|
end.
|
|
|