3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHP の max() と min() に NAN が混ざると、引数の順番と PHP のバージョンで答えが変わります【7.4 / 8.0 / 8.3 / 8.4 で実測】

3
Posted at

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/

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?