如何将扩展精度浮点值转换为字符串?
背景
英特尔 CPU 支持三种浮点格式:
- 32 位 单精度
- 64 位 双精度
- 80 位 扩展精度
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
你会发现,虽然 Single 和 Double 被强制舍入到 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'
什么功能?
FloatToStr和FloatToStrF都是对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;
但是难点留给回答者自己去练习。
Extended
只有 10 个字节。在 Windows 64 位和 iOS 设备上,Extended
是Double
的别名,在 OSX、iOS 模拟器和 Linux 上,Extended
是 16 个字节。因此,Extended
的内部布局取决于平台而变化。使用TExtendedHelper
和TExtended80Rec
来帮助您跨多个平台处理Extended
的组件字段。 - Remy LebeauExactFloatToStr(Extended(0.49999999999999999))
的结果为0.49999999999999998999823495882122159628124791197478771209716796875
。 - LU RDSingle
、Double
以及Extended
。 - Ian Boyd