开发者

How to convert float or currency to a localized string?

In Delphi1, using FloatToStrF or CurrToStrF will automatically use the DecimalSeparator character to represent a decimal mark. Unfortunately DecimalSeparator is declared in SysUtils as Char1,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:

  • Characters "0" through "9" (U+0030..U+0039)
  • One decimal point (.) if the number is a floating-point value (U+002E)
  • A minus sign in the first character position if the number is a negative value (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):

  • might not use a - to indicate negative (e.g. --)
  • might not use a . to indicate a decimal point (e.g. ,,)
  • might not use the latin alphabet 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开发者_C百科 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 separator

1 in my version of Delphi

2 and in current versions of Delphi


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 and GetNumberFormat 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.


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;
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