かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

PHP 配列の要素を任意の順番に並び替えたい

PHP で順番がランダムで渡される配列から特定の要素を特定の順番に移動させた配列を作成したい

環境

PHP 8.2.0

要件

順番がランダムになる ['Mizuki', 'Mikuru', 'Aoi', 'Ichigo', 'Otome', 'Yurika', 'Ran', 'Kaede'] という配列があり、先頭を固定して ['Ichigo', 'Aoi', 'Ran', …その他] となるようにしたい

入力
['Mizuki', 'Mikuru', 'Aoi', 'Ichigo', 'Otome', 'Yurika', 'Ran', 'Kaede']
↓
出力
['Ichigo', 'Aoi', 'Ran', …その他の要素]

1. usort を使って並び替える

usort(array &$array, callable $callback): bool
順序を決めるユーザー定義の比較関数により、 array をその値でソートします。
cf. PHP: usort - Manual

<?php
$array = ['Mizuki', 'Mikuru', 'Aoi', 'Ichigo', 'Otome', 'Yurika', 'Ran', 'Kaede'];

function my_sort(array $array): array {
  usort($array, function($a, $b) {
    // 先頭移動させる要素の順番を決め打ちできるようにする
    if ( ($a === 'Ichigo' && $b === 'Aoi') 
      || ($a === 'Ichigo' && $b === 'Ran')
      || ($a === 'Aoi' && $b === 'Ran') ) {
        return -1;
    }
    if ( ($b === 'Ichigo' && $a === 'Aoi') 
      || ($b === 'Ichigo' && $a === 'Ran')
      || ($b === 'Aoi' && $a === 'Ran') ) {
        return 1;
    }
    // 先頭になる要素とそれ以外の要素の比較では、常に先頭になる要素を前にする
    if ($a === 'Ichigo' || $a === 'Aoi' || $a === 'Ran') { return -1; }
    if ($b === 'Ichigo' || $b === 'Aoi' || $b === 'Ran') { return 1; }
    // それ以外同士の比較は順番を変更しない
    return 0;
  });

  return $array;
}
my_sort($array);
// array(8) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Ran"
//   [3]=> "Mizuki"
//   [4]=> "Mikuru"
//   [5]=> "Otome"
//   [6]=> "Yurika"
//   [7]=> "Kaede"
// }

ソート関数なので直感的ではあるが、任意の順番にしたい要素が増えれば条件が複雑になるので見通しが悪くなるリスクがあり、任意の要素が先頭または最後でないと条件付が難しそう

2. 特定の順番にする要素を取り出して結合する

先頭に移動させる要素を取り出して並び替えた配列を作成し、残りの要素の配列と結合する

<?php
function my_sort(array $array, array $pickups): array {
  $pickupItems = [];
  $otherItems = array_map(function($item) use(&$pickupItems, $pickups) {
    $key = array_search($item, $pickups);
    if ($key !== false) {
      $pickupItems[$key] = $item;
      return NULL;
    }
    return $item;
  }, $array);
  
  // indexになるあ数字を key にした連想配列になっているので key 順に並び替える
  // $pickupItems = array(3) {
  //   [1]=> "Aoi"
  //   [0]=> "Ichigo"
  //   [2]=> "Ran"
  // }
  ksort($pickupItems);
  // NULL な値を取り除く
  $otherItems = array_filter($otherItems, function($item) {
    return !is_null($item);
  });
  // 配列を結合して返す
  return array_merge($pickupItems, $otherItems);
}

$array = ['Mizuki', 'Mikuru', 'Aoi', 'Ichigo', 'Otome', 'Yurika', 'Ran', 'Kaede'];
$pickups = ['Ichigo', 'Aoi', 'Ran'];
my_sort($array, $pickups);
// array(8) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Ran"
//   [3]=> "Mizuki"
//   [4]=> "Mikuru"
//   [5]=> "Otome"
//   [6]=> "Yurika"
//   [7]=> "Kaede"
// }

最後に array_merge をするので $pickups に外胴する要素が無かったり歯抜けになっていても最終的に index が振り直された配列が返される

array_merge(array ...$arrays): array
入力配列が同じキー文字列を有していた場合、そのキーに関する後に指定された値が、 前の値を上書きします。しかし、配列が同じ添字番号を有していても 値は追記されるため、このようなことは起きません。
入力配列の中にある数値添字要素の添字の数値は、 結果の配列ではゼロから始まる連続した数値に置き換えられます。 cf. PHP: array_merge - Manual

