Laravelのコレクション(その5:ソート)

0
158

Laravelコレクション掘り下げシリーズ第五弾。今回はソート系メソッドに関して確認していきます。

例によって、要素の配置変更に対してキーと要素の関係は元の状態が保持されることを基本とし、例外となるケースにのみ言及しています。
また、要素を並べ直すと言う目的から考えて操作対象は配列(キーがインデックス)と考えるのが自然ですが、しばしば触れるようにPHPにおける配列はキーを連番とした連想配列に過ぎないので、全般的に連想配列に対しても適用可能です。ただ、意味があるかどうかは別ですが。

なお、valuesに関しては要素自体の並びが変化する訳ではないので「ソート」に該当するかどうかは微妙ですが、要素の配置変更(先に紹介したサブセット取得なども含む)結果に対して用いられることが多いため、本カテゴリで合わせて紹介しておきます。

reverse

$col1 = collect([1,2,3,4,5]);
$col2 = $col1->reverse();

要素の並び順を反転させます。
結果は以下の通り。

array:5 [
  4 => 5
  3 => 4
  2 => 3
  1 => 2
  0 => 1
]

反転は一次元目のみに作用します。
例えば以下のような二次元配列を考えます。

$col1 = collect([
    [
        1,
        2,
        3,
    ],
    [
        4,
        5,
        6,
    ],
    [
        7,
        8,
        9,
    ],
]);

結果は以下の通り。

array:3 [
  2 => array:3 [
    0 => 7
    1 => 8
    2 => 9
  ]
  1 => array:3 [
    0 => 4
    1 => 5
    2 => 6
  ]
  0 => array:3 [
    0 => 1
    1 => 2
    2 => 3
  ]
]

一次元目としての要素の並び順は逆転していますが、二次元目の要素の並びは元のままです。

shuffle

$col1 = collect([1,2,3,4,5]);
$col2 = $col1->shuffle();

要素の並び順をシャッフルします。
結果は以下の通り。

array:5 [
  0 => 5
  1 => 3
  2 => 2
  3 => 4
  4 => 1
]

上記はあくまで実行結果の一例であり、shuffleに関しては実行ごとに要素の並びが変わります。
また、キー(インデックス)が振り直される点もポイントです。

本機能も一次元目のみに作用します。
reverseで示した二次元配列に対してshuffleを実行した結果は以下の通り。

array:3 [
  0 => array:3 [
    0 => 1
    1 => 2
    2 => 3
  ]
  1 => array:3 [
    0 => 7
    1 => 8
    2 => 9
  ]
  2 => array:3 [
    0 => 4
    1 => 5
    2 => 6
  ]
]

一次元目の要素のみシャッフルされています。

なお、最初にも書いたように本メソッドも連想配列に適用可能です。ただ、キー(インデックス)を振り直すと言う性格上、連想配列との親和性は低いと思われます。

sort

$$col1 = collect([5,2,4,1,3]);
$col2 = $col1->sort();

要素の値に関して昇順にソートします。
結果は以下の通り。

array:5 [
  3 => 1
  1 => 2
  4 => 3
  2 => 4
  0 => 5
]

引数としてコールバックの指定が可能であり、このコールバックの戻り値としてはPHPのuasort関数と同様にする(と言うか同コールバックを引数とするuasortを間接的に呼び出す)ことで複雑なソートを実現することもできるようですが、多くのケースでは後述するsortBy等で代替できると思われるので、素直にそちらを使うべきかと思います。

なお、本メソッドも連想配列に適用可能ですが、結果はかなり分かり難いものになります。
例えば下記のようなケースを考えてみます。

$col1 = collect([
    'a' => 2,
    'b' => 'xyz',
    'c' => [
        'c-1' => 3,
        'c-2' => 'str',
    ],
    'e' => 'abc',
    'd' => 1,
]);
$col2 = $col1->sort();

結果は以下の通り。

array:5 [
  "e" => "abc"
  "b" => "xyz"
  "d" => 1
  "a" => 2
  "c" => array:2 [
    "c-1" => 3
    "c-2" => "str"
  ]
]

文字列と数値であれば文字列の方が優先され、同じ文字列、数値同士に関しては通常の並び順判断に従うようです。要素として単純比較がしづらい配列は後回しと言ったところでしょうか。試していませんがオブジェクトも含めるとどうなったんでしょう?
いずれにしてもこのような使い方はしないと思いますので、これ以上の深追いはやめておきます。

sortBy, sortByDesc

$col1 = collect([
    [
        'a' => 'str1',
        'b' => [
            'b-a' => 1,
            'b-2' => 3,
        ]
    ],
    [
        'a' => 'str2',
        'b' => [
            'b-a' => 2,
            'b-2' => 1,
        ]
    ],
    [
        'a' => 'str3',
        'b' => [
            'b-a' => 3,
            'b-2' => 2,
        ]
    ],
]);
$col2 = $col1->sortBy('b.b-2');
$col3 = $col1->sortByDesc('b.b-2');

