【PHP】宇宙船演算子<=>でソート処理を改めて考えてみた

PHP

PHPの演算子の中でも、なんだかキャッチーなこいつ。

<=>

宇宙船演算子。
皆さん使ったことあります?

現場の上長も知らないことがあったくらい、影の薄いあまり見ないものですよね。

実際のところ私もPHP技術者検定(バージョン7)の勉強でしか見かけたことがなく、すっかり存在を忘れていたんですが…。
先日リリースしたこちらのアプリを開発するにあたって、手腕を発揮してくれたんです。

拡張子クイズ|ファイル拡張子を4択クイズで楽しく学ぶ
拡張子クイズは、202種類のファイル拡張子を4択クイズと辞典で学べる無料サイト。初級からエンジニア向けプロ級、ITパスポート対策コースまで。あなたの拡張子マニア度を診断しよう!

しかも、宇宙船演算子を紐解いたことで、なんとなく理解していた「ソート処理」を学びなおすきっかけにもなりました。

概要

いつから登場したのか

冒頭でも触れましたが、私が宇宙船演算子を初めて見たのは「PHP技術者検定」という資格取得に励んでいた頃です。
で、検定の対象となるPHPのバージョンは7でした。

宇宙船演算子は、PHPバージョン7から登場した新・演算子なんです。

筆者
筆者

確かにUFOっぽい見た目。
名付けた人、よく思いついたなぁ。

記号と書き方

$a <=> $b

「$a と $b を比較して、3通りの値を返す」 という演算子です。

さ、3通り…?? ってなりますよね。
文系出身者には2通りが限界ですので…(trueかfalseか、みたいな)。

PHPの演算子の中でも、返り値が3以上になるのは宇宙船演算子だけ。

そのおかげもあって、実はソート処理で非常に便利な働きをしてくれるんです。

返り値

まずは返り値がどのようなものか見ていきましょう。

返り値意味
-1$a が $b より小さい(前)
0$a と $b が等しい(同じ)
1$a が $b より大きい(後ろ)

具体的にはこんな感じ。
数字はもちろん、文字列でも比較対象となります。

1 <=> 2   // -1 返す(1は2より前)
2 <=> 2   //  0 返す(同じ)
3 <=> 2   //  1 返す(3は2より後ろ)

'apple' <=> 'banana'    // -1 返す(aはbより前)
'banana' <=> 'apple'    //  1 返す(bはaより後ろ)

基本の使い方

特に整列されていない $numbers という配列があるとします。

$numbers = [3, 1, 4, 1, 5];

// usort() は任意の定義でソートを行う関数
usort($numbers, function ($a, $b) {
    return $a <=> $b;  // 昇順でソート
});

print_r($numbers);  // [1, 1, 3, 4, 5]

宇宙船演算子だとたった2行でソートできますが、PHP7以前のバージョンではいちいち

$numbers = [3, 1, 4, 1, 5];

usort($numbers, function ($a, $b) {
    if ($a == $b) {
        return 0;
    }
    return ($a < $b) ? -1 : 1;
});

print_r($numbers);  // [1, 1, 3, 4, 5]

のように書かなければならなかったんですよね。elseif で条件分岐したりとか。
PHP5の開発案件で出くわした記憶があります…。

また、降順にするなら return $b <=> $a; というように $b と $a の順序を反転するだけでいいんです。
返り値だけを見るとややこしいですが、処理の結果はいたって単純ですよね。

ここではシンプルな昇順・降順を例に挙げておりますが、宇宙船演算子は値を思い通りの順番に並び替えるのに特化した演算子なんです。

実装例

ここからが実際のユースケースです。
拡張子クイズの開発工程(Laravel)で、カテゴリーの順序をDBと異なる定義順で並べ替える必要がありました。

定義した順序

例えば、拡張子のカテゴリーをこのように定義しなおしたい!とします。

// Extension モデル
class Extension extends Model
{
    const CATEGORIES = [
        'image' => '画像',
        'video' => '動画',
        'audio' => '音声',
        'archive' => '圧縮・アーカイブ',
        'document' => '文章・オフィス',
    ];
}

でも、ここで const している内容は、DBとは異なる順番。
DBから取得したカテゴリーを CATEGORIES の定義順にしなければなりません。

宇宙船演算子でソート