<?php
$array = ['Sumire', 'Aoi', 'Ichigo', 'Akari', 'Yurika', 'Hinaki'];
$pickups = ['Ichigo', 'Aoi', 'Ran', 'Yurika'];
my_sort($array, $pickups);
// array(6) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Yurika"
//   [3]=> "Sumire"
//   [4]=> "Akari"
//   [5]=> "Hinaki"
// }

この方法は任意の要素の集合が先頭または最後だと配列のマージで済むが、そうでない場合は array_splice などで追加する必要がある

2-2. ピックアップした要素を任意の場所に挿入するパターン

<?php
function my_sort(array $array, array $orders): array {
  $pickupItems = [];
  $otherItems = array_map(function($item) use(&$pickupItems, $orders) {
    $key = array_search($item, $orders);
    if ($key !== false) {
      $pickupItems[$key] = $item;
      return NULL;
    }
    return $item;
  }, $array);
  
  // indexになるあ数字を key にした連想配列になっている
  // index の小さい順に挿入しないと結果がずれるので key 順に並び替える
  ksort($pickupItems);
  // NULL な値を取り除く
  $newArray = array_filter($otherItems, function($item) {
    return !is_null($item);
  });

  // $pickupItems を任意の位置に挿入する
  foreach($pickupItems as $index => $item) {
    array_splice($newArray, $index, 0, $item);
  }
  
  return $newArray;
}

$array = ['Ichigo', 'Aoi', 'Ran', 'Yurika', 'Sumire', 'Hinaki', 'Akari', 'Otome'];
$orders = [1 => 'Akari', 3 => 'Sumire', 5 => 'Hinaki'];
my_sort($array, $orders);
// array(8) {
//   [0]=> "Ichigo"
//   [1]=> "Akari"
//   [2]=> "Aoi"
//   [3]=> "Sumire"
//   [4]=> "Ran"
//   [5]=> "Hinaki"
//   [6]=> "Yurika"
//   [7]=> "Otome"
// }

この方法だと任意の index に値を移動させられる
array_splice が都度配列の index を付け直すので、配列の要素が大きかったり挿入する要素が多いと重くなりそうな気もする

追記 array_filter を使う方法

指定の要素以外の配列を作るのに array_map を使うより array_filter を使ったほうがシンプルで良さそうだと指摘をいただきました。それはそう! (後で NULL の要素を詰める必要もないですし)

array_filter(array $array, ?callable $callback = null, int $mode = 0): array
cf. PHP: array_filter - Manual

<?php
function my_sort(array $input, array $pickups): array {
  // ピックアップする配列が存在していれば取り出す
  $pickupItems = array_filter($pickups, fn($item) => in_array($item, $input, true));
  $otherItems = array_filter($input, fn($item) => !in_array($item, $pickups, true));
  // array_filter で返された配列の添字は元の配列の値が生きたまま
  // array_merge で配列を結合する際に添字が破棄されて配列の順番で新しいインデックスが振り直される
  return array_merge($pickupItems, $otherItems);
}

$array = ['Mizuki', 'Mikuru', 'Aoi', 'Ichigo', 'Otome', 'Yurika', 'Ran', 'Kaede'];
$pickups = ['Ichigo', 'Aoi', 'Ran'];
my_sort($array, $pickups);
// array(8) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Ran"
//   [3]=> "Mizuki"
//   [4]=> "Mikuru"
//   [5]=> "Otome"
//   [6]=> "Yurika"
//   [7]=> "Kaede"
// }

$array = ['Sumire', 'Aoi', 'Ichigo', 'Akari', 'Yurika', 'Hinaki'];
$pickups = ['Ichigo', 'Aoi', 'Ran', 'Yurika'];
my_sort($array, $pickups);
// array(6) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Yurika"
//   [3]=> "Sumire"
//   [4]=> "Akari"
//   [5]=> "Hinaki"
// }

挿入するインデックスを指定する場合も filter した配列を使う方が便利

<?php
function my_sort(array $input, array $pickups): array {
  $pickupItems = array_filter($pickups, fn($item) => in_array($item, $input, true));
  $newArray = array_filter($input, fn($item) => !in_array($item, $pickups, true));
  
  foreach($pickupItems as $index => $item) {
    array_splice($newArray, $index, 0, $item);
  }
  return $newArray;
}

