In Delphi1, using FloatToStrF
or CurrToStrF
will automatically use the DecimalSeparator
character to represent a decimal mark. Unfortunately DecimalSeparator
is declared in SysUtils as Char
1,2:
var
DecimalSeparator: Char;
While the LOCALE_SDECIMAL
is allowed to be up to three characters:
Character(s) used for the decimal separator, for example, "." in "3.14" or "," in "3,14". The maximum number of characters allowed for this string is four, including a terminating null character.
This causes Delphi to fail to read the decimal separator correctly; falling back to assume a default decimal separator of ".
":
DecimalSeparator := GetLocaleChar(DefaultLCID, LOCALE_SDECIMAL, '.');
On my computer, which is quite a character, this cause floating point and currency values to be incorrectly localized with a U+002E (full stop) decimal mark.
i am willing to call the Windows API functions directly, which are designed to convert floating point, or currency, values into a localized string:
GetNumberFormat
GetCurrencyFormat
Except these functions take a string of picture codes, where the only characters allowed are:
U+0030
..U+0039
).
) if the number is a floating-point value (U+002E
)U+002D
)What would be a good way1 to convert a floating point, or currency, value to a string that obeys those rules? e.g.
1234567.893332
-1234567
given that the local user's locale (i.e. my computer):
-
to indicate negative (e.g. --
).
to indicate a decimal point (e.g. ,,
)0123456789
to represent digits (e.g. [removed arabic digits that crash SO javascript parser])A horrible, horrible, hack, which i could use:
function FloatToLocaleIndependantString(const v: Extended): string;
var
oldDecimalSeparator: Char;
begin
oldDecimalSeparator := SysUtils.DecimalSeparator;
SysUtils.DecimalSeparator := '.'; //Windows formatting functions assume single decimal point
try
Result := FloatToStrF(Value, ffFixed,
18, //Precision: "should be 18 or less for values of type Extended"
9 //Scale 0..18. Sure...9 digits before decimal mark, 9 digits after. Why not
);
finally
SysUtils.DecimalSeparator := oldDecimalSeparator;
end;
end;
Additional info on the chain of functions the VCL uses:
FloatToStrF
and CurrToStrF
calls:
FloatToText
calls:
FloatToDecimal
Note
DecimalSeparator: Char
, the single character global is deprecated, and replaced with another single character decimal separator1 in my version of Delphi
2 and in current versions of Delphi
We can convert a string to float in Python using the float() function. This is a built-in function used to convert an object to a floating point number. Internally, the float() function calls specified object __float__() function.
The "C" (or currency) format specifier is used to convert a number to a string representing a currency amount. Let us see an example. double value = 139.87; Now to display the above number until three decimal places, use (“C3”) currency format specifier.
Ok, this may not be what you want, but it works with D2007 and up. Thread safe and all.
uses Windows,SysUtils;
var
myGlobalFormatSettings : TFormatSettings;
// Initialize special format settings record
GetLocaleFormatSettings( 0,myGlobalFormatSettings);
myGlobalFormatSettings.DecimalSeparator := '.';
function FloatToLocaleIndependantString(const value: Extended): string;
begin
Result := FloatToStrF(Value, ffFixed,
18, //Precision: "should be 18 or less for values of type Extended"
9, //Scale 0..18. Sure...9 digits before decimal mark, 9 digits after. Why not
myGlobalFormatSettings
);
end;
Delphi does provide a procedure called FloatToDecimal
that converts floating point (e.g. Extended
) and Currency
values into a useful structure for further formatting. e.g.:
FloatToDecimal(..., 1234567890.1234, ...);
gives you:
TFloatRec
Digits: array[0..20] of Char = "12345678901234"
Exponent: SmallInt = 10
IsNegative: Boolean = True
Where Exponent
gives the number of digits to the left of decimal point.
There are some special cases to be handled:
Exponent is zero
Digits: array[0..20] of Char = "12345678901234"
Exponent: SmallInt = 0
IsNegative: Boolean = True
means there are no digits to the left of the decimal point, e.g. .12345678901234
Exponent is negative
Digits: array[0..20] of Char = "12345678901234"
Exponent: SmallInt = -3
IsNegative: Boolean = True
means you have to place zeros in between the decimal point and the first digit, e.g. .00012345678901234
Exponent is -32768
(NaN, not a number)
Digits: array[0..20] of Char = ""
Exponent: SmallInt = -32768
IsNegative: Boolean = False
means the value is Not a Number, e.g. NAN
Exponent is 32767
(INF, or -INF)
Digits: array[0..20] of Char = ""
Exponent: SmallInt = 32767
IsNegative: Boolean = False
means the value is either positive or negative infinity (depending on the IsNegative
value), e.g. -INF
We can use FloatToDecimal
as a starting point to create a locale-independent string of "pictures codes".
This string can then be passed to appropriate Windows GetNumberFormat
or GetCurrencyFormat
functions to perform the actual correct localization.
i wrote my own CurrToDecimalString
and FloatToDecimalString
which convert numbers into the required locale independent format:
class function TGlobalization.CurrToDecimalString(const Value: Currency): string;
var
digits: string;
s: string;
floatRec: TFloatRec;
begin
FloatToDecimal({var}floatRec, Value, fvCurrency, 0{ignored for currency types}, 9999);
//convert the array of char into an easy to access string
digits := PChar(Addr(floatRec.Digits[0]));
if floatRec.Exponent > 0 then
begin
//Check for positive or negative infinity (exponent = 32767)
if floatRec.Exponent = 32767 then //David Heffernan says that currency can never be infinity. Even though i can't test it, i can at least try to handle it
begin
if floatRec.Negative = False then
Result := 'INF'
else
Result := '-INF';
Exit;
end;
{
digits: 1234567 89
exponent--------^ 7=7 digits on left of decimal mark
}
s := Copy(digits, 1, floatRec.Exponent);
{
for the value 10000:
digits: "1"
exponent: 5
Add enough zero's to digits to pad it out to exponent digits
}
if Length(s) < floatRec.Exponent then
s := s+StringOfChar('0', floatRec.Exponent-Length(s));
if Length(digits) > floatRec.Exponent then
s := s+'.'+Copy(digits, floatRec.Exponent+1, 20);
end
else if floatRec.Exponent < 0 then
begin
//check for NaN (Exponent = -32768)
if floatRec.Exponent = -32768 then //David Heffernan says that currency can never be NotANumber. Even though i can't test it, i can at least try to handle it
begin
Result := 'NAN';
Exit;
end;
{
digits: .000123456789
^---------exponent
}
//Add zero, or more, "0"'s to the left
s := '0.'+StringOfChar('0', -floatRec.Exponent)+digits;
end
else
begin
{
Exponent is zero.
digits: .123456789
^
}
if length(digits) > 0 then
s := '0.'+digits
else
s := '0';
end;
if floatRec.Negative then
s := '-'+s;
Result := s;
end;
Aside from the edge cases of NAN
, INF
and -INF
, i can now pass these strings to Windows:
class function TGlobalization.GetCurrencyFormat(const DecimalString: WideString; const Locale: LCID): WideString;
var
cch: Integer;
ValueStr: WideString;
begin
Locale
LOCALE_INVARIANT
LOCALE_USER_DEFAULT <--- use this one (windows.pas)
LOCALE_SYSTEM_DEFAULT
LOCALE_CUSTOM_DEFAULT (Vista and later)
LOCALE_CUSTOM_UI_DEFAULT (Vista and later)
LOCALE_CUSTOM_UNSPECIFIED (Vista and later)
}
cch := Windows.GetCurrencyFormatW(Locale, 0, PWideChar(DecimalString), nil, nil, 0);
if cch = 0 then
RaiseLastWin32Error;
SetLength(ValueStr, cch);
cch := Windows.GetCurrencyFormatW(Locale, 0, PWideChar(DecimalString), nil, PWideChar(ValueStr), Length(ValueStr));
if (cch = 0) then
RaiseLastWin32Error;
SetLength(ValueStr, cch-1); //they include the null terminator /facepalm
Result := ValueStr;
end;
The
FloatToDecimalString
andGetNumberFormat
implementations are left as an exercise for the reader (since i actually haven't written the float one yet, just the currency - i don't know how i'm going to handle exponential notation).
And Bob's yer uncle; properly localized floats and currencies under Delphi.
i already went through the work of properly localizing Integers, Dates, Times, and Datetimes.
Note: Any code is released into the public domain. No attribution required.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With