5

Can anyone explain why NAN and a variable equal to NAN behave differently depending on the version of PHP?

Consider the following code:

$nan = NAN;
print "PHP Version: " . phpversion(). "\n" .
  '0 <  NAN ? ' . ( 0 < NAN   ? 'TRUE' : 'FALSE' ) . "\n" .
  '0 >  NAN ? ' . ( 0 > NAN   ? 'TRUE' : 'FALSE' ) . "\n" . 
  '0 == NAN ? ' . ( 0 == NAN  ? 'TRUE' : 'FALSE' ) . "\n" . 
  '0 <  $nan ? ' . ( 0 < $nan  ? 'TRUE' : 'FALSE' ) . "\n" .
  '0 >  $nan ? ' . ( 0 > $nan  ? 'TRUE' : 'FALSE' ) . "\n" .
  '0 == $nan ? ' . ( 0 == $nan ? 'TRUE' : 'FALSE' ) . "\n" .
  'is_nan(NAN) ' . ( is_nan(NAN)  ? 'TRUE' : 'FALSE' ) . "\n" .
  'is_nan($nan) ' . ( is_nan($nan) ? 'TRUE' : 'FALSE' ) . "\n" .
  'gettype(NAN) is '  . gettype(NAN)  . "\n" .
  'gettype($nan) is ' . gettype($nan) . "\n";

Now, if I run this code against many versions of PHP (using MAMP) the following are results:

PHP Version: 5.3.5
0 <  NAN ? TRUE
0 >  NAN ? TRUE
0 == NAN ? FALSE
0 <  $nan ? TRUE
0 >  $nan ? TRUE
0 == $nan ? FALSE
is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double

PHP Version: 5.6.30 (and 5.5.30, 5.4.45)
0 <  NAN ? FALSE
0 >  NAN ? FALSE
0 == NAN ? FALSE
0 <  $nan ? FALSE
0 >  $nan ? FALSE
0 == $nan ? FALSE
is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double

PHP Version: 7.1.1 (and 7.0.15)
0 <  NAN ? TRUE
0 >  NAN ? TRUE
0 == NAN ? FALSE
0 <  $nan ? FALSE
0 >  $nan ? FALSE
0 == $nan ? FALSE
is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double

Can a function in PHP rely on a comparison to NAN or should NAN only be used with is_nan()?

Andy
  • 53
  • 1
  • 4

2 Answers2

3

This bug has now been fixed by this commit. The explanation bellow shows what was causing it.

About your first question, it seems that since PHP7 you get different results depending on whether the expression is evaluated during compilation or during runtime.

First off, it is important to note that according to IEEE754 all comparisions where one of the elements is a NAN, should return false. You can find some details in this answer. Taking that into account, the behavior of 5.6 seems to be the correct one.


So, for the case of runtime evaluation(0 < $nan), that comparison is done in the VM by this code:

result = ((double)Z_LVAL_P(op1) < Z_DVAL_P(op2));

That operation will assign 0 to result which is eventually returned. This will give us the expected output of FALSE.


In the case of compile time evaluation(0 < NAN), the comparison ir done during compilation by this code:

case TYPE_PAIR(IS_LONG, IS_DOUBLE):
    Z_DVAL_P(result) = (double)Z_LVAL_P(op1) - Z_DVAL_P(op2);
    ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_DVAL_P(result)));
    return SUCCESS;

Here there are some other things happening, the result of the subtraction 0 - NAN = NAN that value then goes through the ZEND_NORMALIZE_BOOL macro that reads as follows:

#define ZEND_NORMALIZE_BOOL(n)          \
    ((n) ? (((n)>0) ? 1 : -1) : 0)

As you can see, if (n) > 0 is false (which is the case for NAN), the macro will return -1. That value is then passed to the is_smaller_function where you'll find:

ZVAL_BOOL(result, (Z_LVAL_P(result) < 0));

Given that at this point result holds -1, this will be evaluated to TRUE.


About your second question, I would suggest you should never rely on comparisons with NAN and stick to using is_nan(). Note that even NAN == NAN evaluates to false.

pmmaga
  • 410
  • 7
  • 18
  • Thanks for the detective work. It would seem that it is an oddity that in 7+ you get the TRUE result in compile-based evaluation. The take home for me is to avoid the potential for any NAN comparisons... – Andy Jul 22 '17 at 23:54
  • It was actually fun to figure out. :) FYI, a bug was reported: https://bugs.php.net/bug.php?id=74974 – pmmaga Jul 23 '17 at 00:15
1

This is what the documentation says about NaN:

NaN

Some numeric operations can result in a value represented by the constant NAN. This result represents an undefined or unrepresentable value in floating-point calculations. Any loose or strict comparisons of this value against any other value, including itself, but except TRUE, will have a result of FALSE.

Because NAN represents any number of different values, NAN should not be compared to other values, including itself, and instead should be checked for using is_nan().

In plain English, NaN is not a value. It is a notation that denotes a result that cannot be computed or stored using the internal representation of real numbers using the floating point standard.

NaN is not equal to anything else. NaN is not equal even with another NaN because many different computations can produce NaN.

The only reliable way to check if a result is NaN is to use the is_nan() function.

Only the last four tests from your code are valid:

is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double
axiac
  • 68,258
  • 9
  • 99
  • 134