読者です 読者をやめる 読者になる 読者になる

WebEngine

だらだらと綴る技術系メモ

値が重複することを考えて連想配列をソート[PHP]

2017.5.9修正

この記事のタイトルは「キーに優先順位をつけて連想配列をソート」というものでしたが、読み返したところ誤解を招く表現であると考え、「値が重複することを考えて連想配列をソート」というものに変更させていただきました。(ソースコード内の関数名も変更させていただきました)

前提


usort関数を使います

ユーザ定義の関数でソートを行える関数です。
以下はPHP公式マニュアルから抜粋したサンプルコードです。

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

$a = array(3, 2, 5, 6, 1);

usort($a, "cmp");

foreach ($a as $key => $value) {
    echo "$key: $value\n";
}
?>


usort関数にはソートしたい配列と自分で定義した関数を渡します。(つまり引数の関数は、コールバック関数になるわけです)

このユーザ定義の関数では、最初の引数と2つ目の引数の比較結果を返す必要があります。最初の引数のほうが2番目の引数より大きいときは正の数を、2番目の引数と等しいときは0を、そして2番目の引数より小さいときは負の数を返す必要があります。

上記のソースコードでは

  • 変数aと変数bが同じならば0を返す
  • 変数aが変数bがより小さいならば-1(負の数)を返す
  • 変数aが変数bより大きいならば1(正の数)を返す

ということをやっているわけです。

ただ、usort関数にわざわざ関数名をつけて分割して渡してやるパターンは、ブログが長くなってしまいますので、無名関数で渡したいと思います。

<?php

$a = array(3, 2, 5, 6, 1);

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

foreach ($a as $key => $value) {
  echo "$key: $value\n";
}

こんな感じで本題のソースコードは書いていきたいと思います。


本題

<?php

// 普通はこれがデータベースなどから取得したデータ
$ary = array(
  array(
    'id' => 1001,
    'name' => 'alice',
    'birthday' => 20010408
  ),
  array(
    'id' => 1002,
    'name' => 'bell',
    'birthday' => 20010408
  ),
  array(
    'id' => 1003,
    'name' => 'clara',
    'birthday' => 20001227
  )
);

function checkDuplicationSort($ary) {
  usort($ary, function($a, $b) {
    if ($a['birthday'] === $b['birthday']) {
      if (strcmp($a['name'], $b['name']) === 0) {
        return ($a['id'] > $b['id']);
      } else {
        return strcmp($a['name'], $b['name']);
      }
    } else {
      return ($a['birthday'] > $b['birthday']);
    }
  });

  return $ary;
}

// -- main --
$sorted_ary = checkDuplicationSort($ary);
echo "<pre>";
print_r($sorted_ary);
echo "</pre>";

※ 最近環境を変えたのでxdebugの設定をしておらず、上記のように<pre>タグを使ってデバッグしています。邪魔な人は消してください。

見てのとおり、if文で、誕生日が同じならば名前でソート、名前が同じならばIDでソートするようにしているだけです。(つまりゴリ押し)
誕生日、名前、IDの順に調べていっているんですね。まあ、データベースから取得してきた場合、IDがダブることはないだろう、ということで IDが最終チェック項目にしてあります。

結果は下記のようになります。

Array
(
    [0] => Array
        (
            [id] => 1003
            [name] => clara
            [birthday] => 20001227
        )

    [1] => Array
        (
            [id] => 1001
            [name] => alice
            [birthday] => 20010408
        )

    [2] => Array
        (
            [id] => 1002
            [name] => bell
            [birthday] => 20010408
        )

)

まず、もっとも早く生まれたclaraさんが最初に、そのあとのaliceさんとbellさんは誕生日が 一緒なので名前でソートされます。a、bとアルファベット順に並び替えられるわけです。

オブジェクトもソートしたい

Studentクラスをyomiでソートします。

<?php

class Student {
  public $id;
  public $name;
  public $yomi;

  function __construct($id, $name, $yomi) {
    $this->id = $id;
    $this->name = $name;
    $this->yomi = $yomi;
  }
}

// -- main --
$students = array();
// PHPでは[] = hoge のようにすることで配列末尾にどんどん要素を追加していけます
$students[] = new Student(1000, "Alice", "アリス");
$students[] = new Student(1001, "Bell", "ベル");
$students[] = new Student(1002, "Clara", "クララ");

usort($students, function($a, $b) {
  return strcmp($a->yomi, $b->yomi);
});

echo "<pre>";
var_dump($students);
echo "</pre>";


結果は次のようになります。

array(3) {
  [0]=>
  object(Student)#1 (3) {
    ["id"]=>
    int(1000)
    ["name"]=>
    string(5) "Alice"
    ["yomi"]=>
    string(9) "アリス"
  }
  [1]=>
  object(Student)#3 (3) {
    ["id"]=>
    int(1002)
    ["name"]=>
    string(5) "Clara"
    ["yomi"]=>
    string(9) "クララ"
  }
  [2]=>
  object(Student)#2 (3) {
    ["id"]=>
    int(1001)
    ["name"]=>
    string(4) "Bell"
    ["yomi"]=>
    string(6) "ベル"
  }
}

ちゃんと「あいうえお順」に並び替えられていることがわかります。
ソートの重複をチェックしたければ連想配列のときと同様、usortの無名関数内でif文を適用してやれば良いだけです。