$array = ['Ichigo', 'Aoi', 'Ran', 'Yurika', 'Sumire', 'Hinaki', 'Akari', 'Otome'];
$orders = [1 => 'Akari', 3 => 'Sumire', 5 => 'Hinaki'];
my_sort($array, $orders);
// array(8) {
//   [0]=> "Ichigo"
//   [1]=> "Akari"
//   [2]=> "Aoi"
//   [3]=> "Sumire"
//   [4]=> "Ran"
//   [5]=> "Hinaki"
//   [6]=> "Yurika"
//   [7]=> "Otome"
// }

3. インデックスを付け直す

配列を捜査してインデックスを key として付け直し最後に ksort してしまう方法

ksort はインデックスが衝突すると要素が削除されてしまうので、その他の要素にもインデックスを振る必要がある

<?php
$array = [
    0 => 'Ichigo',
    2 => 'Ran',
    3 => 'Akari',
    1 => 'Aoi',
];
ksort($array);
// array(4) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Ran"
//   [3]=> "Akari"
// }

// インデックスが衝突すると値が消える
$array = [
    0 => 'Ichigo',
    'Ran',
    3 => 'Akari',
    1 => 'Aoi',  
];
ksort($array);
// array(3) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [3]=> "Akari"
// }

インデックスを振り直した配列を作成する方法

<?php
function my_sort(array $array, array $orders): array {
  $newArray = [];
  // 特定要素を挿入するインデックス
  $ordersIndexs = array_keys($orders);

  foreach($array as $index => $item) {
    // 並び替える要素かどうか判定
    $key = array_search($item, $orders);
    if ($key !== false) {
      $newArray[$key] = $item;
      continue;
    }
    
    // 使用する index とか被っている場合はインデックスをずらす
    $currentLength = count($newArray);
    $newIndex = count($newArray);
    $isReservedIndex = array_search($newIndex, $ordersIndexs);
    // $newIndex が既に index が新しい配列に使用済みかどうか
    $hasIndexInNewArray = array_key_exists($newIndex, $newArray);

    if ($isReservedIndex === false && $hasIndexInNewArray === false) {
      // 使用する index と被ってなければ配列の最後に追加する
      $newArray[] = $item;
      continue;
    }
    
    while($isReservedIndex !== false || $hasIndexInNewArray === true) {
      $newIndex += 1;
      // $newIndex が既に index が新しい配列に使用済みの場合は index を更にずらす
      $hasIndexInNewArray = array_key_exists($newIndex, $newArray);
      if ($hasIndexInNewArray) {
        while($hasIndexInNewArray !== false) {
          $newIndex += 1;
          $hasIndexInNewArray = array_key_exists($newIndex, $newArray);
        }
      }
      $isReservedIndex = array_search($newIndex, $ordersIndexs);
    }
    $newArray[$newIndex] = $item;
  }

  ksort($newArray);
  return $newArray;
}

$array = ['Mizuki', 'Mikuru', 'Aoi', 'Otome', 'Yurika', 'Ran', 'Kaede', 'Ichigo'];
$orders = ['Ichigo', 'Aoi', 'Ran'];
my_sort($array, $orders);
// array(8) {
//   [0]=> "Ichigo"
//   [1]=> "Aoi"
//   [2]=> "Ran"
//   [3]=> "Mizuki"
//   [4]=> "Mikuru"
//   [5]=> "Otome"
//   [6]=> "Yurika"
//   [7]=> "Kaede"
// }

// 特定の位置に挿入もOK
$array = ['Ichigo', 'Aoi', 'Ran', 'Yurika', 'Sumire', 'Hinaki', 'Akari', 'Otome'];
$orders = [1 => 'Akari', 3 => 'Sumire', 5 => 'Hinaki'];
my_sort($array, $orders);
// array(8) {
//   [0]=>"Ichigo"
//   [1]=> "Akari"
//   [2]=> "Aoi"
//   [3]=> "Sumire"
//   [4]=> "Ran"
//   [5]=> "Hinaki"
//   [6]=> "Yurika"
//   [7]=> "Otome"
// }

任意のインデックスに挿入が可能だけど、while が二重になっていたりと個人的に少し見通しが悪いようにも感じる

所感

配列の任意の位置に要素を移動させたいだけだったのだけど、PHP は同じ数値でも インデックス と 添字 が別になっていたり配列の操作メソッドが破壊的なものが多かったり、無記名関数の中で外にある配列を操作時は & を使った参照渡しにしなければだったり JavaScript と違いが多く感覚的にできない部分が多く思った以上に大変でした
(また暫くPHPを触らないとすぐ忘れそう…)


[参考]

最近配信されたのでアニメ見ました。拙者ループものでこういうエンディング好き…!!