$grouped = Extension::orderBy('extension')
    ->get()
    ->groupBy('category')
    ->sortKeysUsing(function ($a, $b) {
        $order = array_keys(Extension::CATEGORIES);
        // $order = ['image', 'video', 'audio', 'archive', 'document']
        
        return array_search($a, $order) <=> array_search($b, $order);
    });

extension テーブルから取得したデータを category ごとに組み分けし、さらに配列化した定義順 $order の順番でソートしています。

sortKeysUsing とは

Collection のキーを自分で決めたルールで並び替えるメソッドになります。
Laravelのプロジェクトにおいて、宇宙船演算子を使うならほぼ100%一緒に登場するであろう関数です。

あくまでも宇宙船演算子そのものは順番を入れ替える力はありません。
ほら、返り値が3つって話したでしょう。

返り値が1か、0か、-1かを見て、関数が入れ替えてくれるんです。
この sortKeysUsing も、並び替え関数のひとつ。

ここでは

return array_search($a, $order) <=> array_search($b, $order);

でキーを判定された順番にしてくれてます。

array_search() とは

配列の中から指定した「値」を探して、その値が入っている「キー」を返す関数です。
先の return … では、各カテゴリーの位置を調べる役割を担っています。

$aPosition = array_search('video', $order);   // 1
$bPosition = array_search('image', $order);   // 0

// 位置を比較
return $aPosition <=> $bPosition;  // 1 <=> 0 = 1
// 結果:'video' は 'image' より後ろ

ソート関数あれこれ

ちなみに、PHPとLaravelでは宇宙船演算子と一緒に使えるソート関数がいくつかあります。

関数対象Laravelで使う場合用途
usort()配列の値–配列の要素をカスタムルールでソート
uasort()配列の値(キーも保持)–キーを保持したままソート
uksort()配列のキーsortKeysUsing()配列のキーをカスタムルールでソート
sort()配列の値(昇順)->sort()シンプルな昇順ソート
sortKeys()配列のキー(昇順)->sortKeys()キーの昇順ソート

よくある使い方

複数条件でソート

こちらのパターンは、並び替えしたい条件が複数ある場合に有効です。
Excelのフィルターに近い感覚。

// 難易度で昇順、その中で名前でアルファベット順
usort($extensions, function ($a, $b) {
    // 難易度が異なれば、難易度で比較
    $diffCompare = $a['difficulty'] <=> $b['difficulty'];
    if ($diffCompare !== 0) {
        return $diffCompare;
    }
    
    // 難易度が同じなら、名前で比較
    return $a['name'] <=> $b['name'];
});

null セーフに書く

null の可能性がある値を扱う場合、宇宙船演算子の前に null チェックが必要です。

return ($a <=> $b) ?: 0;

// または nullsafe operator を使う
return $a?->value <=> $b?->value;

(?:)とは?

エルヴィス演算子と呼び、PHPバージョン5.3から登場。
値が false なら代わりの値を返す。
ここで重要なのが null ではなく、falseである点。

$value = ”; // 空文字(false 相当だが null ではない)

$result1 = $value ?? ‘default’; // ” を返す(null ではないので)
$result2 = $value ?: ‘default’; // ‘default’ を返す(falsy だから)

複数のソート条件がある場合、エルヴィス演算子を連鎖させることで、順序を優先順位順に評価できます。

return ($a['category'] <=> $b['category']) 
       ?: ($a['difficulty'] <=> $b['difficulty'])
       ?: ($a['name'] <=> $b['name']);
  1. カテゴリーで比較 → 0 以外(異なる)なら即座に返す
  2. 同じなら難易度で比較 → 0 以外なら返す
  3. それでも同じなら名前で比較

まとめ

実は、後の開発工程でテーブル設計を見直し、DBの制約でソート順序を管理するように変更しました。
が、数行でデータの順序を自由に組み替えられるのってかなり優秀じゃないですか?

テーブル構成まで変える必要はないけど、今だけ並べ替えなくちゃ、みたいな…。

UIを意識すると「ココとココ、逆の方がいい」とか、デザイナーからもらったカンプが順番通りじゃないこととか、あるじゃないですか。
そういうタイミングで力を発揮してくれるな~と感じました。

ではまた次回!

タイトルとURLをコピーしました