Как преобразовать четыре символа в 32-битный плавающий IEEE-754 в Perl?

У меня есть проект, в котором функция получает четыре 8-битных символа и должна преобразовать полученный 32-битный IEEE-754 float в обычный номер Perl. Похоже, что должен быть более быстрый способ, чем рабочий код ниже, но я не смог найти более простую функцию пакета, которая работает.

Это не работает, но похоже, что он близок:

$float = unpack("f", pack("C4", @array[0..3]);  # Fails for small numbers

Работает:

@bits0 = split('', unpack("B8", pack("C", shift)));
@bits1 = split('', unpack("B8", pack("C", shift)));
@bits2 = split('', unpack("B8", pack("C", shift)));
@bits3 = split('', unpack("B8", pack("C", shift)));
push @bits, @bits3, @bits2, @bits1, @bits0;

$mantbit = shift(@bits);
$mantsign = $mantbit ? -1 : 1;
$exp = ord(pack("B8", join("",@bits[0..7])));
splice(@bits, 0, 8);

# Convert fractional float to decimal
for (my $i = 0; $i < 23; $i++) {
    $f = $bits[$i] * 2 ** (-1 * ($i + 1));
    $mant += $f;
}
$float = $mantsign * (1 + $mant) * (2 ** ($exp - 127));

У кого-то есть лучший способ?

Ответ 1

Я бы взял противоположный подход: забудьте распаковать, придерживайтесь бит-скрипок.

Сначала соберите 32-битное слово. В зависимости от цели, это может быть наоборот:

my $word = ($byte0 << 24) + ($byte1 << 16) + ($byte2 << 8) + $byte3;

Теперь извлеките части слова: бит знака, экспонента и мантисса:

my $sign = ($word & 0x80000000) ? -1 : 1;
my $expo = (($word & 0x7F800000) >> 23) - 127;
my $mant = ($word & 0x007FFFFF | 0x00800000);

Соберите свой поплавок:

my $num = $sign * (2 ** $expo) * ( $mant / (1 << 23));

Вот несколько примеров на Wikipedia.

  • Протестировано на 0xC2ED4000 = > -118.625, и оно работает.
  • Протестировано на 0x3E200000 = > 0.15625 и нашел ошибку! (Фиксированный)
  • Не забывайте обрабатывать бесконечности и NaN, когда $expo == 255

Ответ 2

Лучший способ сделать это - использовать pack().

my @bytes = ( 0xC2, 0xED, 0x40, 0x00 );
my $float = unpack 'f', pack 'C4', @bytes;

Или, если источник и место назначения имеют различную ориентацию:

my $float = unpack 'f', pack 'C4', reverse @bytes;

Вы говорите, что этот метод "не работает - кажется, что он близок" и "не подходит для небольших чисел", но вы не приводите пример. Я бы предположил, что то, что вы на самом деле видите, округляется, где, например, число упаковано как 1.234, но оно распаковано как 1.23399996757507. Это не функция pack(), а точность 4-байтового поплавка.