Created
February 11, 2022 14:37
-
-
Save generalmimon/fda3ff57f50eb87244e9a800ffd9837f to your computer and use it in GitHub Desktop.
Test which PHP versions support {e,E,g,G} types in unpack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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