PHP の max() と min() に NAN が混ざると、答えが引数の順番で変わります。同じ順番でも、PHP のバージョンによって答えが逆になりました。PHP 7.4.33、8.0.30、8.3.6、8.4.21 で測った結果です。
| 式 | 7.4.33 | 8.0.30 | 8.3.6 | 8.4.21 |
|---|---|---|---|---|
max(NAN, 842.0) |
842.0 | 842.0 | NAN | 842.0 |
max(842.0, NAN) |
NAN | NAN | 842.0 | NAN |
min(NAN, 842.0) |
NAN | NAN | NAN | 842.0 |
min(842.0, NAN) |
842.0 | 842.0 | 842.0 | NAN |
max([NAN, 1.0, 2.0]) |
NAN | NAN | NAN | NAN |
max([1.0, NAN, 2.0]) |
2.0 | 2.0 | 2.0 | 2.0 |
max([1.0, 2.0, NAN]) |
2.0 | 2.0 | 2.0 | 2.0 |
配列を1つ渡す形では、4つとも同じでした。先頭が NAN なら NAN、それ以外の場所なら NAN は無視されます。引数を2つ並べる形では、8.3.6 だけ max() の答えが逆になり、8.4.21 では min() の答えが 8.0 までと逆になりました。8.3.6 は Ubuntu 24.04 のパッケージです。8.2 は手元に用意できず、測っていません。
先に断っておくと、NAN はどんな値と比べても「大きい」「小さい」「等しい」のどれにもならない値で、これは PHP に限らない IEEE 754 の決まりです。PHP のマニュアルの max() には、型の違う値を渡すと予測できない結果になりうる、という注意があります。NAN についての記述はありませんでした。ここで書くのは、それが自作の WordPress プラグインで実際にどう効いたかと、手元の4つのバージョンで並べて測った結果です。
NAN はどこから来たか
自作の Rapls PDF Image Creator は、PDF のページの枠(/MediaBox [0 0 595 842] など)をファイルから読んで、ページの大きさを見積もっています。大きすぎるページは、解像度を下げて描くためです。
枠の数は、文字列から (float) で読みます。ここに、400桁の数を書いた PDF が来ると、こうなります。
| 式 | 4つのバージョンとも |
|---|---|
(float) '1' . str_repeat('0', 400) |
INF |
INF - INF |
NAN |
(float) 'nan' |
0.0 |
NAN == NAN |
false |
NAN <=> 1.0 |
1 |
1.0 <=> NAN |
1 |
(int) NAN |
0 |
json_encode(NAN) |
false(Inf and NaN cannot be JSON encoded) |
'nan' という文字列は 0.0 になるので、外から NAN を直接渡されることはありません。でも、とても大きな数は INF になり、枠の幅は「右端 - 左端」で求めるので、両端が INF なら幅は NAN です。
<=> は、どちらの向きで比べても 1 を返しました。NAN と比べると「左のほうが大きい」とも「右のほうが大きい」とも言える、ということです。
大きい枠のあとに NAN が来ると
プラグインは、ファイルの中の枠をすべて読んで、いちばん大きい面積を max() で更新していました。
$area = 0.0;
foreach ($areas as $a) {
$area = max($area, $a);
}
ここに、14,400pt 角の大きな枠、NAN になる枠、Letter、A4 の順で面積が来たときの結果です。
| バージョン |
$area の最後の値 |
|---|---|
| 7.4.33 | 500,990(A4) |
| 8.0.30 | 500,990(A4) |
| 8.3.6 | 207,360,000(14,400pt 角) |
| 8.4.21 | 500,990(A4) |
8.3.6 以外では、NAN のところで max(207360000.0, NAN) が NAN になり、次の max(NAN, 612*792) で NAN が捨てられて Letter になります。いちばん大きい枠は消えて、A4 が「最大」として残りました。プラグインはこれを見て、普通の大きさのページとして 150 dpi で描こうとします。
プラグインの 1.4.39 のコードのコメントには「PHP 8.2 以前では、引数の順番で答えが決まる」と書いてあります。手元で測った範囲では、8.4.21 でも同じことが起きました。
1.4.39 の直し方
NAN と INF を、max() に渡す前に止めます。
$width = abs((float) $x1 - (float) $x0);
$height = abs((float) $y1 - (float) $y0);
if (!is_finite($width) || !is_finite($height) || !is_finite($width * $height)) {
return null; // 大きさとして読めないものは、答えない
}
$area = max($area, $width * $height);
is_finite() は、NAN と INF と -INF のどれでも false を返します。INF も、ページの大きさとしては使えない値なので、NAN と一緒に止めています。プラグインは null を返したとき、ページを小さい解像度で実際に描いて測る方に回します。
max() と min() を使うとき
- NAN が混ざると、
max()とmin()の答えは引数の順番で変わる - 同じ順番でも、PHP のバージョンで答えが変わる(8.3.6 の
max()、8.4.21 のmin()) - 配列を1つ渡す形は、手元の4バージョンでは同じだったが、先頭の NAN だけは残る
- 外から来た数で計算するなら、
max()に渡す前にis_finite()で止める - 文字列の
'nan'は 0.0 になるが、とても大きな数は INF になり、INF - INF で NAN ができる
あなたのコードの max() に、外から来た数が入っていませんか。
手元で試すコマンド
<?php
// nan.php
$e = fn($v) => is_float($v) && is_nan($v) ? 'NAN' : var_export($v, true);
$inf = (float) ('1' . str_repeat('0', 400));
$nan = $inf - $inf;
$r = [
'max(NAN, 842.0)' => max(NAN, 842.0),
'max(842.0, NAN)' => max(842.0, NAN),
'min(NAN, 842.0)' => min(NAN, 842.0),
'min(842.0, NAN)' => min(842.0, NAN),
'max([NAN, 1, 2])' => max([NAN, 1.0, 2.0]),
'max([1, NAN, 2])' => max([1.0, NAN, 2.0]),
'INF - INF' => $nan,
'NAN <=> 1.0' => NAN <=> 1.0,
'1.0 <=> NAN' => 1.0 <=> NAN,
];
$area = 0.0;
foreach ([14400.0 * 14400.0, $nan, 612.0 * 792.0, 595.0 * 842.0] as $a) {
$area = max($area, $a);
}
$r['大きい枠→NAN→Letter→A4'] = $area;
echo PHP_VERSION, "\n";
foreach ($r as $k => $v) {
printf(" %-24s %s\n", $k, $e($v));
}
for p in php7.4 php8.0 php8.3 php8.4; do $p nan.php; done
php7.4 などは、手元にあるバージョンのコマンド名に置き換えてください。
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/