Hack#97

Piece Framework 勉強会の後の懇親会で出た(と思う)正規表現を使わずに整数であるかどうかを確認するにはどうするか?というネタと、Binary Hacks の Hack#97 - 浮動小数点数のビット列表現 を Pure PHP でやってみる、の両方をいっぺんに。ctype_digit() は一文字目が符号だとアウトなので、あえて使わずに。

<?php

/**
 * Parse the value as an integer.
 *
 * @param mixed $num       Numerical value.
 * @param bool  $allow_hex Whether allows to parse the hexadecimal string.
 *      If set to true, hexadecimal string is handled as 32-bit signed integer.
 *      The hexadecimal string must be lead with "0x" or "0X".
 *      The default is true.
 * @return int False is returned if non-numerical value or fractional number given.
 */
function parse_int($num, $allow_hex = true)
{
    if (is_int($num)) {
        return $num;
    } elseif (is_float($num)) {
        return parse_int_cast($num);
    } elseif (is_string($num)) {
        if (stripos($num, '0x') === 0 && ctype_xdigit(substr($num, 2))) {
            // hexadecimal string, lead with "0x" or "0X"
            $hex = substr($num, 2);
        } elseif (is_numeric($num)) {
            // numerical string
            return parse_int_cast($num);
        } else {
            return false;
        }
    } else {
        // boolean, array, object and resource
        return false;
    }

    // parse hexadecimal string as 32-bit signed integer
    if (!$allow_hex || strlen($hex) > 8) {
        return false;
    }
    if (strlen($hex) == 8 && strpos('01234567', $hex[0]) === false) {
        $hex = (string)(hexdec($hex[0]) - 8) . substr($hex, 1);
        return hexdec($hex) - 2147483647 - 1;
    } else {
        return hexdec($hex);
    }
}

/**
 * Parse the value as a fraction.
 *
 *
 * @param mixed $num       Numerical value.
 * @param bool  $allow_hex Whether allows to parse the hexadecimal string.
 *      If set to true, the hexadecimal string is handled as IEEE-754 format.
 *      The hexadecimal string must be lead with "0x" or "0X".
 *      The result of parsing hexadecimal may not be accurate because
 *      be calculated twice or more: at least in C-level and in PHP-level.
 *      The default is true.
 * @return float False is returned if non-numerical value or infinite number given.
 */
function parse_float($num, $allow_hex = true)
{
    if (is_int($num)) {
        return parse_float_cast($num);
    } elseif (is_float($num)) {
        return parse_float_cast($num);
    } elseif (is_string($num)) {
        if (stripos($num, '0x') === 0 && ctype_xdigit(substr($num, 2))) {
            // hexadecimal string, lead with "0x" or "0X"
            $hex = substr($num, 2);
        } elseif (is_numeric($num)) {
            // numerical string
            return parse_float_cast($num);
        } else {
            return false;
        }
    } else {
        // boolean, array, object and resource
        return false;
    }

    // parse hexadecimal string as IEEE-754 fromat
    // NOTE: using pack() and unpack() is accurate, but machine-dependent
    if (!$allow_hex || strlen($hex) > 16) {
        return false;
    }
    $bin = parse_float_hex2bin($hex);
    if (strlen($bin) == 32) {
        // IEEE-754 single precision
        $sign = (substr($bin, 0, 1) == '1') ? -1 : 1;
        $exponent = pow(2, bindec(substr($bin, 1, 8)) - 127);
        $mantissa = parse_float_mantissa_bin2float(substr($bin, -23));
    } else {
        // IEEE-754 double precision
        $sign = (substr($bin, 0, 1) == '1') ? -1 : 1;
        $exponent = pow(2, bindec(substr($bin, 1, 11)) - 1023);
        $mantissa = parse_float_mantissa_bin2float(substr($bin, -52));
    }
    return parse_float_cast($sign * $exponent * $mantissa);
}

/**
 * Parse the number as an integer.
 *
 * @param mixed $num Numerical value.
 * @return int False is returned if fractional number given.
 */
function parse_int_cast($num)
{
    $i = (int)$num;
    $f = (float)$num;
    if ($i != $f) {
        return false;
    }
    return $i;
}

/**
 * Parse the number as a fraction.
 *
 * @param mixed $num  Numerical value.
 * @return float False is returned if infinite number or not a number given.
 */
function parse_float_cast($num)
{
    $f = (float)$num;
    if (is_infinite($f) || is_nan($f)) {
        return false;
    }
    return $f;
}

/**
 * Convert the number from hexadecimal representation to binary representation.
 *
 * @param string $hex Hexadecimal number.
 * @return string The empty bits are padded with 0's per 32-bit.
 */
function parse_float_hex2bin($hex)
{
    if ($mod = strlen($hex) % 4) {
        $hex = str_repeat('0', 4 - $mod) . $hex;
    }
    $bin = implode('', array_map('parse_float_hex2bin_chunk', str_split($hex, 4)));
    if ($mod = strlen($bin) % 32) {
        $bin = str_repeat('0', 32 - $mod) . $bin;
    }
    return $bin;
}

/**
 * Convert the number from hexadecimal representation to binary representation.
 * Called from parse_float_hex2bin() per 16-bit to avoid integer overflow.
 *
 * @param string $hex Hexadecimal number.
 * @return string The empty bits are padded with 0's.
 */
function parse_float_hex2bin_chunk($hex)
{
    return str_pad(decbin(hexdec($hex)), 16, '0', STR_PAD_LEFT);
}

/**
 * Parse the IEEE-754 mantissa.
 * Convert from the binary representation to the fraction number.
 *
 * @param string $mantissa Binary representation of the IEEE-754 mantissa.
 * @return float
 */
function parse_float_mantissa_bin2float($mantissa) {
    $m = 1.0;
    for ($i = 0; $i < strlen($mantissa); $i++) {
        if ($mantissa[$i] == '1') {
            $m += pow(2, -($i + 1));
        }
    }
    return $m;
}

/**
 * for PHP 4.x
 */
if (!function_exists('stripos') && !@include_once 'PHP/Compat/Function/stripos.php') {
    /**
     * Returns the numeric position of the first occurrence
     * of needle in the haystack string.
     *
     * @param string $haystack String to be scanned.
     * @param string $needle   String to be searched.
     * @param int    $offset   Positon to start searching.
     * @return int False is returned if $needle is not found.
     *
     * @see PERA::PHP_Compat's PHP/Compat/Function/stripos.php
     */
    function stripos($haystack, $needle, $offset = 0)
    {
        return strpos(strtoupper($haystack), strtoupper($needle), $offset);
    }
}
if (!function_exists('str_split') && !@include_once 'PHP/Compat/Function/str_split.php') {
    /**
     * Convert a string to an array.
     *
     * @param string $string       String to be splitted.
     * @param int    $split_length Length of chunks. The default is 1.
     * @return array False is returned if $split_length is less than 1.
     *
     * @see PERA::PHP_Compat's PHP/Compat/Function/str_split.php
     */
    function str_split($string, $split_length = 1)
    {
        if ($split_length < 1) {
            trigger_error("str_split(): The length of each segment"
                        . " must be greater than zero.", E_USER_WARNING);
            return false;
        }
        $chunk = array();
        $limit = strlen($string);
        for ($pos = 0; $pos < $limit; $pos += $split_length) {
            $chunk[] = substr($string, $pos, $split_length);
        }
        return $chunk;
    }
}

?>