Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I convert four characters into a 32-bit IEEE-754 float in Perl?

I have a project where a function receives four 8-bit characters and needs to convert the resulting 32-bit IEEE-754 float to a regular Perl number. It seems like there should be a faster way than the working code below, but I have not been able to figure out a simpler pack function that works.

It does not work, but it seems like it is close:

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

Works:

@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));

Anyone have a better way?

like image 765
Dan Littlejohn Avatar asked Apr 20 '09 22:04

Dan Littlejohn


2 Answers

I'd take the opposite approach: forget unpacking, stick to bit twiddling.

First, assemble your 32 bit word. Depending on endianness, this might have to be the other way around:

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

Now extract the parts of the word: the sign bit, exponent and mantissa:

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

Assemble your float:

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

There's some examples on Wikipedia.

  • Tested this on 0xC2ED4000 => -118.625 and it works.
  • Tested this on 0x3E200000 => 0.15625 and found a bug! (fixed)
  • Don't forget to handle infinities and NaNs when $expo == 255
like image 107
NickZoic Avatar answered Oct 06 '22 19:10

NickZoic


The best way to do this is to use pack().

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

Or if the source and destination have different endianness:

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

You say that this method "does not work - it seems like it is close" and "fails for small numbers", but you don't give an example. I'd guess that what you are actually seeing is rounding where, for example, a number is packed as 1.234, but it is unpacked as 1.23399996757507. That isn't a function of pack(), but of the precision of a 4-byte float.

like image 22
jmcnamara Avatar answered Oct 06 '22 18:10

jmcnamara