如何将浮点数或货币转换为本地化字符串?

15
在Delphi中,使用FloatToStrFCurrToStrF会自动使用DecimalSeparator字符来表示小数点。不幸的是,DecimalSeparator在SysUtils中声明为Char1,2
var 
  DecimalSeparator: Char;

虽然LOCALE_SDECIMAL允许最多三个字符:

用于十进制分隔符的字符,例如在“3.14”中使用“。”或在“3,14”中使用“,”。此字符串允许的最大字符数为四个,包括一个终止空字符。

这会导致Delphi无法正确读取十进制分隔符;回退以假定默认的十进制分隔符为“.”:

DecimalSeparator := GetLocaleChar(DefaultLCID, LOCALE_SDECIMAL, '.');

在我的电脑上这是一个相当特殊的情况,这会导致浮点数和货币值使用U+002E(句点)小数标记进行不正确的本地化。

我愿意直接调用Windows API函数,这些函数旨在将浮点数或货币值转换为本地化字符串:

但是,这些函数需要一串图像代码的字符串,其中仅允许以下字符:

  • 字符“0”到“9”(U+0030..U+0039
  • 如果数字是浮点数,则允许一个小数点符号(.)(U+002E
  • 如果数字是负数,则在第一个字符位置上允许一个负号(U+002D

如何才能很好地将浮点数或货币值转换为遵守这些规则的字符串呢?例如:

  • 1234567.893332
  • -1234567

考虑到本地用户的语言环境(即我的计算机):


一个非常可怕的hack,虽然我可以使用它:

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;

关于VCL使用的函数链的其他信息:

注意

  • DecimalSeparator: Char,单个全局字符已过时,并被另一个单个字符的小数分隔符替换

1在我的Delphi版本中
2以及当前版本的Delphi


5
我很喜欢阅读你的问题。 - Andreas Rejbrand
它们肯定需要很长时间来编写、格式化、链接等等。而在这种情况下,一个包含阿拉伯数字的早期迭代版本崩溃了。很高兴知道这些努力没有被忽视。 - Ian Boyd
顺便提一下,我们有几个使用你所谓的可怕黑客(是的,它非常丑陋)的Delphi5应用程序正在运行。不过要小心,线程可能会咬你,还有第三方组件。 - LU RD
1
@LU - 更新有什么好处?TFormatSettings.DecimalSeparator 仍然是一个字符。 - Sertac Akyuz
@LU RD:哦天哪;我没想过线程问题。Sertac Akyuz:这就是为什么它是个hack;我会对一个局部问题进行全局修复(http://blogs.msdn.com/b/oldnewthing/archive/2008/12/11/9193695.aspx)。当我在做的时候,它*感觉*像个hack,因为我知道它很容易出现这样的问题。 - Ian Boyd
显示剩余11条评论
2个回答

3

Delphi提供了一个名为FloatToDecimal的过程,可以将浮点数(例如Extended)和Currency值转换为有用的结构以进行进一步格式化。例如:

FloatToDecimal(..., 1234567890.1234, ...);

给你:
TFloatRec
   Digits: array[0..20] of Char = "12345678901234"
   Exponent: SmallInt =           10
   IsNegative: Boolean =          True

“Exponent”表示小数点左侧的数字位数。

需要处理一些特殊情况:

  • 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


我们可以使用FloatToDecimal作为起点,创建一个与语言环境无关的"图片代码字符串"。

然后,将此字符串传递给适当的Windows GetNumberFormatGetCurrencyFormat函数,以执行实际的正确本地化。

我编写了自己的CurrToDecimalStringFloatToDecimalString,将数字转换为所需的语言环境无关格式:

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;

除了NANINF-INF这些特殊情况外,我现在可以将这些字符串传递给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;

"

FloatToDecimalStringGetNumberFormat的实现留给读者自己练习(因为我实际上还没有写过浮点数,只写过货币-我不知道如何处理指数表示法)。

这样一来,Delphi下的浮点数和货币就可以正确本地化了。

我已经完成了整数、日期、时间和日期时间的正确本地化工作。

注意:任何代码都是公有领域。不需要归属。

"

2

好的,这可能不是你想要的,但它适用于D2007及以上版本。线程安全且完全可行。

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;

我不能直接使用它;我仍然需要通过Windows的GetNumberFormat运行它,将其转换为适当的字符串。除此之外,我没有D2007;但我可能能够窃取RTL源代码 - 除非它仍然在汇编包含文件中(如D5)。 - Ian Boyd
你可以查看 FPC 源代码,他们的 SysUtils 单元中有类似的函数。 - LU RD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接