Преобразовать расширенный (80-разрядный) в строку

Как я могу преобразовать значение с плавающей точкой расширенной точности в строку?

Фон

Процессор Intel поддерживает три формата с плавающей запятой:

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
Другие вопросы по тегам