将扩展(80位)转换为字符串

9

如何将扩展精度浮点值转换为字符串?

背景

英特尔 CPU 支持三种浮点格式:

Delphi 对扩展精度浮点格式有本地支持。

扩展精度可分解为:

  • 1 个符号位
  • 15 个指数位
  • 1 个整数部分位(即数字以 0.1. 开头)
  • 63 个尾数位

您可以将扩展精度的尾数大小与其他浮点类型进行比较:

| Type     | Sign  | Exponent | Integer | Mantissa | 
|----------|-------|----------|---------|----------|
| Single   | 1 bit |  8 bits  |  n/a    | 23 bits  |
| Double   | 1 bit | 11 bits  |  n/a    | 52 bits  |
| Extended | 1 bit | 15 bits  | 1 bit   | 63 bits  |

Extended支持比single和double更高的精度。

例如,取实数.49999999999999999,并将其表示为二进制:

Single:   0.1000000000000000000000000
Double:   0.10000000000000000000000000000000000000000000000000000
Extended: 0.01111111111111111111111111111111111111111111111111111111010001111

你会发现,虽然 SingleDouble 被强制舍入到 0.1 二进制 (0.5 十进制),但是 extended 仍具有一定的精度。

那么如何将二进制小数转换为字符串呢?

如果我尝试将 extended 值 0.49999999999999998 转换为字符串:

FloatToStr(v);

当我查看Extended内部时发现它并不是0.5,但该函数返回了0.5

0x3FFDFFFFFFFFFFFFFD1E

对于其他扩展值,情况也是如此;Delphi中的所有函数(在我所能找到的)都返回0.5:
Value                   Hex representation      FloatToSTr
0.499999999999999980    0x3FFDFFFFFFFFFFFFFD1E  '0.5'
0.499999999999999981    0x3FFDFFFFFFFFFFFFFD43  '0.5'
0.499999999999999982    0x3FFDFFFFFFFFFFFFFD68  '0.5'
0.499999999999999983    0x3FFDFFFFFFFFFFFFFD8D  '0.5'
0.499999999999999984    0x3FFDFFFFFFFFFFFFFDB2  '0.5'
0.499999999999999985    0x3FFDFFFFFFFFFFFFFDD7  '0.5'
0.499999999999999986    0x3FFDFFFFFFFFFFFFFDFB  '0.5'
0.499999999999999987    0x3FFDFFFFFFFFFFFFFE20  '0.5'
0.499999999999999988    0x3FFDFFFFFFFFFFFFFE45  '0.5'
0.499999999999999989    0x3FFDFFFFFFFFFFFFFE6A  '0.5'
0.499999999999999990    0x3FFDFFFFFFFFFFFFFE8F  '0.5'
...                     ...
0.49999999999999999995  0x3FFDFFFFFFFFFFFFFFFF  '0.5'

什么功能?

FloatToStrFloatToStrF都是对FloatToText的包装。

FloatToText最终使用FloatToDecimal从extended中提取浮点数的各个部分并存储在一个记录中:

TFloatRec = packed record
   Exponent: Smallint;
   Negative: Boolean;
   Digits: array[0..20] of Byte;
end;

在我的情况下:
var
   v: Extended;
   fr: TFloatRec;
begin
   v := 0.499999999999999980;

   FloatToDecimal({var}fr, v, fvExtended, 18, 9999);
end;

解码后的浮点数结果如下:

  • 指数: 0 (SmallInt)
  • 负数: False (Boolean)
  • 数字: [53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] (array[0..20] of Byte)

Digits 是由 ASCII 字符组成的数组:

  • 指数: 0
  • 负数: False
  • 数字: '5'

FloatToDecimal 只支持 18 位精度

扩展精度浮点数的 63 位尾数的精度可以降至:

1 / (2^63)  
= 1.08420217248550443400745280086994171142578125 × 10^-19   
= 0.000000000000000000108420217248550443400745280086994171142578125
    \_________________/ 
            |
        19 digits

