2

I'm working on code that has to do the following with the result of a calculation:

If the result exceeds the limit that can be represented in PHP's integer type then throw an exception.

If the result doesn't exceed that limit but it did result in a float being generated, issue a warning and round the result to an integer.

I've implemented the following method to do this:

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;
}

However it fails unit testing when attempting to add 1 to PHP_INT_MAX. I tried the following in PHP interactive mode:

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)

So it looks like my detection code will only work if the result goes about 5 orders of magnitude out of range.

As I want sums that generate a float to pass provided the result can be rounded to an integer, simply throwing an exception if the result isn't an int wouldn't meet the requirements.

Is there a reliable way of detecting that a number has exceeded integer range, even by a small amount?

UPDATE: Further investigation shows that the value can go over by up to 1025 before it's actually considered bigger than 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)

UPDATE 2: I've implemented a provisional fix, but that fix is really hacky and inelegant so I'm leaving this question open in the hope that someone has a better suggestion.

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

The idea is that if the numbers are mathematically the same according to a PHP comparison, but they're not the same after the numbers have been cast to string then they must have overflowed, but by less than can be detected with a > or < comparison. This seems to work in unit testing, but I really don't think this is the best solution and am currently constructing a more rigorous set of unit tests to see what happens with values just below the boundary, just above it, or exactly on it.

UPDATE 3: The above approach won't work with negative overflow. If the result triggers a negative overflow the result is a double, but its value is still the same as (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)

1 Answer 1

0

Turns out the answer was incredibly simple once I thought of it. All it took was to redefine the MIN and MAX constants to not be the biggest possible positive and negative integer values, but to define them as the biggest values that when the value being tested and the MIN/MAX values are both cast to float, the value under test will still be within the range of MIN/MAX.

Experimentation has shown that making the limits 512 short of the absolute limit achieves this.

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

Now any value outside that range can be detected regardless of whether a cast to float occurs or not.

There is still some issues with this approach (the backoff zone probably doesn't need to be anything like this big on a 32 bit system) but it's a much more elegant fix than type juggling and string comparisons.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.