はじめに
こんにちは。
グレンジ Advent Calendar 2017の12月8日の記事を書かせていただきます、1年目でサーバサイドのエンジニアをしているy-encoreです。
本日は開発で使っているPHPのarray_uniqueについての話をしたいと思います。
(仮タイトルの内容から変えてしまってすみません)
array_uniqueとは?
内容についてはこちら。
http://php.net/manual/ja/function.array-unique.php
PHPをよく使う方はご存知かと思いますが、配列の値が重複するものを削除する関数になっています。
実際に例を出してみると、
$testArray = [
'one' => 1,
'two' => 1,
'three' => 2,
'four' => 1,
'five' => 3,
];
print_r(array_unique($testArray));
Array
(
[one] => 1
[three] => 2
[five] => 3
)
keyが無くてもOK。数値・文字列の混在でもstring変換後===
で一致するものを削除できます。
$testArray = [
1,
'1',
2,
1,
'3',
];
print_r(array_unique($testArray));
Array
(
[0] => 1
[2] => 2
[4] => 3
)
多次元の配列に適用したい
データを連想配列の形で取ってきてそれをまとめた配列を作り、その中から重複データを削除したいと言うときがあります。
先程のphp.netの説明では、
注意: array_unique() は、 多次元配列での使用を想定したものではないことに注意しましょう。
とありましたが、「array_unique 多次元配列」などと検索してみると、
array_uniqueの第2引数sort_flags
にSORT_REGULAR
を指定すれば上手く行きそうです。
$testArray = [
'one' => [
'first' => 1,
'second' => 2,
],
'two' => [
'first' => 1,
'second' => 2,
],
'three' => [
'first' => 0,
'second' => 2,
],
'four' => [
'first' => 1,
'second' => 2,
],
];
print_r(array_unique($testArray, SORT_REGULAR));
Array
(
[one] => Array
(
[first] => 1
[second] => 2
)
[three] => Array
(
[first] => 0
[second] => 2
)
)
なるほど、上手いこと重複データが削除されていそうです。
上手くいかない例もある
php_uniqueの実装は、重複判定のためにまず1回ソートをしてから同一のものを取り除いています。
先程出てきたsort_flags
というのも、このソート時の型を何に合わせるかを指定しているもので、SORT_REGULAR
の場合には型変換を特にすることなく比較を行ってソートします。
重複判定にソートが必要なため、ソート時の比較が上手くいかない要素が含まれている場合はarray_uniqueの結果が不安定になります。
多次元配列のarray_uniqueをするために用いたSORT_REGULAR
ですが、
こちらの記事で指摘されている通り、数値と文字列が混在する場合には不安定な挙動を示すことがあるようです。
http://d.hatena.ne.jp/hnw/20090227
その他、重複チェックをしたい連想配列の構造がそれぞれ異なる場合にも、要素同士の比較が上手く出来ないためにarray_uniqueは結果が不安定となります。
具体的な例を出してみます。
まずは単純比較ができない連想配列を作成するメソッドを作ります
function createTestArray($keyName) {
return [
'one' => 1,
'two' => 2,
$keyName => 3,
];
}
echo '(three < san) ' . (createTestArray('three') < createTestArray('san') ? 'true' : 'false');
echo '(three > san) ' . (createTestArray('three') > createTestArray('san') ? 'true' : 'false');
echo '(three === san) ' . (createTestArray('three') === createTestArray('san') ? 'true' : 'false');
echo '(three === three) ' . (createTestArray('three') === createTestArray('three') ? 'true' : 'false');
echo '(san === san) ' . (createTestArray('san') === createTestArray('san') ? 'true' : 'false');
(three < san) : false
(three > san) : false
(three === san) : false
(three === three) : true
(san === san) : true
構造が異なる連想配列同士では大小比較がどちらもfalseになり、単純比較ではソートができないことがわかります。
このような連想配列をリストとして持つ配列に対してarray_uniqueをかけてみます。
$keyNameList = [
'three',
'san',
];
$testArrayList = [];
for ($i = 0; $i < 30; $i++) {
$testArrayList[] = createTestArray($keyNameList[rand(0, 1)]);
}
print_r(array_unique($testArrayList, SORT_REGULAR));
Array
(
[0] => Array
(
[one] => 1
[two] => 2
[san] => 3
)
[1] => Array
(
[one] => 1
[two] => 2
[san] => 3
)
[3] => Array
(
[one] => 1
[two] => 2
[three] => 3
)
[18] => Array
(
[one] => 1
[two] => 2
[three] => 3
)
)
一部は確かに消えていますが、重複する要素が残っている状態のままであることが確認できます。
array_uniqueを使わずに重複要素を取り除く
構造が異なる連想配列を要素に持つ配列の重複を取り除くときには、array_uniqueを使わずに自前で実装するのが安全かと思います。
計算コストは高くなりますが、foreachとin_arrayを使って比較的簡単に書くことが出来ます。
function myArrayUnique($array) {
$uniqueArray = [];
foreach($array as $key => $value) {
if (!in_array($value, $uniqueArray)) {
$uniqueArray[$key] = $value;
}
}
return $uniqueArray;
}
また、キーが不要であればarray_reduceとin_arrayでも書けます。
重複を取り除く場合はキーが不要なことが多いので、こちらのほうが綺麗かもしれません。
$uniqueArray = array_reduce($array, function($carry, $item) {
if (!in_array($item, $carry)) {
$carry[] = $item;
}
return $carry;
}, []);
さいごに
ここまで読んでくださった方、ありがとうございました!
グレンジのアドベントカレンダーはまだまだ続きますのでぜひよろしくお願いします。
[前日]12/7 raitome : Spineの最新版を使ってみた
https://qiita.com/raitome/items/5ae1fce0bb4fc68fc796
[翌日]12/9 flankids : Unityで始める!負荷削減
https://qiita.com/flankids/items/a7b1be3100e4129d1c58