diff --git a/framework/Exceptions/messages/messages.txt b/framework/Exceptions/messages/messages.txt index 0e3be1b8c..4592b40ac 100644 --- a/framework/Exceptions/messages/messages.txt +++ b/framework/Exceptions/messages/messages.txt @@ -576,7 +576,10 @@ dbcron_property_unchangeable = TDbCronModule.{0} is already initialized and ca timescheduler_invalid_string = TTimeScheduler could not parse the schedule element '{0}' -datasize_no_negative_size = TDataSize.Size cannot be negative. +bithelper_bad_fp_format = TBitHelper cannot work with '{0}' exponent bits, '{1}' mantissa bits, '{2}' exponent bias, in a {3} bit PHP environment. +bithelper_invalid_color_in = TBitHelper must have at least one In Bit. '{0}' was given. +bithelper_invalid_color_out = TBitHelper must have at least one Out Bit. '{0}' was given. +bithelper_bad_mirror_bits = TBitHelper cannot mirror '{0}' bits in a {1} bit PHP environment. ar_data_invalid = {0}.copyFrom() can only take an object or array as parameter. ar_save_invalid = The {0} instance cannot be saved because it is either deleted or in an unknown state. diff --git a/framework/Util/TBitHelper.php b/framework/Util/TBitHelper.php new file mode 100644 index 000000000..9027cf564 --- /dev/null +++ b/framework/Util/TBitHelper.php @@ -0,0 +1,527 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Util; + +use Prado\Exceptions\TInvalidDataValueException; + +/** + * TBitHelper class. + * + * This class contains static functions for bit-wise and byte operations, like color + * bit shifting, unsigned right bit shift, mirroring the order of bits, flipping + * endian, and formatting floats into and from smaller float representations like + * half floats (Fp16, Bf16) and mini floats (Fp8). It also can check for negative + * floats including negative zero. + * + * Shifting bits for color accuracy requires repeating the bits rather than + * just adding extra 0/1 bits. {@link colorBitShift} properly adds and removes bits + * to an integer color value by replicating the bits for new bits. + * + * There are specific floating point conversion methods for converting float to: + * - Fp16 with {@link floatToFp16} and back with {@link fp16ToFloat}. + * - Bf16 with {@link floatToBf16} and back with {@link bf16ToFloat}. + * - Fp8-e5m2 with {@link floatToFp8Range} and back with {@link fp8RangeToFloat}. + * - Fp8-e4m3 with {@link floatToFp8Precision} and back with {@link fp8PrecisionToFloat}. + * These functions use the general conversion functions {@link floatToFpXX} and + * {@link fpXXToFloat} where the number of bits for the exponent and mantissa are + * parameters. For example, 24 bit floats or 14 bit floats can be created. + * + * {@link mirrorBits} can mirror arbitrary runs of bits in an integer. There is + * quick mirroring for specific exponents of two: {@link mirrorByte} for 8 bits, + * {@link mirrorShort} for 16 bits, {@link mirrorLong} for 32 bits, and, on 64 bit + * instances of PHP, {@link mirrorLongLong} for 64 bits. + * + * There are endian byte reversal functions: {@link flipEndianShort}, {@link flipEndianLong}, + * and, on 64 bit instances of PHP, {@link flipEndianLongLong}. + * + * {@link bitCount} calculates the number of bits required to represent a specific + * number. 255 return 8 bits, 256 returns 9 bits. + * + * {@link isNegativeFloat} is used to determine if a float has the negative bit + * set. It will return true on any negative float number, including negative zero. + * {@link isNegativeZero} can check if a float is a negative zero. PHP cannot normally + * check for negative zero float and requires these special functions to so. + * + * The Levels and Masks are for O(1) time bit reversals of 8, 16, 32, and 64 bit integers. + * The TBitHelper class automatically adjusts itself for 32 or 64 bit PHP environments. + * + * When quickly mirroring bits or switching endian, the high bits are also converted + * like the low bits. E.g. When mirroring a Byte, all bytes in the integer are + * individually mirrored in place. When converting a Short, each short in the integer + * will be converted in place. In the instance of a Long, for 64 bit systems will + * convert both Longs -in place- in its LongLong (64 bit) unit integer type. + * Converting LongLong is only supported in 64 bit PHP environments. + * + * @author Brad Anderson + * @since 4.2.3 + */ +class TBitHelper +{ + public const Level1 = (PHP_INT_SIZE >= 8) ? 0x5555555555555555 : 0x55555555; + public const NLevel1 = ~self::Level1; + public const Mask1 = (PHP_INT_SIZE >= 8) ? 0x7FFFFFFFFFFFFFFF : 0x7FFFFFFF; + public const Level2 = (PHP_INT_SIZE >= 8) ? 0x3333333333333333 : 0x33333333; + public const NLevel2 = ~self::Level2; + public const Mask2 = self::Mask1 >> 1; + public const Level3 = (PHP_INT_SIZE >= 8) ? 0x0F0F0F0F0F0F0F0F : 0x0F0F0F0F; + public const NLevel3 = ~self::Level3; + public const Mask3 = self::Mask1 >> 3; + public const Level4 = (PHP_INT_SIZE >= 8) ? 0x00FF00FF00FF00FF : 0x00FF00FF; + public const NLevel4 = ~self::Level4; + public const Mask4 = self::Mask1 >> 7; + public const Level5 = (PHP_INT_SIZE >= 8) ? 0x0000FFFF0000FFFF : 0x0000FFFF; + public const NLevel5 = ~self::Level5; + public const Mask5 = self::Mask1 >> 15; + public const Level6 = (PHP_INT_SIZE >= 8) ? 0x00000000FFFFFFFF : -1; + public const NLevel6 = ~self::Level6; + public const Mask6 = self::Mask1 >> 31; + + /** + * Motorola is Big Endian with the Most Significant Byte first whereas Intel uses + * Little Endian with the Least Significant Byte first. This mainly only affects + * the binary reading and writing of data types that are 2 bytes or larger. + * @return bool Is the PHP environment in Big Endian Motorola Byte format. + */ + public static function isSystemBigEndian(): bool + { + static $bigEndian = null; + if ($bigEndian === null) { + $bigEndian = unpack('S', "\x00\x01")[1] === 1; + } + return $bigEndian; + } + + /** + * @return bool Is the PHP environment 64 bit and supports the 64 bit LongLong type. + */ + public static function hasLongLong(): bool + { + return PHP_INT_SIZE >= 8; + } + + /** + * This returns true with all negative floats, including -0.0. Normally "$float < 0" + * will not include -0.0, where this function does include -0.0. + * @param float $value The float to check for being negative. + * @return bool Is a negative float. + */ + public static function isNegativeFloat(float $value): bool + { + return $value < 0 || $value === -0.0 && (ord(pack('G', $value)) & 0x80) !== 0; + } + + /** + * This returns true with negative zero (-0.0). Checking for negative zero floats + * requires this special function because PHP cannot be directly check for negative + * zero due to '-0.0 === 0.0'. + * @param float $value The float to check for being negative. + * @return bool Is a negative zero float. + */ + public static function isNegativeZero(float $value): bool + { + return $value === -0.0 && (ord(pack('G', $value)) & 0x80) !== 0; + } + + /** + * Encodes a PHP float into an N-bit floating point number (in an integer) representation. + * This function can be configured with arbitrary number of Exponent Bits, Mantissa Bits, + * Exponent Bias, and IEEE Conformance (for subnormal numbers, INF, -INF, and NAN). + * The total number of floating point bits to be parsed is "$exponentBits + $mantissaBits + 1". + * + * With default parameter values, this functions as floatToFp16. + * @param float $value The PHP float to encode. + * @param int $exponentBits The number of bits used for the exponent, default: null for 5. + * @param int $mantissaBits The number of bits used for the mantissa, default: null for 10. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @param bool $IEEEConformance Whether to follow the IEEE 754 standard for special values + * (NaN, INF, -INF, and subnormal). Default true + * @throws TInvalidDataValueException on bad floating point configuration values. + * @return int The a short form float representation of the float $value. + */ + public static function floatToFpXX(float $value, ?int $exponentBits = null, ?int $mantissaBits = null, ?int $exponentBias = null, bool $IEEEConformance = true): int + { + $exponentBits = ($exponentBits === null) ? 5 : $exponentBits; + $mantissaBits = ($mantissaBits === null) ? 10 : $mantissaBits; + $exponentMaxValue = (1 << $exponentBits) - 1; + $exponentBias = ($exponentBias === null) ? $exponentMaxValue >> 1 : $exponentBias; + if ($exponentBits <= 0 || $mantissaBits <= 0 || ($exponentBits + $mantissaBits + 1) > PHP_INT_SIZE * 8 || $exponentBias < 0 || $exponentBias > $exponentMaxValue) { + throw new TInvalidDataValueException('bithelper_bad_fp_format', $exponentBits, $mantissaBits, $exponentBias, PHP_INT_SIZE * 8); + } + $sign = self::isNegativeFloat($value) ? 1 : 0; + $value = abs($value); + $exponent = 0; + $mantissa = 0; + + if ($IEEEConformance && is_nan($value)) { + $exponent = $exponentMaxValue; + $mantissa = (1 << $mantissaBits) - 1; + } elseif ($IEEEConformance && (is_infinite($value) || $value >= pow(2, ($exponentMaxValue - 1) - $exponentBias) * (1 << $mantissaBits))) { + $exponent = $exponentMaxValue; + } elseif ($value == 0) { + $mantissa = 0; + } else { + $exponent = floor(log($value, 2)) + $exponentBias; + if ($exponent <= 0) { + $mantissa = round($value / pow(2, 1 - $exponentBias - $mantissaBits)); + $exponent = 0; + } elseif ($exponent >= $exponentMaxValue) { + $exponent = $exponentMaxValue; + $mantissa = 0; + } else { + $totalMantissaValues = (1 << $mantissaBits); + $mantissa = round(($value / pow(2, $exponent - $exponentBias) - 1.0) * $totalMantissaValues); + if ($mantissa === $totalMantissaValues) { + $exponent++; + $mantissa = 0; + } + } + } + $fpXX = ($sign << ($exponentBits + $mantissaBits)) | ($exponent << $mantissaBits) | $mantissa; + return $fpXX; + } + + /** + * This encodes a PHP float into a Fp16 (1 bit sign, 5 bits exponent, 10 bits mantissa) float. + * @param float $value The float to encode. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return int The encoded 2 byte Fp16 float. + */ + public static function floatToFp16(float $value, ?int $exponentBias = null): int + { + return self::floatToFpXX($value, 5, 10, $exponentBias); + } + + /** + * This encodes a PHP float into a Bf16 (1 bit sign, 8 bits exponent, 7 bits mantissa) + * float. This preserves the range of typical 4 byte floats but drops 2 bytes of + * precision from 23 bits to 7 bits. + * @param float $value The float to encode. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return int The encoded 2 byte Bf16 float. + */ + public static function floatToBf16(float $value, ?int $exponentBias = null): int + { + return self::floatToFpXX($value, 8, 7, $exponentBias); + } + + /** + * This encodes a PHP float into an FP8 (1 bit sign, 5 bits exponent, 2 bits mantissa) float. + * The FP8 E5M2 format is for lower precision and higher range. + * @param float $value The float to encode. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return int The encoded 1 byte FP8-E5M2 float. + */ + public static function floatToFp8Range(float $value, ?int $exponentBias = null): int + { + return self::floatToFpXX($value, 5, 2, $exponentBias); + } + + /** + * This encodes a PHP float into an FP8 (1 bit sign, 4 bits exponent, 3 bits mantissa) float. + * The FP8 E4M3 format is for higher precision and lower range. + * @param float $value The float to encode. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return int The encoded 1 byte FP8-E4M3 float. + */ + public static function floatToFp8Precision(float $value, ?int $exponentBias = null): int + { + return self::floatToFpXX($value, 4, 3, $exponentBias); + } + + /** + * Decodes an N-bit floating point encoded as an integer to a PHP floating-point number. + * This function can be configured with arbitrary number of Exponent Bits, Mantissa Bits, + * Exponent Bias, and IEEE Conformance (for subnormal numbers, INF, -INF, and NAN). + * The total number of floating point bits to be parsed is "$exponentBits + $mantissaBits + 1". + * + * With default parameter values, this functions as fp16ToFloat. + * @param int $fpXX The encoded N-bit floating point number. + * @param int $exponentBits The number of bits used for the exponent, default: null for 5. + * @param int $mantissaBits The number of bits used for the mantissa, default: null for 10. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @param bool $IEEEConformance Whether to follow the IEEE 754 standard for special values + * (NaN, INF, -INF, and subnormal). Default true + * @throws TInvalidDataValueException on bad floating point configuration values. + * @return float The PHP float of the encoded $fpXX float. + */ + public static function fpXXToFloat(int $fpXX, ?int $exponentBits = null, ?int $mantissaBits = null, ?int $exponentBias = null, bool $IEEEConformance = true): float + { + $exponentBits = ($exponentBits === null) ? 5 : $exponentBits; + $mantissaBits = ($mantissaBits === null) ? 10 : $mantissaBits; + $exponentMaxValue = (1 << $exponentBits) - 1; + if ($exponentBits <= 0 || $mantissaBits <= 0 || ($exponentBits + $mantissaBits + 1) > PHP_INT_SIZE * 8 || + ($exponentBias !== null && ($exponentBias < 0 || $exponentBias > $exponentMaxValue))) { + throw new TInvalidDataValueException('bithelper_bad_fp_format', $exponentBits, $mantissaBits, $exponentBias, PHP_INT_SIZE * 8); + } + $exponentBias = ($exponentBias === null) ? $exponentMaxValue >> 1 : $exponentBias; + $sign = ($fpXX >> ($exponentBits + $mantissaBits)) & 0x1; + $exponent = ($fpXX >> $mantissaBits) & $exponentMaxValue; + $mantissa = $fpXX & ((1 << $mantissaBits) - 1); + if ($IEEEConformance && $exponent == 0) { // subnormal numbers. + $value = $mantissa * pow(2, 1 - $exponentBias - $mantissaBits); + } elseif ($IEEEConformance && $exponent == $exponentMaxValue) { + $value = ($mantissa == 0) ? INF : NAN; + } else { + $value = pow(2, $exponent - $exponentBias) * (1.0 + ($mantissa / (1 << $mantissaBits))); + } + if ($sign) { + $value = -$value; + } + return $value; + } + + /** + * This decodes a Fp16 (5 bits exponent, 10 bits mantissa) encoded float into a PHP Float. + * @param int $fp16 the Fp16 encoded float. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return float The Fp16 float decoded as a PHP float. + */ + public static function fp16ToFloat(int $fp16, ?int $exponentBias = null): float + { + return self::fpXXToFloat($fp16, 5, 10, $exponentBias); + } + + /** + * This decodes a Bf16 (8 bits exponent, 7 bits mantissa) encoded float into a PHP + * Float. + * @param int $bf16 the BF16 encoded float. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return float The Bf16 float decoded as a PHP float. + */ + public static function bf16ToFloat(int $bf16, ?int $exponentBias = null): float + { + return self::fpXXToFloat($bf16, 8, 7, $exponentBias); + } + + /** + * This decodes a FP8 (5 bits exponent, 2 bits mantissa) encoded float into a PHP Float. + * @param int $fp8 the FP8-E5M2 encoded float. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return float The FP8-E5M2 float decoded as a PHP float. + */ + public static function fp8RangeToFloat(int $fp8, ?int $exponentBias = null): float + { + return self::fpXXToFloat($fp8, 5, 2, $exponentBias); + } + + /** + * This decodes a FP8 (4 bits exponent, 3 bits mantissa) encoded float into a PHP Float. + * @param int $fp8 the FP8-E4M3 encoded float. + * @param null|int $exponentBias The bias to apply to the exponent. If null, it defaults to + * half the maximum exponent value. Default: null. + * @return float The FP8-E4M3 float decoded as a PHP float. + */ + public static function fp8PrecisionToFloat(int $fp8, ?int $exponentBias = null): float + { + return self::fpXXToFloat($fp8, 4, 3, $exponentBias); + } + + /** + * This calculates the number of bits required to represent a given number. + * eg. If there are 256 colors, then the maximum representable number in 8 bits + * is 255. A $value of 255 returns 8 bits, and 256 returns 9 bits, to represent + * the number. + * @param int $value The number to calculate the bits required to represent it. + * @return int The number of bits required to represent $n + */ + public static function bitCount(int $value): int + { + if ($value === 0) { + return 0; + } elseif ($value < 0) { // Negative numbers need one more bit. + $value = (-$value) << 1; + } + if ($value < 0) { + return PHP_INT_SIZE * 8; + } + return (int) ceil(log($value + 1, 2)); + } + + /** + * This method shifts color bits. When removing bits, they are simply dropped. + * When adding bits, it replicates the existing bits for new bits to create the + * most accurate higher bit representation of the color. + * @param int $value The color value to expand or contract bits. + * @param int $inBits The number of bits of the input value. + * @param int $outBits The number of bits of the output value. + * @return int The $value shifted to $outBits in size. + * @throw TInvalidDataValueException when the $inBits or $outBits are less than + * 1 or greater than the Max Int Size for this PHP implementation. + */ + public static function colorBitShift(int $value, int $inBits, int $outBits): int + { + if ($inBits < 1 || $inBits > PHP_INT_SIZE * 8) { + throw new TInvalidDataValueException("bithelper_invalid_color_in", $inBits); + } + if ($outBits < 1 || $outBits > PHP_INT_SIZE * 8) { + throw new TInvalidDataValueException("bithelper_invalid_color_out", $outBits); + } + $dif = $outBits - $inBits; + if ($dif > 0) { + $return = $value; + do { + $dd = min($inBits, $dif); + $return = ($return << $dd) | ($value >> ($inBits - $dd)); + $dif -= $dd; + } while ($dif > 0); + return $return; + } elseif ($dif < 0) { + $dif = -$dif; + return ($value >> $dif) & (PHP_INT_MAX >> ($dif - 1)); + } + return $value; + } + + /** + * This does a right bit shift but the signed bit is not replicated in the high + * bit (with a bit-and). + * In normal PHP right bit shift, the signed bit is what make up any new bit in + * the shift. + * @param int $value The integer to bit shift. + * @param int $bits How much to shift the bits right. Positive is right shift, + * Negative is left shift. + * @return int The shifted integer without the high bit repeating. + */ + public static function unsignedShift(int $value, int $bits): int + { + if ($bits > 0) { + return ($value >> $bits) & (PHP_INT_MAX >> ($bits - 1)); + } elseif ($bits < 0) { + return $value << -$bits; + } else { + return $value; + } + } + + /** + * This mirrors $nbit bits from $value. For example, 0b100 becomes 0b001 @ $nbit = 3 + * and 0x0100 become 0x0010 @ $nbit = 4. + * @param int $value The bits to reverse. + * @param int $nbit The number of bits to reverse. + * @throws TInvalidDataValueException when $nbits is over the maximum size of a PHP int. + * @return int reversed bits of $value. + */ + public static function mirrorBits(int $value, int $nbit): int + { + if ($nbit > PHP_INT_SIZE * 8) { + throw new TInvalidDataValueException('bithelper_bad_mirror_bits', $nbit, PHP_INT_SIZE * 8); + } + for ($i = 0, $result = 0; $i < $nbit; $i++) { + $result <<= 1; + $result |= $value & 1; + $value >>= 1; + } + return $result; + } + + /** + * This quickly mirrors the 8 bits in each byte of $n. + * @param int $n The integer to mirror the bits of each byte. + * @return int reversed 8 bits of $n. + */ + public static function mirrorByte(int $n): int + { + $n = ((($n & self::NLevel1) >> 1) & self::Mask1) | (($n & self::Level1) << 1); + $n = ((($n & self::NLevel2) >> 2) & self::Mask2) | (($n & self::Level2) << 2); + return ((($n & self::NLevel3) >> 4) & self::Mask3) | (($n & self::Level3) << 4); + } + + /** + * This quickly mirrors the 16 bits in each [2 byte] short of $n. + * @param int $n The integer to mirror the bits of each short. + * @return int reversed 16 bits of $n. + */ + public static function mirrorShort(int $n): int + { + $n = ((($n & self::NLevel1) >> 1) & self::Mask1) | (($n & self::Level1) << 1); + $n = ((($n & self::NLevel2) >> 2) & self::Mask2) | (($n & self::Level2) << 2); + $n = ((($n & self::NLevel3) >> 4) & self::Mask3) | (($n & self::Level3) << 4); + return ((($n & self::NLevel4) >> 8) & self::Mask4) | (($n & self::Level4) << 8); + + } + + /** + * This quickly mirrors the 32 bits in each [4 byte] long of $n. + * @param int $n The integer to mirror the bits of each long. + * @return int reversed 32 bits of $n. + */ + public static function mirrorLong(int $n): int + { + $n = ((($n & self::NLevel1) >> 1) & self::Mask1) | (($n & self::Level1) << 1); + $n = ((($n & self::NLevel2) >> 2) & self::Mask2) | (($n & self::Level2) << 2); + $n = ((($n & self::NLevel3) >> 4) & self::Mask3) | (($n & self::Level3) << 4); + $n = ((($n & self::NLevel4) >> 8) & self::Mask4) | (($n & self::Level4) << 8); + return ((($n & self::NLevel5) >> 16) & self::Mask5) | (($n & self::Level5) << 16); + } + + /** + * This quickly mirrors the 64 bits of $n. This only works with 64 bit PHP systems. + * For speed, there is no check to validate that the system is 64 bit PHP. You + * must do the validation if/when needed with method {@link hasLongLong}. + * @param int $n The 8 byte integer to mirror the bits of. + * @return int reversed 64 bits of $n. + */ + public static function mirrorLongLong(int $n): int + { + $n = ((($n & self::NLevel1) >> 1) & self::Mask1) | (($n & self::Level1) << 1); + $n = ((($n & self::NLevel2) >> 2) & self::Mask2) | (($n & self::Level2) << 2); + $n = ((($n & self::NLevel3) >> 4) & self::Mask3) | (($n & self::Level3) << 4); + $n = ((($n & self::NLevel4) >> 8) & self::Mask4) | (($n & self::Level4) << 8); + $n = ((($n & self::NLevel5) >> 16) & self::Mask5) | (($n & self::Level5) << 16); + return ((($n & self::NLevel6) >> 32) & self::Mask6) | (($n & self::Level6) << 32); + } + + /** + * This quickly flips the endian in each [2 byte] short of $n. + * @param int $n The 2 byte short to reverse the endian. + * @return int reversed endian of $n. + */ + public static function flipEndianShort(int $n): int + { + return ((($n & self::NLevel4) >> 8) & self::Mask4) | (($n & self::Level4) << 8); + } + + /** + * This quickly flips the endian in each [4 byte] long of $n. + * @param int $n The 4 byte long to reverse the endian. + * @return int The reversed endian of $n. + */ + public static function flipEndianLong(int $n): int + { + $n = ((($n & self::NLevel4) >> 8) & self::Mask4) | (($n & self::Level4) << 8); + return ((($n & self::NLevel5) >> 16) & self::Mask5) | (($n & self::Level5) << 16); + } + + /** + * This quickly fligs the endian of an 8 byte integer. This only works with 64 + * bit PHP systems. 32 bit systems will treat the bit field as floats and invariably + * fail. + * + * For speed, there is no check to validate that the system is 64 bit PHP. You + * must do the validation if/when needed with method {@link hasLongLong}. + * @param int $n The 8 byte long long to reverse the endian. + * @return int reversed 8 bytes endian of $n. + */ + public static function flipEndianLongLong(int $n): int + { + $n = ((($n & self::NLevel4) >> 8) & self::Mask4) | (($n & self::Level4) << 8); + $n = ((($n & self::NLevel5) >> 16) & self::Mask5) | (($n & self::Level5) << 16); + return ((($n & self::NLevel6) >> 32) & self::Mask6) | (($n & self::Level6) << 32); + } +} diff --git a/framework/classes.php b/framework/classes.php index 69faf8b77..c990df6cb 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -287,6 +287,7 @@ 'TBaseBehavior' => 'Prado\Util\TBaseBehavior', 'TBehavior' => 'Prado\Util\TBehavior', 'TBehaviorsModule' => 'Prado\Util\TBehaviorsModule', +'TBitHelper' => 'Prado\Util\TBitHelper', 'TBrowserLogRoute' => 'Prado\Util\TBrowserLogRoute', 'TCallChain' => 'Prado\Util\TCallChain', 'TClassBehavior' => 'Prado\Util\TClassBehavior', diff --git a/tests/unit/Util/TBitHelperTest.php b/tests/unit/Util/TBitHelperTest.php new file mode 100755 index 000000000..c9dbc21c2 --- /dev/null +++ b/tests/unit/Util/TBitHelperTest.php @@ -0,0 +1,414 @@ += 8, TBitHelper::hasLongLong()); + } + + public function testIsNegativeFloat() + { + self::assertEquals(false, TBitHelper::isNegativeFloat(0.0)); + self::assertEquals(false, TBitHelper::isNegativeFloat(1.0)); + self::assertEquals(false, TBitHelper::isNegativeFloat(INF)); + self::assertEquals(false, TBitHelper::isNegativeFloat(NAN)); + self::assertEquals(true, TBitHelper::isNegativeFloat(-0.0), "negative zero is not flagged as a negative float"); + self::assertEquals(true, TBitHelper::isNegativeFloat(-1.0)); + self::assertEquals(true, TBitHelper::isNegativeFloat(-INF)); + } + + public function testIsNegativeZero() + { + self::assertEquals(false, TBitHelper::isNegativeZero(0.0)); + self::assertEquals(false, TBitHelper::isNegativeZero(1.0)); + self::assertEquals(false, TBitHelper::isNegativeZero(INF)); + self::assertEquals(false, TBitHelper::isNegativeZero(NAN)); + self::assertEquals(true, TBitHelper::isNegativeZero(-0.0), "negative zero is not flagged as a negative zero"); + self::assertEquals(false, TBitHelper::isNegativeZero(-1.0)); + self::assertEquals(false, TBitHelper::isNegativeZero(-INF)); + + self::assertTrue(-0.0 === 0.0, "isNegativeFloat is no longer needed because -0.0 !== 0.0 in PHP"); + } + + public function testBitCount() + { + // Positive numbers + self::assertEquals(0, TBitHelper::bitCount(0)); + self::assertEquals(1, TBitHelper::bitCount(1)); + self::assertEquals(2, TBitHelper::bitCount(2)); + self::assertEquals(2, TBitHelper::bitCount(3)); + self::assertEquals(3, TBitHelper::bitCount(4)); + self::assertEquals(3, TBitHelper::bitCount(7)); + self::assertEquals(4, TBitHelper::bitCount(8)); + self::assertEquals(4, TBitHelper::bitCount(15)); + self::assertEquals(5, TBitHelper::bitCount(16)); + self::assertEquals(5, TBitHelper::bitCount(31)); + self::assertEquals(6, TBitHelper::bitCount(32)); + self::assertEquals(6, TBitHelper::bitCount(63)); + self::assertEquals(7, TBitHelper::bitCount(64)); + self::assertEquals(7, TBitHelper::bitCount(127)); + self::assertEquals(8, TBitHelper::bitCount(128)); + self::assertEquals(8, TBitHelper::bitCount(255)); + + //Negative numbers have one extra bit to represent their negativity. + self::assertEquals(2, TBitHelper::bitCount(-1)); + self::assertEquals(3, TBitHelper::bitCount(-2)); + self::assertEquals(3, TBitHelper::bitCount(-3)); + self::assertEquals(4, TBitHelper::bitCount(-4)); + self::assertEquals(4, TBitHelper::bitCount(-7)); + self::assertEquals(5, TBitHelper::bitCount(-8)); + self::assertEquals(5, TBitHelper::bitCount(-15)); + self::assertEquals(6, TBitHelper::bitCount(-16)); + self::assertEquals(6, TBitHelper::bitCount(-31)); + self::assertEquals(7, TBitHelper::bitCount(-32)); + self::assertEquals(7, TBitHelper::bitCount(-63)); + self::assertEquals(8, TBitHelper::bitCount(-64)); + self::assertEquals(8, TBitHelper::bitCount(-127)); + self::assertEquals(9, TBitHelper::bitCount(-128)); + self::assertEquals(9, TBitHelper::bitCount(-255)); + + self::assertEquals(PHP_INT_SIZE * 8, TBitHelper::bitCount(PHP_INT_MIN + 1)); + self::assertEquals(PHP_INT_SIZE * 8 - 1, TBitHelper::bitCount(PHP_INT_MAX)); + self::assertEquals(PHP_INT_SIZE * 8, TBitHelper::bitCount(PHP_INT_MIN / 2)); + self::assertEquals(PHP_INT_SIZE * 8 - 1, TBitHelper::bitCount(PHP_INT_MIN / 2 + 1)); + } + + public function testColorBitShift() + { + try { + TBitHelper::colorBitShift(0xAA, 0, 8); + self::fail("failed to throw TInvalidDataValueException with negative inBits"); + } catch(TInvalidDataValueException $e) { + } + try { + TBitHelper::colorBitShift(0xAA, PHP_INT_SIZE * 8 + 1, 8); + self::fail("failed to throw TInvalidDataValueException with inBits too large"); + } catch(TInvalidDataValueException $e){ + } + try { + TBitHelper::colorBitShift(0xAA, 8, 0); + self::fail("failed to throw TInvalidDataValueException with negative outBits"); + } catch(TInvalidDataValueException $e) { + } + try { + TBitHelper::colorBitShift(0xAA, 8, PHP_INT_SIZE * 8 + 1); + self::fail("failed to throw TInvalidDataValueException with outBits too large"); + } catch(TInvalidDataValueException $e) { + } + + self::assertEquals(0b1011001110, TBitHelper::colorBitShift(0b1011001110001010, 16, 10)); + self::assertEquals(0b1111111111, TBitHelper::colorBitShift(0b1, 1, 10)); + self::assertEquals(0b10101010101, TBitHelper::colorBitShift(0b10, 2, 11)); + self::assertEquals(0b1101101101, TBitHelper::colorBitShift(0b110, 3, 10)); + self::assertEquals(0b101010101010101, TBitHelper::colorBitShift(0b1010, 4, 15)); + self::assertEquals(0b111011101110111, TBitHelper::colorBitShift(0b1110, 4, 15)); + self::assertEquals(0b001000100010001, TBitHelper::colorBitShift(0b0010, 4, 15)); + } + + public function testUnsignedShift() + { + self::assertEquals(0, TBitHelper::unsignedShift(0, 0)); + self::assertEquals(PHP_INT_MAX, TBitHelper::unsignedShift(PHP_INT_MAX, 0)); + self::assertEquals(PHP_INT_MIN, TBitHelper::unsignedShift(PHP_INT_MIN, 0)); + self::assertEquals(-1, TBitHelper::unsignedShift(-1, 0)); + self::assertEquals(1, TBitHelper::unsignedShift(1, 0)); + + self::assertEquals(2, TBitHelper::unsignedShift(1, -1)); + self::assertEquals(4, TBitHelper::unsignedShift(1, -2)); + self::assertEquals(8, TBitHelper::unsignedShift(2, -2)); + + self::assertEquals(0, TBitHelper::unsignedShift(1, 1)); + self::assertEquals(1, TBitHelper::unsignedShift(2, 1)); + self::assertEquals(2, TBitHelper::unsignedShift(4, 1)); + self::assertEquals(4, TBitHelper::unsignedShift(16, 2)); + + self::assertEquals(-1, -1 >> 1); + self::assertEquals(-1, -1 >> 2); + self::assertEquals(-1, -1 >> 3); + self::assertEquals(PHP_INT_MAX, TBitHelper::unsignedShift(-1, 1)); + self::assertEquals(PHP_INT_MAX >> 1, TBitHelper::unsignedShift(-1, 2)); + self::assertEquals(PHP_INT_MAX >> 2, TBitHelper::unsignedShift(-1, 3)); + } + + // Convert PHP float to Half and mini float bit representations + public function testFloatToFp16() + { + self::assertEquals(0x0000, TBitHelper::floatToFp16(0), "TBitHelper::floatToFp16 0 is not converting to 0x0000; float zero is not half float zero"); + self::assertEquals(0x3C00, TBitHelper::floatToFp16(1), "TBitHelper::floatToFp16 1 is not converting to 0x3C00; float one is not half float one"); + self::assertEquals(0x8001, TBitHelper::floatToFp16(-5.960464477539063E-8), "TBitHelper::floatToFp16 0x8001 is not converting smallest sub-normal number"); + self::assertEquals(0x03FF, TBitHelper::floatToFp16(6.097555160522461E-5), "TBitHelper::floatToFp16 0x03FF is not converting largest sub-normal number"); + self::assertEquals(0x0400, TBitHelper::floatToFp16(6.103515625E-5), "TBitHelper::floatToFp16 0x0400 is not converting smallest normal number"); + self::assertEquals(0x3555, TBitHelper::floatToFp16(0.333251953125), "TBitHelper::floatToFp16 0x3555 is not converting the nearest to 1/3"); + self::assertEquals(0x3bff, TBitHelper::floatToFp16(0.99951171875), "TBitHelper::floatToFp16 0x3bff is not converting the largest less than 1"); + self::assertEquals(0x3C01, TBitHelper::floatToFp16(1.0009765625), "TBitHelper::floatToFp16 0x3C01 is not converting the smallest more than 1"); + self::assertEquals(0x7bff, TBitHelper::floatToFp16(65504.0), "TBitHelper::floatToFp16 0x7bff is not converting the largest normal number"); + self::assertEquals(0x7C00, TBitHelper::floatToFp16(90000.0), "TBitHelper::floatToFp16 overly large numbers should become infinity"); + self::assertEquals(0x7C00, TBitHelper::floatToFp16(INF), "TBitHelper::floatToFp16 0x7C00 is not converting infinity"); + self::assertEquals(0x8000, TBitHelper::floatToFp16(-0.0), "TBitHelper::floatToFp16 0x8000 is not converting negative zero"); + self::assertEquals(0xC000, TBitHelper::floatToFp16(-2), "TBitHelper::floatToFp16 0xC000 is not converting negative two"); + self::assertEquals(0xFC00, TBitHelper::floatToFp16(-INF), "TBitHelper::floatToFp16 0xFC00 is not converting negative infinity"); + self::assertEquals(0x7FFF, TBitHelper::floatToFp16(NAN), "TBitHelper::floatToFp16 0xFC01 is not converting Not-A-Number (NaN)."); + } + + public function testFloatToBf16() + { + self::assertEquals(0x0000, TBitHelper::floatToBf16(0), "TBitHelper::floatToBf16 0 is not converting to 0x0000; float zero is not half float zero"); + self::assertEquals(0x3F80, TBitHelper::floatToBf16(1), "TBitHelper::floatToBf16 1 is not converting to 0x3F80; float one is not half float one"); + self::assertEquals(0x8001, TBitHelper::floatToBf16(-9.183549615799121E-41), "TBitHelper::floatToBf16 0x8001 is not converting smallest sub-normal number"); + self::assertEquals(0x007F, TBitHelper::floatToBf16(1.1663108012064884E-38), "TBitHelper::floatToBf16 0x03FF is not converting largest sub-normal number"); + self::assertEquals(0x0080, TBitHelper::floatToBf16(1.1754943508222875E-38), "TBitHelper::floatToBf16 0x0400 is not converting smallest normal number"); + self::assertEquals(0x3EAB, TBitHelper::floatToBf16(0.333984375), "TBitHelper::floatToBf16 0x3555 is not converting the nearest to 1/3"); + self::assertEquals(0x3F7F, TBitHelper::floatToBf16(0.99609375), "TBitHelper::floatToBf16 0x3bff is not converting the largest less than 1"); + self::assertEquals(0x3F81, TBitHelper::floatToBf16(1.0078125), "TBitHelper::floatToBf16 0x3C01 is not converting the smallest more than 1"); + self::assertEquals(0x7f7f, TBitHelper::floatToBf16(3.3895313892515355E+38), "TBitHelper::floatToBf16 0x7bff is not converting the largest normal number"); + self::assertEquals(0x7f80, TBitHelper::floatToBf16(3.3895313892515355E+39), "TBitHelper::floatToBf16 overly large numbers should become infinity"); + self::assertEquals(0x7f80, TBitHelper::floatToBf16(INF), "TBitHelper::floatToBf16 0x7f80 is not converting infinity"); + self::assertEquals(0x8000, TBitHelper::floatToBf16(-0.0), "TBitHelper::floatToBf16 0x8000 is not converting negative zero"); + self::assertEquals(0xC000, TBitHelper::floatToBf16(-2), "TBitHelper::floatToBf16 0xC000 is not converting negative two"); + self::assertEquals(0xFF80, TBitHelper::floatToBf16(-INF), "TBitHelper::floatToBf16 0xFC00 is not converting negative infinity"); + self::assertEquals(0x7FFF, TBitHelper::floatToBf16(NAN), "TBitHelper::floatToBf16 0xFC01 is not converting Not-A-Number (NaN)."); + } + + public function testFloatTFp8Range() + { + self::assertEquals(0x00, TBitHelper::floatToFp8Range(0), "TBitHelper::floatToFp8Range 0 is not converting to 0x00; float zero is not half float zero"); + self::assertEquals(0x3C, TBitHelper::floatToFp8Range(1), "TBitHelper::floatToFp8Range 1 is not converting to 0x3C; float one is not half float one"); + self::assertEquals(0x81, TBitHelper::floatToFp8Range(-1.52587890625E-5), "TBitHelper::floatToFp8Range 0x81 is not converting smallest sub-normal number"); + self::assertEquals(0x03, TBitHelper::floatToFp8Range(4.57763671875E-5), "TBitHelper::floatToFp8Range 0x03 is not converting largest sub-normal number"); + self::assertEquals(0x04, TBitHelper::floatToFp8Range(6.103515625E-5), "TBitHelper::floatToFp8Range 0x04 is not converting smallest normal number"); + self::assertEquals(0x35, TBitHelper::floatToFp8Range(0.3125), "TBitHelper::floatToFp8Range 0x35 is not converting the nearest to 1/3"); + self::assertEquals(0x3B, TBitHelper::floatToFp8Range(0.875), "TBitHelper::floatToFp8Range 0x3B is not converting the largest less than 1"); + self::assertEquals(0x3D, TBitHelper::floatToFp8Range(1.25), "TBitHelper::floatToFp8Range 0x3D is not converting the smallest more than 1"); + self::assertEquals(0x7B, TBitHelper::floatToFp8Range(57344.0), "TBitHelper::floatToFp8Range 0x7B is not converting the largest normal number"); + self::assertEquals(0x7C, TBitHelper::floatToFp8Range(64000.0), "TBitHelper::floatToFp8Range overly large numbers should become infinity"); + self::assertEquals(0x7C, TBitHelper::floatToFp8Range(INF), "TBitHelper::floatToFp8Range 0x7C is not converting infinity"); + self::assertEquals(0x80, TBitHelper::floatToFp8Range(-0.0), "TBitHelper::floatToFp8Range 0x80 is not converting negative zero"); + self::assertEquals(0xC0, TBitHelper::floatToFp8Range(-2), "TBitHelper::floatToFp8Range 0xC0 is not converting negative two"); + self::assertEquals(0xFC, TBitHelper::floatToFp8Range(-INF), "TBitHelper::floatToFp8Range 0xFC is not converting negative infinity"); + self::assertEquals(0x7F, TBitHelper::floatToFp8Range(NAN), "TBitHelper::floatToFp8Range 0x7F is not converting Not-A-Number (NaN)."); + } + + public function testFloatToFp8Precision() + { + self::assertEquals(0x00, TBitHelper::floatToFp8Precision(0), "TBitHelper::floatToFp8Precision 0 is not converting to 0x00; float zero is not half float zero"); + self::assertEquals(0x38, TBitHelper::floatToFp8Precision(1), "TBitHelper::floatToFp8Precision 1 is not converting to 0x38; float one is not half float one"); + self::assertEquals(0x81, TBitHelper::floatToFp8Precision(-0.001953125), "TBitHelper::floatToFp8Precision 0x81 is not converting smallest sub-normal number"); + self::assertEquals(0x07, TBitHelper::floatToFp8Precision(0.013671875), "TBitHelper::floatToFp8Precision 0x07 is not converting largest sub-normal number"); + self::assertEquals(0x08, TBitHelper::floatToFp8Precision(0.015625), "TBitHelper::floatToFp8Precision 0x08 is not converting smallest normal number"); + self::assertEquals(0x2B, TBitHelper::floatToFp8Precision(0.34375), "TBitHelper::floatToFp8Precision 0x2B is not converting the nearest to 1/3"); + self::assertEquals(0x37, TBitHelper::floatToFp8Precision(0.9375), "TBitHelper::floatToFp8Precision 0x37 is not converting the largest less than 1"); + self::assertEquals(0x39, TBitHelper::floatToFp8Precision(1.125), "TBitHelper::floatToFp8Precision 0x39 is not converting the smallest more than 1"); + self::assertEquals(0x77, TBitHelper::floatToFp8Precision(240.0), "TBitHelper::floatToFp8Precision 0x77 is not converting the largest normal number"); + self::assertEquals(0x78, TBitHelper::floatToFp8Precision(300.0), "TBitHelper::floatToFp8Precision overly large numbers become infinity"); + self::assertEquals(0x78, TBitHelper::floatToFp8Precision(INF), "TBitHelper::floatToFp8Precision 0x78 is not converting infinity"); + self::assertEquals(0x80, TBitHelper::floatToFp8Precision(-0.0), "TBitHelper::floatToFp8Precision 0x80 is not converting negative zero"); + self::assertEquals(0xC0, TBitHelper::floatToFp8Precision(-2), "TBitHelper::floatToFp8Precision 0xC0 is not converting negative two"); + self::assertEquals(0xF8, TBitHelper::floatToFp8Precision(-INF), "TBitHelper::floatToFp8Precision 0xF8 is not converting negative infinity"); + self::assertEquals(0x7F, TBitHelper::floatToFp8Precision(NAN), "TBitHelper::floatToFp8Precision 0x7F is not converting Not-A-Number (NaN)."); + } + + // Convert half and mini bit (float) representations to PHP Float + public function testFp16ToFloat() + { + self::assertEquals(0, TBitHelper::fp16ToFloat(0x0000), "TBitHelper::fp16ToFloat 0x0000 is not converting to 0; half float zero is not float zero"); + self::assertEquals(1, TBitHelper::fp16ToFloat(0x3C00), "TBitHelper::fp16ToFloat 0x3C00 is not converting to 1; half float one is not float one"); + self::assertEquals(-5.960464477539063E-8, TBitHelper::fp16ToFloat(0x8001), "TBitHelper::fp16ToFloat 0x8001 is not converting smallest sub-normal number"); + self::assertEquals(6.097555160522461E-5, TBitHelper::fp16ToFloat(0x03FF), "TBitHelper::fp16ToFloat 0x03FF is not converting largest sub-normal number"); + self::assertEquals(6.103515625E-5, TBitHelper::fp16ToFloat(0x0400), "TBitHelper::fp16ToFloat 0x0400 is not converting smallest normal number"); + self::assertEquals(0.333251953125, TBitHelper::fp16ToFloat(0x3555), "TBitHelper::fp16ToFloat 0x3555 is not converting the nearest to 1/3"); + self::assertEquals(0.99951171875, TBitHelper::fp16ToFloat(0x3bff), "TBitHelper::fp16ToFloat 0x3bff is not converting the largest less than 1"); + self::assertEquals(1.0009765625, TBitHelper::fp16ToFloat(0x3C01), "TBitHelper::fp16ToFloat 0x3C01 is not converting the smallest more than 1"); + self::assertEquals(65504, TBitHelper::fp16ToFloat(0x7bff), "TBitHelper::fp16ToFloat 0x7bff is not converting the largest normal number"); + self::assertEquals(INF, TBitHelper::fp16ToFloat(0x7C00), "TBitHelper::fp16ToFloat 0x7C00 is not converting infinity"); + self::assertTrue(TBitHelper::isNegativeZero(TBitHelper::fp16ToFloat(0x8000)), "TBitHelper::fp16ToFloat 0x8000 is not converting negative zero"); + self::assertEquals(-2, TBitHelper::fp16ToFloat(0xC000), "TBitHelper::fp16ToFloat 0xC000 is not converting negative two"); + self::assertEquals(-INF, TBitHelper::fp16ToFloat(0xFC00), "TBitHelper::fp16ToFloat 0xFC00 is not converting negative infinity"); + self::assertTrue(is_nan(TBitHelper::fp16ToFloat(0xFC01)), "TBitHelper::fp16ToFloat 0xFC01 is not converting Not-A-Number (NaN)"); + self::assertTrue(is_nan(TBitHelper::fp16ToFloat(0x7FFF)), "TBitHelper::fp16ToFloat 0x7FFFis not converting Not-A-Number (NaN)"); + } + + public function testBf16ToFloat() + { + self::assertEquals(0, TBitHelper::bf16ToFloat(0x0000), "TBitHelper::bf16ToFloat 0x0000 is not converting to 0; half float zero is not float zero"); + self::assertEquals(1, TBitHelper::bf16ToFloat(0x3F80), "TBitHelper::bf16ToFloat 0x3F80 is not converting to 1; half float one is not float one"); + self::assertEquals(-9.183549615799121E-41, TBitHelper::bf16ToFloat(0x8001), "TBitHelper::bf16ToFloat 0x8001 is not converting smallest sub-normal number"); + self::assertEquals(1.1663108012064884E-38, TBitHelper::bf16ToFloat(0x007F), "TBitHelper::bf16ToFloat 0x007F is not converting largest sub-normal number"); + self::assertEquals(1.1754943508222875E-38, TBitHelper::bf16ToFloat(0x0080), "TBitHelper::bf16ToFloat 0x0080 is not converting smallest normal number"); + self::assertEquals(0.333984375, TBitHelper::bf16ToFloat(0x3EAB), "TBitHelper::bf16ToFloat 0x3EAB is not converting the nearest to 1/3"); + self::assertEquals(0.99609375, TBitHelper::bf16ToFloat(0x3F7F), "TBitHelper::bf16ToFloat 0x3F7F is not converting the largest less than 1"); + self::assertEquals(1.0078125, TBitHelper::bf16ToFloat(0x3F81), "TBitHelper::bf16ToFloat 0x3F81 is not converting the smallest more than 1"); + self::assertEquals(3.3895313892515355E+38, TBitHelper::bf16ToFloat(0x7f7f), "TBitHelper::bf16ToFloat 0x7f7f is not converting the largest normal number"); + self::assertEquals(INF, TBitHelper::bf16ToFloat(0x7f80), "TBitHelper::bf16ToFloat 0x7f80 is not converting infinity"); + self::assertTrue(TBitHelper::isNegativeZero(TBitHelper::bf16ToFloat(0x8000)), "TBitHelper::bf16ToFloat 0x8000 is not converting negative zero"); + self::assertEquals(-2, TBitHelper::bf16ToFloat(0xC000), "TBitHelper::bf16ToFloat 0xC000 is not converting negative two"); + self::assertEquals(-INF, TBitHelper::bf16ToFloat(0xFF80), "TBitHelper::bf16ToFloat 0xFF80 is not converting negative infinity"); + self::assertTrue(is_nan(TBitHelper::bf16ToFloat(0xFF81)), "TBitHelper::bf16ToFloat 0xFF81 is not converting Not-A-Number (NaN)"); + } + + public function testFp8RangeToFloat() + { + self::assertEquals(0, TBitHelper::fp8RangeToFloat(0x00), "TBitHelper::fp8RangeToFloat 0x00 is not converting to 0; half float zero is not float zero"); + self::assertEquals(1, TBitHelper::fp8RangeToFloat(0x3C), "TBitHelper::fp8RangeToFloat 0x3C is not converting to 1; half float one is not float one"); + self::assertEquals(-1.52587890625E-5, TBitHelper::fp8RangeToFloat(0x81), "TBitHelper::fp8RangeToFloat 0x81 is not converting smallest sub-normal number"); + self::assertEquals(4.57763671875E-5, TBitHelper::fp8RangeToFloat(0x03), "TBitHelper::fp8RangeToFloat 0x03 is not converting largest sub-normal number"); + self::assertEquals(6.103515625E-5, TBitHelper::fp8RangeToFloat(0x04), "TBitHelper::fp8RangeToFloat 0x04 is not converting smallest normal number"); + self::assertEquals(0.3125, TBitHelper::fp8RangeToFloat(0x35), "TBitHelper::fp8RangeToFloat 0x35 is not converting the nearest to 1/3"); + self::assertEquals(0.875, TBitHelper::fp8RangeToFloat(0x3B), "TBitHelper::fp8RangeToFloat 0x3B is not converting the largest less than 1"); + self::assertEquals(1.25, TBitHelper::fp8RangeToFloat(0x3D), "TBitHelper::fp8RangeToFloat 0x3D is not converting the smallest more than 1"); + self::assertEquals(57344.0, TBitHelper::fp8RangeToFloat(0x7B), "TBitHelper::fp8RangeToFloat 0x7B is not converting the largest normal number"); + self::assertEquals(INF, TBitHelper::fp8RangeToFloat(0x7C), "TBitHelper::fp8RangeToFloat 0x7C is not converting infinity"); + self::assertTrue(TBitHelper::isNegativeZero(TBitHelper::fp8RangeToFloat(0x80)), "TBitHelper::fp8RangeToFloat 0x80 is not converting negative zero"); + self::assertEquals(-2, TBitHelper::fp8RangeToFloat(0xC0), "TBitHelper::fp8RangeToFloat 0xC0 is not converting negative two"); + self::assertEquals(-INF, TBitHelper::fp8RangeToFloat(0xFC), "TBitHelper::fp8RangeToFloat 0xFC is not converting negative infinity"); + self::assertTrue(is_nan(TBitHelper::fp8RangeToFloat(0x7F)), "TBitHelper::fp8RangeToFloat 0x7F is not converting Not-A-Number (NaN)"); + } + + public function testFp8PrecisionToFloat() + { + self::assertEquals(0, TBitHelper::fp8PrecisionToFloat(0x00), "TBitHelper::fp8PrecisionToFloat 0x00 is not converting to 0; half float zero is not float zero"); + self::assertEquals(1, TBitHelper::fp8PrecisionToFloat(0x38), "TBitHelper::fp8PrecisionToFloat 0x38 is not converting to 1; half float one is not float one"); + self::assertEquals(-0.001953125, TBitHelper::fp8PrecisionToFloat(0x81), "TBitHelper::fp8PrecisionToFloat 0x81 is not converting smallest sub-normal number"); + self::assertEquals(0.013671875, TBitHelper::fp8PrecisionToFloat(0x07), "TBitHelper::fp8PrecisionToFloat 0x07 is not converting largest sub-normal number"); + self::assertEquals(0.015625, TBitHelper::fp8PrecisionToFloat(0x08), "TBitHelper::fp8PrecisionToFloat 0x08 is not converting smallest normal number"); + self::assertEquals(0.34375, TBitHelper::fp8PrecisionToFloat(0x2B), "TBitHelper::fp8PrecisionToFloat 0x2B is not converting the nearest to 1/3"); + self::assertEquals(0.9375, TBitHelper::fp8PrecisionToFloat(0x37), "TBitHelper::fp8PrecisionToFloat 0x37 is not converting the largest less than 1"); + self::assertEquals(1.125, TBitHelper::fp8PrecisionToFloat(0x39), "TBitHelper::fp8PrecisionToFloat 0x39 is not converting the smallest more than 1"); + self::assertEquals(240.0, TBitHelper::fp8PrecisionToFloat(0x77), "TBitHelper::fp8PrecisionToFloat 0x77 is not converting the largest normal number"); + self::assertEquals(INF, TBitHelper::fp8PrecisionToFloat(0x78), "TBitHelper::fp8PrecisionToFloat 0x78 is not converting infinity"); + self::assertTrue(TBitHelper::isNegativeZero(TBitHelper::fp8PrecisionToFloat(0x80)), "TBitHelper::fp8PrecisionToFloat 0x80 is not converting negative zero"); + self::assertEquals(-2, TBitHelper::fp8PrecisionToFloat(0xC0), "TBitHelper::fp8PrecisionToFloat 0xC0 is not converting negative two"); + self::assertEquals(-INF, TBitHelper::fp8PrecisionToFloat(0xF8), "TBitHelper::fp8PrecisionToFloat 0xF8 is not converting negative infinity"); + self::assertTrue(is_nan(TBitHelper::fp8PrecisionToFloat(0x7F)), "TBitHelper::fp8PrecisionToFloat 0x7F is not converting Not-A-Number (NaN)"); + } + + + public function testMirrorBits() + { + self::assertEquals(0, TBitHelper::mirrorBits(3, 0)); + + $size = PHP_INT_SIZE * 8; + for ($bits = 1; $bits < $size; $bits++) { + for ($bit = 0, $n = 1, $ref = 1 << ($bits - 1); $bit < $bits; $bit++) { + self::assertEquals($ref, $r = TBitHelper::mirrorBits($n, $bits), "$bits bits being reversed: '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + $ref >>= 1; + } + } + self::assertEquals(0, TBitHelper::mirrorBits(3, -1)); + } + + public function testMirrorByte() + { + $bits = PHP_INT_SIZE * 8; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $base = (($bit >> 3) << 3); + $ref = TBitHelper::mirrorBits($n >> $base, 8) << $base; + self::assertEquals($ref, $r = TBitHelper::mirrorByte($n), "$bits bits being reversed: '" . decbin($n) . "' => '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + } + } + + public function testMirrorShort() + { + $bits = PHP_INT_SIZE * 8; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $base = (($bit >> 4) << 4); + $ref = TBitHelper::mirrorBits($n >> $base, 16) << $base; + self::assertEquals($ref, $r = TBitHelper::mirrorShort($n), "$bits bits being reversed: '" . decbin($n) . "' => '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + } + } + + public function testMirrorLong() + { + $bits = PHP_INT_SIZE * 8; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $base = (($bit >> 5) << 5); + $ref = TBitHelper::mirrorBits($n >> $base, 32) << $base; + self::assertEquals($ref, $r = TBitHelper::mirrorLong($n), "$bits bits being reversed: '" . decbin($n) . "' => '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + } + } + + public function testMirrorLongLong() + { + if (PHP_INT_SIZE === 4) { + $this->markTestSkipped("Cannot run " . TBitHelper::class . "::mirrorLongLong on 32 bit systems"); + } else { + $bits = PHP_INT_SIZE * 8; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $base = (($bit >> 6) << 6); + $ref = TBitHelper::mirrorBits($n >> $base, 64) << $base; + self::assertEquals($ref, $r = TBitHelper::mirrorLongLong($n), "$bit/$bits bits being reversed: '" . decbin($n) . "' => \n'" . decbin($r) . "' is not the reference value of \n'" . decbin($ref) . "'."); + $n <<= 1; + } + } + } + + public function testFlipEndianShort() + { + $bits = 16; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $d = pack('n', $n); + $c = $d[0]; + $d[0] = $d[1]; + $d[1] = $c; + $ref = unpack('n', $d)[1]; + self::assertEquals($ref, $r = TBitHelper::flipEndianShort($n), "flip endian short: '" . decbin($n) . "' => '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + } + } + + public function testFlipEndianLong() + { + $bits = 32; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $d = pack('N', $n); + $c = $d[0]; + $d[0] = $d[3]; + $d[3] = $c; + $c = $d[1]; + $d[1] = $d[2]; + $d[2] = $c; + $ref = unpack('N', $d)[1]; + self::assertEquals($ref, $r = TBitHelper::flipEndianLong($n), "flip endian long: '" . decbin($n) . "' => '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + } + } + + public function testFlipEndianLongLong() + { + if (PHP_INT_SIZE === 4) { + $this->markTestSkipped("Cannot run " . TBitHelper::class . "::flipEndianLongLong on 32 bit systems"); + } else { + $bits = 64; + for ($bit = 0, $n = 1; $bit < $bits; $bit++) { + $d = pack('Q', $n); + $c = $d[0]; + $d[0] = $d[7]; + $d[7] = $c; + + $c = $d[1]; + $d[1] = $d[6]; + $d[6] = $c; + + $c = $d[2]; + $d[2] = $d[5]; + $d[5] = $c; + + $c = $d[3]; + $d[3] = $d[4]; + $d[4] = $c; + $ref = unpack('Q', $d)[1]; + self::assertEquals($ref, $r = TBitHelper::flipEndianLongLong($n), "flip endian long long: '" . decbin($n) . "' => '" . decbin($r) . "' is not the reference value of " . decbin($ref) . "'."); + $n <<= 1; + } + } + } + + +}