Как преобразовать четыре символа в 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-байтового поплавка.