问题在于:
- Extended可以给你有意义的值,最多到第19位数 - FloatToDecimal虽然返回了20位数,但是只接受和生成最大请求18位数的扩展值(货币为19位数)
关于文档:
对于扩展类型的值,精度参数指定结果中请求的有效数字的数量--允许的范围是1..18。
十进制参数指定结果中小数点左侧的最大数字数量。
精度和小数点一起控制结果的舍入方式。要产生一个结果,无论数字的大小都具有给定数量的有效数字,请将小数点参数指定为9999。
转换结果存储在指定的TFloatRec记录中,如下所示:
- 数字——包含高达18(对于扩展类型)或19(对于货币类型)个有效数字,后跟一个空终止符。如果有,则隐含小数点不存储在数字中。
所以我碰到了内置浮点格式化函数的根本限制。
如何格式化80位IEEE扩展精度浮点数?
如果Delphi自己做不到,那么问题就变成了:我该怎么做?
我知道Extended是10字节(SizeOf(Extended)=10)。现在问题涉及到将IEEE浮点数转换为字符串的黑暗艺术。
某些部分很容易:
function ExtendedToDecimal(v: Extended): TFloatRec;
var
    n: UInt64;
const
    BIAS = 16383;
begin
    Result := Default(TFloatRec);

    Result.Negative := v.Sign;
    Result.Exponent := v.Exponent;
    n := v.Mantissa;
//  Result.Digits :=
end;

但是难点留给回答者自己去练习。

额外奖励截图

输入图片描述


2
请注意,在 Windows 32 位系统上,Extended 只有 10 个字节。在 Windows 64 位和 iOS 设备上,ExtendedDouble 的别名,在 OSX、iOS 模拟器和 Linux 上,Extended 是 16 个字节。因此,Extended 的内部布局取决于平台而变化。使用 TExtendedHelperTExtended80Rec 来帮助您跨多个平台处理 Extended 的组件字段。 - Remy Lebeau
1
你看过John Herbsters的ExactFloatToStr(x:Extended)吗? - LU RD
1
Delphi的FloatToStr甚至不能正确地将double转换为字符串..... - David Heffernan
使用上面链接的库,ExactFloatToStr(Extended(0.49999999999999999)) 的结果为 0.49999999999999998999823495882122159628124791197478771209716796875 - LU RD
1
@LURD 我之前没有找到John的代码,它运行得非常好。将其作为答案表述,你就可以获得接受。而且因为它会一直进行,直到没有剩余内容可添加,所以你也可以使用它来打印SingleDouble以及Extended - Ian Boyd
显示剩余5条评论
1个回答

6
如何将扩展精度浮点值转换为字符串?
由于Delphi RTL没有任何正确完整的FloatToStr()函数的实现,用于Extended(和Double)类型,因此需要使用外部库,可在此处找到,最初来自EDN,Codecentral
该库是由John Herbster创建的,他长期贡献于Delphi RTL库,特别是有关浮点数处理方面。GitHub源代码已更新为使用Unicode字符串处理和TFormatSettings结构进行格式化。该库包含一个ExactFloatToStr()函数,可处理Extended、Double和Single类型的浮点数。
Program TestExactFloatToStr; 

{$APPTYPE CONSOLE}

Uses
  SysUtils,ExactFloatToStr_JH0;

begin
  WriteLn(ExactFloatToStr(Extended(0.49999999999999999)));
  WriteLn(ExactFloatToStr(Double(0.49999999999999999)));
  WriteLn(ExactFloatToStr(Single(0.49999999999999999)));
  ReadLn;
end.

输出:

0.49999999999999998999823495882122159628124791197478771209716796875
0.5
0.5

我的Exact command line tool也可以做到这一点。它使用我的BigIntegers来实现。 - Rudy Velthuis
就我所知,我不认为John Herbster编写了任何Delphi RTL例程。RTL中提到的JOH是指John O'Harrow。然而,John Herbster确实是TeamB的成员之一。 - Rudy Velthuis
@RudyVelthuis,来自John的简历:“2002-2005年,担任Team-B的受邀成员,致力于帮助Borland编程工具的其他用户。精通使用IEEE-754定义的浮点变量,这些变量在PC上得到了广泛应用。” - LU RD
是的,但他没有为Delphi编写任何RTL函数。我经常与他交谈,特别是关于这些话题。我甚至在TeamB会议期间在斯科茨谷见过他,当时Borland仍然在那里的校园占了一半。 - Rudy Velthuis
@RudyVelthuis,我确定他一定决心添加足够的QC细节来修复当时一团糟的DateUtils单元。 - LU RD
显示剩余3条评论

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