Преобразовать расширенный (80-разрядный) в строку
Как я могу преобразовать значение с плавающей точкой расширенной точности в строку?
Фон
Процессор Intel поддерживает три формата с плавающей запятой:
- 32-битная одинарная точность
- 64-битная двойная точность
- 80-битная расширенная точность
Delphi имеет встроенную поддержку формата с плавающей точкой расширенной точности.
Расширенная точность делится на:
- 1 знаковый бит
- 15 экспонентных бит
- 1 бит целой части (то есть число начинается с
0.
или же1.
) - 63 биты мантиссы
Вы можете сравнить размер мантиссы Extended с размерами других типов:
| 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 способен с большей точностью, чем одиночный и двойной.
Например, взять реальное число .49999999999999999
и это представление в двоичном виде:
Single: 0.1000000000000000000000000
Double: 0.10000000000000000000000000000000000000000000000000000
Extended: 0.01111111111111111111111111111111111111111111111111111111010001111
Вы видите, что в то время как Single и Double были вынуждены округлить до 0.1 binary
(0,5 десятичного числа), расширенный по-прежнему имеет некоторую точность.
Но как преобразовать двоичные дроби в строку?
Если я пытаюсь преобразовать расширенное значение 0.49999999999999998
в строку:
FloatToStr(v);
функция возвращает 0.5
, когда я вижу внутри Extended и вижу, что это не 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 для извлечения из расширенной записи, которая содержит фрагменты с плавающей точкой:
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)
- Отрицательный: Ложный (логический)
- Цифры: [53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] (массив [0..20 байт)
Digits
находится в массиве символов ascii:
- Экспонент:
0
- Отрицательный:
False
- Цифры:
'5'
FloatToDecimal ограничен 18 цифрами
Точность 63-битной мантиссы с плавающей точкой повышенной точности может снизиться до:
1 / (2^63)
= 1.08420217248550443400745280086994171142578125 × 10^-19
= 0.000000000000000000108420217248550443400745280086994171142578125
\_________________/
|
19 digits
Проблема в том, что:
- Расширенная может дать вам значимые значения до 19-й цифры
- FloatToDecimal, возвращая до 20 цифр, только принимает и генерирует максимальный запрос из 18 цифр для расширенных значений (19 цифр для валюты)
Для документации:
Для значений типа Extended параметр Precision указывает запрашиваемое количество значащих цифр в результате - допустимый диапазон равен 1..18.
Параметр Decimals указывает запрашиваемое максимальное количество цифр слева от десятичной точки в результате.
Точность и десятичные дроби вместе управляют округлением результата. Чтобы получить результат, который всегда имеет заданное число значащих цифр, независимо от величины числа, укажите 9999 для параметра "Десятичные знаки".
Результат преобразования сохраняется в указанной записи TFloatRec следующим образом:Цифры - содержит до 18 (для типа Extended) или 19 (для типа Currency) значащих цифр, за которыми следует нулевой терминатор. Подразумеваемая десятичная точка (если есть) не сохраняется в цифрах.
Итак, я столкнулся с фундаментальным ограничением встроенных функций форматирования с плавающей точкой
Как отформатировать 80-битное значение с плавающей запятой IEEE с повышенной точностью?
Если Delphi не может сделать это сам, возникает вопрос: как мне это сделать?
Я знаю, что расширенный составляет 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;
Но сложная часть оставлена в качестве упражнения для ответа.
Бонус Скриншот
1 ответ
Как я могу преобразовать значение с плавающей точкой расширенной точности в строку?
Поскольку Delphi RTL не имеет никаких реализаций правильного и полного FloatToStr()
функция для Extended
(а также Double
в этом отношении), нужно было бы использовать внешнюю библиотеку, найденную здесь и первоначально в EDN, Codecentral.
Библиотека была создана Джоном Хербстером (John Herbster), давним автором RTL-библиотек Delphi, особенно в отношении обработки с плавающей запятой. Исходный код 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