並び順が逆になるだけなのでセットで。指定の要素の値で昇順(降順)にソートします。
上記例のように多次元配列においてネストの深い位置の要素を対象に指定することもできます。
結果は以下の通り。

// sortByの結果
array:3 [
  1 => array:2 [
    "a" => "str2"
    "b" => array:2 [
      "b-a" => 2
      "b-2" => 1
    ]
  ]
  2 => array:2 [
    "a" => "str3"
    "b" => array:2 [
      "b-a" => 3
      "b-2" => 2
    ]
  ]
  0 => array:2 [
    "a" => "str1"
    "b" => array:2 [
      "b-a" => 1
      "b-2" => 3
    ]
  ]
]
// sortByDescの結果
array:3 [
  0 => array:2 [
    "a" => "str1"
    "b" => array:2 [
      "b-a" => 1
      "b-2" => 3
    ]
  ]
  2 => array:2 [
    "a" => "str3"
    "b" => array:2 [
      "b-a" => 3
      "b-2" => 2
    ]
  ]
  1 => array:2 [
    "a" => "str2"
    "b" => array:2 [
      "b-a" => 2
      "b-2" => 1
    ]
  ]
]

引数にコールバックを指定することで、独自の判断基準で並び順を制御するための数値を決定することができます。
以下のようなケースを考えてみます。

$col1 = collect([
    [
        'a' => 3,
        'b' => 7,
    ],
    [
        'a' => 1,
        'b' => 4,
    ],
    [
        'a' => 2,
        'b' => 9,
    ],
]);
$col2 = $col1->sortBy(function($val) {
    return $val['a'] + $val['b'];
});
$col3 = $col1->sortByDesc(function($val) {
    return $val['a'] + $val['b'];
});

一次元目の要素内の2つの要素の和をソートの判断材料としています。
結果は以下の通り。

// sortByの結果
array:3 [
  1 => array:2 [
    "a" => 1
    "b" => 4
  ]
  0 => array:2 [
    "a" => 3
    "b" => 7
  ]
  2 => array:2 [
    "a" => 2
    "b" => 9
  ]
]
// sortByDescの結果
array:3 [
  2 => array:2 [
    "a" => 2
    "b" => 9
  ]
  0 => array:2 [
    "a" => 3
    "b" => 7
  ]
  1 => array:2 [
    "a" => 1
    "b" => 4
  ]
]

sortKeys, sortKeysDesc

$col1 = collect([
    'b' => 1,
    'c' => 2,
    'a' => 3,
]);
$col2 = $col1->sortKeys();
$col3 = $col1->sortKeysDesc();

並び順が逆になるだけなのでセットで。キーで昇順(降順)にソートします。
結果は以下の通り。

// sortKeysの結果
array:3 [
  "a" => 3
  "b" => 1
  "c" => 2
]
// sortKeysDescの結果
array:3 [
  "c" => 2
  "b" => 1
  "a" => 3
]

values

$col1 = collect([1,2,3,4,5]);
$col2 = $col1->reverse();
$col3 = $col2->values();

インデックスを振り直します。
先に示したようにreverseは要素の並び順を反転させますが、キー(インデックス)は保持されたままでした。その結果に対してvaluesを実行すると下記のようにインデックスが振り直されます。

array:5 [
  0 => 5
  1 => 4
  2 => 3
  3 => 2
  4 => 1
]

他のメソッドも含めて要素の配置を変えつつもキー(インデックス)が保持されたままという例は多数あります(と言うかこちらの方が基本)。その結果をそのまま配列として取り出してもfor文で期待通りに使えないなど問題になるケースもあります。そのような際に本メソッドを使用してインデックスを適正に振り直す必要があります。
このような目的で本メソッドを使用すべき局面がしばしば発生しますが、往々にして忘れがちでもあるため注意が必要です。

なお、本メソッドも連想配列に適用可能ですが、shuffleと同様にキー(インデックス)を振り直すと言う性格上、連想配列との親和性は低いと思われます。

総括

ソート系の処理は要素の並び順を変更することが目的ですからインデックスの振り直しは無条件に行っても良いように思うのですが、多くのメソッドで基本に忠実にキー(インデックス)を保持したまま配置のみを変更しています。キーと値の結び付きを重視していることから連想配列も考慮してのことのように推測されますが、そもそも連想配列に対して要素の並び順を操作したい動機が今一つ思いつかないので、この辺の仕様は今一つ釈然としないところです。

なお、その中でshuffleのみがインデックスを振り直すのですが、この辺の判断基準はどうなっているんでしょうね?