Надежное обнаружение целочисленного переполнения / потери значимости

Я работаю над кодом, который должен делать следующее с результатом вычисления:

Если результат превышает предел, который может быть представлен в целочисленном типе PHP, генерируется исключение.

Если результат не превышает этот предел, но все же привело к генерации числа с плавающей запятой, выдайте предупреждение и округлите результат до целого числа.

Для этого я реализовал следующий метод:

const MAX = PHP_INT_MAX;
const MIN = (PHP_INT_MAX * -1) -1;

private function validateResult ($result)
{
    // Check that we still have an integer
    if (!is_int ($result))
    {
        // If the result is out of bounds for an integer then throw an exception
        if (($result > static::MAX) || ($result < static::MIN ))
        {
            // We've gone out of bounds
            throw new exception\AmountRangeException ("New value exceeds the limits of integer storage");
        }

        // If the result can be rounded into an integer then do so and issue
        // a warning.  
        trigger_error ("A non-integer value of $result resulted and has been rounded", E_USER_NOTICE);
        $result = (int) round ($result);
    }

    return $result;
}

Однако он не проходит модульное тестирование при попытке добавить 1 к PHP_INT_MAX. В интерактивном режиме PHP я пробовал следующее:

php > var_dump (PHP_INT_MAX);
int(9223372036854775807)
php > var_dump (PHP_INT_MAX + 1);
double(9.2233720368548E+18)
php > var_dump ((PHP_INT_MAX + 1) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 10) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 100) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 1000) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 10000) > PHP_INT_MAX);
bool(true)

Таким образом, похоже, что мой код обнаружения будет работать только в том случае, если результат выйдет примерно на 5 порядков за пределы допустимого диапазона.

Поскольку я хочу, чтобы суммы, которые генерируют значение с плавающей запятой для передачи, при условии, что результат может быть округлен до целого числа, просто генерируя исключение, если результат не является int, не будет соответствовать требованиям.

Есть ли надежный способ обнаружить, что число превышает целочисленный диапазон, даже на небольшую величину?

ОБНОВЛЕНИЕ. Дальнейшее исследование показывает, что значение может превысить 1025, прежде чем оно действительно будет считаться большим, чем PHP_INT_MAX.

php > var_dump ((PHP_INT_MAX + 1025) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 1026) > PHP_INT_MAX);
bool(true)

ОБНОВЛЕНИЕ 2: я внедрил временное исправление, но это исправление действительно хакерское и неэлегантное, поэтому я оставляю этот вопрос открытым в надежде, что у кого-то есть лучшее предложение.

if ((($result > static::MAX) || (($result == static::MAX) && ((string) $result != (string) static::MAX))) 
|| (($result < static::MIN) || (($result == static::MIN) && ((string) $result != (string) static::MIN)))) {}

Идея состоит в том, что если числа математически одинаковы согласно сравнению PHP, но они не те же самые после того, как числа были преобразованы в строку, тогда они должны быть переполнены, но меньше, чем может быть обнаружено с помощью> или ‹ сравнение. Кажется, это работает в модульном тестировании, но я действительно не думаю, что это лучшее решение, и в настоящее время я создаю более строгий набор модульных тестов, чтобы увидеть, что происходит со значениями чуть ниже границы, чуть выше нее или точно на ней. .

ОБНОВЛЕНИЕ 3: описанный выше подход не работает с отрицательным переполнением. Если результат вызывает отрицательное переполнение, результат будет двойным, но его значение останется таким же, как (PHP_INT_MAX * 1) - 1

php > var_dump ((PHP_INT_MAX * -1) - 1);
int(-9223372036854775808)
php > var_dump ((PHP_INT_MAX * -1) - 2);
double(-9223372036854775808)

person GordonM    schedule 22.02.2015    source источник


Ответы (1)


Оказывается, когда я подумал, ответ был невероятно простым. Все, что потребовалось, - это переопределить константы MIN и MAX, чтобы они не были максимально возможными положительными и отрицательными целыми числами, а определяли их как самые большие значения, которые, когда проверяемое значение и значения MIN / MAX преобразуются в float, тестируемое значение будет по-прежнему находиться в диапазоне MIN / MAX.

Эксперименты показали, что этого можно достичь, сделав пределы 512 меньше абсолютного предела.

const MAX   = PHP_INT_MAX - 512;
const MIN   = (PHP_INT_MAX * -1) + 512;

Теперь любое значение вне этого диапазона может быть обнаружено независимо от того, происходит ли приведение к типу float или нет.

По-прежнему есть некоторые проблемы с этим подходом (зона отсрочки, вероятно, не должна быть чем-то вроде этой большой в 32-битной системе), но это гораздо более элегантное исправление, чем жонглирование типами и сравнение строк.

person GordonM    schedule 23.02.2015