PHPのmaxとminは空配列でValueErrorになる?混在型や同値で結果がブレる理由
max()とmin()は、引数(または配列)の中から比較で最大・最小になる値を返す関数です。空配列を渡すとPHP 8.0以降はValueErrorになり、型が混ざった値では「PHPの比較ルール」と「同値なら先頭を返す」という挙動で結果が決まります。
最大値を取るだけの関数なので、あまり警戒していませんでした。ただ、DBやCSVから来た値に対して使うと、空のときに落ちたり、数字のつもりの文字列が混ざって思わぬ値が返ったりします。今回はそのあたりを整理してみます。
空配列を渡すとどうなる?
PHP 8.0以降、空配列を渡すとValueErrorが投げられます。PHP 7系では警告を出してfalseを返していたので、そのつもりで書いたコードは8系で急に例外になります。
<?php
$prices = [];
try {
echo max($prices);
} catch (ValueError $e) {
echo $e->getMessage();
// max(): Argument #1 ($value) must contain at least one element
}
検索結果が0件になりうる場面では、呼ぶ前に空かどうかを見るのが素直です。デフォルト値を決めておけば、呼び出し側でnullを気にしなくて済みます。
<?php
$maxPrice = $prices === [] ? null : max($prices);
// 空なら0として扱ってよいなら、展開で0を混ぜる手もあります
$maxPriceOrZero = max([...$prices, 0]);
後者は「0を下限にする」という意味が入るので、負の値を扱うデータには向きません。そこだけ注意ですね。
同じ値が複数あるとどちらが返る?
比較して等しい値が複数あるときは、先に渡したほうが返ります。値が完全に同じなら気になりませんが、型が違うと差が出ます。
<?php
var_dump(max(1, '1')); // int(1)
var_dump(max('1', 1)); // string(1) "1"
var_dump(max(1, 1.0)); // int(1)
var_dump(max(1.0, 1)); // float(1)
後続の処理で厳密比較(===)をしていると、この差でテストが通ったり落ちたりします。型をそろえてから渡しておくと、挙動が読みやすくなります。
文字列と数値が混ざるとどう比べられる?
max()の比較は、<や>と同じPHPの通常の比較ルールです。数字だけの文字列どうしは数値として、片方が数字でない文字列なら文字列として比べられます。
<?php
var_dump(max('10', '9')); // string(2) "10" 数値として比較
var_dump(max('10', '9a')); // string(2) "9a" '9a'は数値文字列ではないので文字列比較
var_dump(max(['100', '20', '3x'])); // string(2) "3x"
CSVを読み込んだだけの配列は全部文字列ですし、1行だけ「3x」のような汚れた値が混ざっていても、エラーにはなりません。最大値として「3x」が返ってきて、気づくのは集計結果がおかしいと言われたあとになりがちです。
数値として扱うつもりなら、先にintvalやfilter_varで変換するか、検証してから渡すのが安全です。変換した場合、汚れた値は0などに化けるので、その扱いも決めておく必要があります。
配列どうしを比べると要素数が先に見られる?
max()に配列を複数渡すと、配列が比較されます。このとき先に見られるのは要素数で、要素数が同じときだけ先頭から順に値を比べます。
<?php
var_dump(max([1, 2], [1, 3])); // [1, 3] 要素数が同じなので値を順に比較
var_dump(max([1, 2, 3], [5])); // [1, 2, 3] 要素数が多いほうが大きい扱い
「二次元配列の最大の行を取る」つもりで書いたmax()が、要素数の多い行を返してしまう、というパターンです。行の中の特定の値で選びたいときは、array_columnで取り出してからmax()に渡します。
最大値のキーはどう取る?
max()は値だけを返し、キーは返しません。キーが欲しいときは、返ってきた値でarray_searchを引き直します。ただしarray_searchは最初に見つかったキーしか返さないので、同じ最大値が複数ある場合はarray_keysで全部取ります。
<?php
$scores = ['a' => 3, 'b' => 7, 'c' => 7];
var_dump(array_search(max($scores), $scores)); // string(1) "b"
var_dump(array_keys($scores, max($scores))); // ['b', 'c']
array_keysの第2引数は緩い比較で探すので、数値と文字列が混ざる配列ではここでも先ほどの比較ルールが効いてきます。型がそろったデータで使うぶんには、これで十分だと思います。
まとめ
max()とmin()は、空配列でValueError、同値なら先頭、型が混ざればPHPの比較ルール、というところだけ押さえておけばだいぶ安全に使えます。外から来た値に使うときは、先に空チェックと型の変換を挟んでおく、くらいの気持ちでいるのがちょうどいいかもしれません。
よくある質問
Q. max()に空配列を渡したときのPHP 7と8の違いは?
A. PHP 7系は警告を出してfalseを返し、PHP 8.0以降はValueErrorを投げます。8系へ上げるときは、空配列になりうる箇所を洗っておくと安心です。
Q. 最大値の行(キー)を取りたいときはどうしますか?
A. max()で値を取り、array_searchでキーを引くか、同値が複数ありうるならarray_keysで全部取ります。
Q. 文字列の数字が混ざった配列でmax()を使っても大丈夫ですか?
A. 数字だけの文字列なら数値として比較されますが、数字でない文字列が1つ混ざると文字列比較になる組み合わせがあります。先にintvalやfilter_varで型をそろえるほうが確実です。