Skip to content

Instantly share code, notes, and snippets.

@generalmimon
Created February 11, 2022 14:37
Show Gist options
  • Save generalmimon/fda3ff57f50eb87244e9a800ffd9837f to your computer and use it in GitHub Desktop.
Save generalmimon/fda3ff57f50eb87244e9a800ffd9837f to your computer and use it in GitHub Desktop.
Test which PHP versions support {e,E,g,G} types in unpack
<?php
// See https://3v4l.org/
declare(strict_types=1);
interface FloatImpl {
public function readF4be(string $bytes): float;
public function readF8be(string $bytes): float;
public function readF4le(string $bytes): float;
public function readF8le(string $bytes): float;
}
// https://github.com/kaitai-io/kaitai_struct_php_runtime/blob/78ec65a2e96872514136b5289450af2af9c92556/lib/Kaitai/Struct/Stream.php
class ImplManual implements FloatImpl {
// ---
// Big-endian
public function readU4be(string $bytes): int {
return unpack("N", $bytes)[1];
}
public function readU8be(string $bytes): int {
return unpack("J", $bytes)[1];
}
// ---
// Little-endian
public function readU4le(string $bytes): int {
return unpack("V", $bytes)[1];
}
public function readU8le(string $bytes): int {
return unpack("P", $bytes)[1];
}
// ---
// Big-endian
/**
* Single precision floating-point number
*/
public function readF4be(string $bytes): float {
$bits = $this->readU4be($bytes);
return $this->decodeSinglePrecisionFloat($bits);
}
/**
* Double precision floating-point number.
*/
public function readF8be(string $bytes): float {
$bits = $this->readU8be($bytes);
return $this->decodeDoublePrecisionFloat($bits);
}
// ---
// Little-endian
/**
* Single precision floating-point number.
*/
public function readF4le(string $bytes): float {
$bits = $this->readU4le($bytes);
return $this->decodeSinglePrecisionFloat($bits);
}
/**
* Double precision floating-point number.
*/
public function readF8le(string $bytes): float {
$bits = $this->readU8le($bytes);
return $this->decodeDoublePrecisionFloat($bits);
}
private function decodeSinglePrecisionFloat(int $bits): float {
$fractionToFloat = function (int $fraction): float {
$val = 0;
for ($i = 22, $j = 1; $i >= 0; $i--, $j++) {
$bit = ((1 << $i) & $fraction) >> $i;
$val += 2 ** (-$j) * $bit;
}
return $val;
};
// Sign - 31 bit, one bit
$sign = ($bits >> 31) == 0 ? 1 : -1;
// Exponent - [23..30] bits, 8 bits
$exponent = ($bits >> 23) & 0xff;
// Fraction/mantissa/significand - [22..0] bits, 23 bits,
$fraction = $bits & 0x7fffff;
if (0 === $exponent) {
if ($fraction === 0) {
// $exponent === 0, $fraction === 0.
// We use 0.0 to have ability to return -0.0, the integer 0 does not work.
return $sign * 0.0;
}
// $exponent === 0, $fraction !== 0 => return denormalized number
return $sign * 2 ** (-126) * $fractionToFloat($fraction);
} elseif (255 === $exponent) {
if ($fraction !== 0) {
// $exponent === 255, $fraction !== 0.
return NAN;
}
// $exponent === 255, $fraction === 0.
return $sign * INF;
}
// $exponent is not either 0 or 255.
return $sign * 2 ** ($exponent - 127) * (1 + $fractionToFloat($fraction));
}
private function decodeDoublePrecisionFloat(int $bits): float {
$fractionToFloat = function (int $fraction): float {
$val = 0;
for ($i = 51, $j = 1; $i >= 0; $i--, $j++) {
$bit = ((1 << $i) & $fraction) >> $i;
$val += 2 ** (-$j) * $bit;
}
return $val;
};
// Sign - 63 bit, one bit
$sign = ($bits >> 63) == 0 ? 1 : -1;
// Exponent - [52..62] bits, 11 bits
$exponent = ($bits >> 52) & 0x7ff;
// Fraction/mantissa/significand - [51..0] bits, 52 bits,
$fraction = $bits & 0xfffffffffffff;
if (0 === $exponent) {
if ($fraction === 0) {
// $exponent === 0, $fraction === 0.
// We use 0.0 to have ability to return -0.0, the integer 0 does not work.
return $sign * 0.0;
}
// $exponent === 0, $fraction !== 0 => return denormalized number
return $sign * 2 ** (-1022) * $fractionToFloat($fraction);
} elseif (2047 === $exponent) {
if ($fraction !== 0) {
// $exponent === 2047, $fraction !== 0.
return NAN;
}
// $exponent === 2047, $fraction === 0.
return $sign * INF;
}
// $exponent is not either 0 or 2047.
return $sign * 2 ** ($exponent - 1023) * (1 + $fractionToFloat($fraction));
}
}
// https://web.archive.org/web/20170617094522/http://php.net/manual/en/function.pack.php
class ImplNative implements FloatImpl {
public function readF4be(string $bytes): float {
// $bytes = $this->readBytes(4);
return unpack("G", $bytes)[1];
}
public function readF8be(string $bytes): float {
// $bytes = $this->readBytes(8);
return unpack("E", $bytes)[1];
}
public function readF4le(string $bytes): float {
// $bytes = $this->readBytes(4);
return unpack("g", $bytes)[1];
}
public function readF8le(string $bytes): float {
// $bytes = $this->readBytes(8);
return unpack("e", $bytes)[1];
}
}
class TestImpl {
protected $_m_singleValue;
protected $_m_doubleValue;
protected $_m_singleValueBe;
protected $_m_doubleValueBe;
protected $_m_approximateValue;
public function singleValue() { return $this->_m_singleValue; }
public function doubleValue() { return $this->_m_doubleValue; }
public function singleValueBe() { return $this->_m_singleValueBe; }
public function doubleValueBe() { return $this->_m_doubleValueBe; }
public function approximateValue() { return $this->_m_approximateValue; }
public function __construct(FloatImpl $_io) {
$this->_io = $_io;
}
private function _read() {
$data = "\x00\x00\x00\x3f\x00\x00\x00\x00\x00\x00\xd0\x3f\x3f\x00\x00\x00\x3f\xd0\x00\x00\x00\x00\x00\x00\x19\x04\x9e\x3f";
$i = 0;
$this->_m_singleValue = $this->_io->readF4le(self::slice($data, $i, ($i += 4)));
$this->_m_doubleValue = $this->_io->readF8le(self::slice($data, $i, ($i += 8)));
$this->_m_singleValueBe = $this->_io->readF4be(self::slice($data, $i, ($i += 4)));
$this->_m_doubleValueBe = $this->_io->readF8be(self::slice($data, $i, ($i += 8)));
$this->_m_approximateValue = $this->_io->readF4le(self::slice($data, $i, ($i += 4)));
}
private static function slice(string $data, int $start, int $end) {
return substr($data, $start, max(0, $end - $start));
}
public function run() {
$this->_read();
$this->assertEquals(1, $this->singleValue(), 0.5);
$this->assertEquals(2, $this->singleValueBe(), 0.5);
$this->assertEquals(3, $this->doubleValue(), 0.25);
$this->assertEquals(4, $this->doubleValueBe(), 0.25);
$this->assertEquals(5, $this->approximateValue(), 1.2345000505447388);
}
private function assertEquals(int $n, $actual, $expected) {
echo 'Assert ' . $n . ': ' . ($actual === $expected ? 'OK' : 'FAIL') . ' ($actual = ' . print_r($actual, true) . ', $expected = ' . print_r($expected, true) . ')' . PHP_EOL;
}
}
$man = new TestImpl(new ImplManual());
echo 'Manual:' . PHP_EOL;
$man->run();
echo PHP_EOL;
$nat = new TestImpl(new ImplNative());
echo 'Native:' . PHP_EOL;
$nat->run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment