はじめに
こんにちは、開発エンジニアの amdaba_sk(ペンネーム未定)です。
今回は PHP のお話です。例えば以下のような配列があったとしましょう。
<?php $target = [ 'ほげ', 'ふが', 'ぴよ', ];
これをソートしたいとします。ただそれだけなら、
<?php sort($target)
でおしまい、Q.E.D.! でもいいのですが、PHP には他にもいろいろな配列のソート方法が用意されていますよね。
この記事は、それらいろいろなソート方法を紹介し、特に日本語文字列を値に持つ配列の場合もっと「イイ感じ」にする方法がありますよというお話です。
なお、ソートに限らず PHP の配列機能について知りたいという方は、弊社ブログのこちらの記事も併せてチェックしてみてくださいね。
もくじ
PHP の配列ソート関数いろいろ
お決まりですが、こういうときまずは公式の「PHP マニュアル」を見に行くようにしましょう。PHP は公式マニュアルがとても充実していますよね。配列のソートについても、それについてまとめた専用のページがあります。
関数名 ソートの基準 キーと値の相関関係 ソート順 関連する関数 array_multisort() 値 連想配列の場合は維持し、数値添字配列の場合は維持しない 最初の配列、あるいはソートオプション array_walk() asort() 値 維持する 昇順 arsort() arsort() 値 維持する 降順 asort() krsort() キー 維持する 降順 ksort() ksort() キー 維持する 昇順 asort() natcasesort() 値 維持する 大文字小文字を区別しない自然順 natsort() natsort() 値 維持する 自然順 natcasesort() rsort() 値 維持しない 降順 sort() shuffle() 値 維持しない ランダム array_rand() sort() 値 維持しない 昇順 rsort() uasort() 値 維持する ユーザー定義 uksort() uksort() キー 維持する ユーザー定義 uasort() usort() 値 維持しない ユーザー定義 uasort()
配列をソートする関数が表でまとめられ、それぞれの関数のソートの基準(キー or 値)、キーと値の相関、ソート順、関連する関数が一覧できます。各関数ごとの詳細ページへのリンクもあって、勉強にも役立ちます。
余談ですが、ソートの基準とキーと値の相関が情報として入ってくるのは、すべての配列が連想配列な PHP の特徴的なところですね。
さて、マニュアルは PHP の一般の配列に関するものですが、本記事では文字列を値として持つ、一次元の連番整数値添字の配列について考えます。例えばいくつかの静的型付け言語だと、対象配列が次のように型付けされる場合です1。
Java: List<String>
, C#: List<string>
, TypeScript: string[]
, Scala: List[String]
, F#: string list
, Haskell: [Text]
また、すでに暗黙的に前提としていますが、特に日本語の文字を含む文字列を想定しています。
このような問題設定の下で、ここでは sort
関数について詳しく見ていくことにしましょう。
標準の配列ソート関数 - sort
sort ( array &$array , int $flags = SORT_REGULAR ) : bool
sort
は配列を昇順にソートする関数です。
第一引数 $array
にソートしたい配列への参照を受け取って、ソートに成功したかどうかを真偽値で返します。引数で渡した配列は、この関数の実行によって変更されます。破壊的な作用を持つ関数ということです。
またオプションの第二引数 $flags
に整数で表されたフラグを取り、要素の大小関係の判定方法を切り替えることが出来ます。使える値は以下の通りです。
- SORT_REGULAR - 通常通りに項目を比較します。 詳細は 比較演算子 で説明されています。
- SORT_NUMERIC - 数値として項目を比較します。
- SORT_STRING - 文字列として項目を比較します。
- SORT_LOCALE_STRING - 現在のロケールに基づいて、文字列として項目を比較します。 比較に使うロケールは、setlocale() 関数で変更できます。
- SORT_NATURAL - 要素の比較を文字列として行い、 natsort() と同様の「自然順」で比較します。
- SORT_FLAG_CASE - SORT_STRING や SORT_NATURAL と (ビットORで) 組み合わせて使い、 文字列のソートで大文字小文字を区別しないようにします。
これらはすべて、 PHP のグローバルな定数として定義されています。いくつかピックアップしてみましょう。
SORT_REGULAR
SORT_REGULAR
は sort
の第二引数 $flags
のデフォルト値です。この値が指定された場合、要素の大小関係の判定には宇宙船演算子 <=>
(と同等の処理)が使われます。
宇宙船演算子の詳細については公式マニュアルの該当するページを参照してほしいのですが、SORT_REGULAR
が指定された場合の sort
は、すべての要素が文字列である前提であれば、次に取り上げる SORT_STRING
と変わりません。
ただしマニュアル上でも以下のような警告があるように、要素に複数の型が混在する場合は注意が必要です。
警告 flags が SORT_REGULAR の場合に 複数の型が混在する配列をソートする場合には、注意してください。 sort() が期待しない結果を出力することがあります。
この件に関する詳細は、かなり古い記事ではありますが、以下が参考になります。
SORT_STRING
SORT_STRING
が指定された場合 sort
関数は各要素を文字列であるとして大小関係を比較します。
文字列であるとして比較するとはつまり、文字列を文字の配列であるとして、文字コードの数値的大小関係に基づく辞書式順序で比較する2ということです。
例えば'abc'
と'abd'
を比較する場合、以下のように先頭から一文字ずつ文字の大小を比較していきます。
文字位置 | 'abc' |
'abd' |
比較 |
---|---|---|---|
1 | a |
a |
a = a |
2 | b |
b |
b = b |
3 | c |
d |
c < d |
全体 | 'abc' |
'abd' |
'abc' < 'abd' |
初めて違いの出た文字位置の文字の大小で、文字列全体の大小が決まります。ちなみに文字列の終端は、どんな文字よりも小さいと判定されます。
PHP においては、最終的に C 言語の memcmp
関数を用いる実装がされています3。
日本語文字列をイイ感じに並べる
ここで以下のような配列をソートすることを考えてみましょう。
順位 | 文字列 | 読み | UTF-8 文字コード値 |
---|---|---|---|
1 | あぶりだし | アブリダシ | E38182 E381B6 E3828A E381A0 E38197 |
2 | あまガミ | アマガミ | E38182 E381BE E382AC E3839F |
3 | ういーん | ウイーン | E38186 E38184 E383BC E38293 |
4 | アフロディテ | アフロディテ | E382A2 E38395 E383AD E38387 E382A3 E38386 |
5 | アフロディーテ | アフロディーテ | E382A2 E38395 E383AD E38387 E382A3 E383BC E38386 |
6 | アプロディテ | アプロディテ | E382A2 E38397 E383AD E38387 E382A3 E38386 |
7 | アマガミ | アマガミ | E382A2 E3839E E382AC E3839F |
8 | ウィーン | ウィーン | E382A6 E382A3 E383BC E383B3 |
9 | 一遍上人 | イッペンショウニン | E4B880 E9818D E4B88A E4BABA |
10 | 悪人正機 | アクニンショウキ | E682AA E4BABA E6ADA3 E6A99F |
11 | 阿闍梨 | アジャリ | E998BF E9978D E6A2A8 |
上表は sort
関数に SORT_STRING
を指定して並べ替えた順に記載しています。各文字列の UTF-8 文字コード値を 16 進数表示で付けました。これを見てもらうと、前節で説明した通りの順序に並んでいることが分かります。
一方で、ぱっと見てこの順序は「イイ感じ」でしょうか? 少し考えれば納得できそうですが、おそらくぱっと見だと「なんでこんな順序になっているの?」と思ってしまうのではないでしょうか。
世界にはいろいろな言語があり、使用している文字も違います。それぞれの言語での最適な文字の並び順が、文字コードの大小とは一致しないこともあります。日本語だと、文字列は基本「読み」の順に並べるのが普通ですよね。
さて、ようやく本題にたどり着きました。日本語文字列を PHP で「イイ感じ」にソートするにはどのようにすればいいでしょうか?
先に結論をいうと、以下の 3 つの方法があります。
sort
関数にSORT_LOCALE_STRING
フラグを指定する- intl 拡張を使う -
Collator::sort
またはcollator_sort
- 独自ルールを実装する -
usort
一つずつ見ていきましょう。
sort
関数に SORT_LOCALE_STRING
フラグを指定する
標準のソート関数である sort
は、先にも説明した通りオプションの第二引数 $flags
によって要素の大小関係の判定方法を切り替えることが出来ます。 $flags
に SORT_LOCALE_STRING
が指定された場合、SORT_STRING
と同様各要素を文字列であるとして大小関係を比較しますが、その際、現在のロケールに基づいて地域化された比較ルールを用います。
文字列を文字の配列であるとして先頭から一文字ずつ文字の大小を比較するという点は SORT_STRING
の時と同様なのですが、文字同士の大小比較ルールが単純な文字コードの大小比較ではありません。
SORT_LOCALE_STRING
を使った場合、PHP は内部的には C 言語の strcoll
関数を使って文字列の比較します4。これによってロケールが示す地域の言語に適した方法で文字の大小比較をしてくれるというわけです。
先ほどのサンプル配列をロケールが ja_JP.UTF-8
の環境下で、 SORT_STRING
と SORT_LOCALE_STRING
のそれぞれでソートした結果を比較すると、下のようになります。
--- SORT_STRING +++ SORT_LOCALE_STRING @@ @@ Array ( 0 => 'あぶりだし' 1 => 'あまガミ' 2 => 'ういーん' - 3 => 'アフロディテ' - 4 => 'アフロディーテ' + 3 => 'アフロディーテ' + 4 => 'アフロディテ' 5 => 'アプロディテ' 6 => 'アマガミ' 7 => 'ウィーン' - 8 => '一遍上人' + 8 => '阿闍梨' 9 => '悪人正機' - 10 => '阿闍梨' + 10 => '一遍上人' )
比較結果を見ると、「ひらがな → カタカナ → 漢字」という文字種ごとの並び順は SORT_STRING
の時と同様です。しかし漢字文字列だけを見ると、「ア → アク → イチ」と読みの順に並んでいて、より日本語の文字列として自然と思える並び順になっていると言えます。
より詳細にどのような比較ルールになっているのかというと、申し訳ないのですがにわか仕込みの C 言語ぢからが足りず、分かりませんでした。おそらく PHP をコンパイルした際の処理系に依存するものと考えているのですが、実際のところどうなのでしょう。教えてつよつよな人……!
intl 拡張を使う - Collator::sort
または collator_sort
public Collator::sort ( array &$arr , int $sort_flag = ? ) : bool
手続き型
collator_sort ( Collator $coll , array &$arr , int $sort_flag = ? ) : bool
PHP をビルドする際に、国際化用拡張モジュール (intl 拡張) を有効にすることで、 ICU ライブラリのラッパーを使うことが出来るようになります。ライブラリで提供される機能はいくつもありますが、ここで注目するのは Collator
です。
Collator
は、ロケールに応じた適切な並び順を考慮した文字列比較機能を提供するクラスです。並び順のルールは UCA に準拠しています。
Collator
を使って配列を並べ替えるには、インスタンスメソッドの Collator::sort
か、あるいは collator_sort
関数を使います。
使い方は標準の sort
関数とほぼ同じで、引数 $arr
にソートしたい配列への参照を受け取って、ソートに成功したかどうかを真偽値で返します。引数で渡した配列は、この関数の実行によって変更されます。破壊的な作用を持つ関数というところも同じです。
またオプションの引数 $sort_flag
に整数で表されたフラグを取り、要素の大小関係の判定方法を切り替えることが出来ます。使える値は以下の通りです。
- Collator::SORT_REGULAR - 通常の比較 (型を変更しない)
- Collator::SORT_NUMERIC - 数値としての比較
- Collator::SORT_STRING - 文字列としての比較
省略した場合のデフォルトは Collator::SORT_REGULAR
ですが、要素がすべて文字列である場合は Collator::SORT_STRING
を指定した場合と結局のところ同じになります5。そして Collator::SORT_STRING
が指定された場合、Collator::sort
は内部的に ICU ライブラリで提供される C 言語の関数 ucol_strcoll
6 を使用して文字列の比較をします7。
ロケール ja_JP.UTF-8
が有効な環境下で、サンプル配列を標準の sort
+ SORT_LOCALE_STRING
と Collator::sort
+ Collator::SORT_STRING
のそれぞれでソートして比較してみましょう。
<?php namespace Tests\Unit; use PHPUnit\Framework\TestCase; use Collator; class CollatorSortTest extends TestCase { /** * ロケールの設定(SORT_LOCALE_STRING 用) */ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); setlocale(LC_COLLATE, 'ja_JP.UTF-8'); } /** * sort + SORT_LOCALE_STRING と Collator::sort + Collator::SORT_STRING の * ソート結果を比較するテストケース * @return void */ public function testCollatorSort() { $sample = [ 'あぶりだし', 'あまガミ', 'ういーん', 'アフロディテ', 'アフロディーテ', 'アプロディテ', 'アマガミ', 'ウィーン', '一遍上人', '悪人正機', '阿闍梨', ]; $sortLocalString = $this->sortAndReturn( fn (&$arr) => sort($arr, SORT_LOCALE_STRING), $sample ); $sortCollator = $this->sortAndReturn( fn (&$arr) => Collator::create('ja_JP.utf8')->sort($arr), $sample ); $this->assertEquals($sortLocalString, $sortCollator); } /** * 配列にソート関数を適用して返す * @param callable $sortFunc ソート関数 * @param array $arr 対象配列 * @return array */ private function sortAndReturn(callable $sortFunc, array $arr): array { $sortFunc($arr); return $arr; } }
実行するとこのテストケースは失敗して、以下の差分出力が出て来ます。
--- Expected (sort + SORT_LOCALE_STRING) +++ Actual (Collator::sort + Collator::SORT_STRING) @@ @@ Array ( 0 => 'あぶりだし' - 1 => 'あまガミ' - 2 => 'ういーん' - 3 => 'アフロディーテ' - 4 => 'アフロディテ' - 5 => 'アプロディテ' - 6 => 'アマガミ' - 7 => 'ウィーン' + 1 => 'アフロディーテ' + 2 => 'アフロディテ' + 3 => 'アプロディテ' + 4 => 'アマガミ' + 5 => 'あまガミ' + 6 => 'ウィーン' + 7 => 'ういーん' 8 => '阿闍梨' 9 => '悪人正機' 10 => '一遍上人' )
比較結果を見ると、「かな文字 → 漢字」という並び順は相変わらずですが、Collator::sort
では sort
+ SORT_LOCALE_STRING
とは違って、ひらがなとカタカナの区別がなくなり、音の順で並べられるようになりました。また漢字文字列同士であればやはり読みの順に並んでいて、sort
+ SORT_LOCALE_STRING
よりもさらに日本語の文字列として自然と思える並び順になっていると言えます。
独自ルールを実装する - usort
usort (array &$array , callable $callback ): bool
sort
+ SORT_LOCAL_STRING
や Collator::sort
でも満足できないこだわり派の方には、独自に比較関数を実装する方法をご紹介しましょう。
usort
は第一引数 $array
にソートしたい配列への参照を受け取って、ソートに成功したかどうかを真偽値で返します。引数で渡した配列は、この関数の実行によって変更されます。破壊的な作用を持つ関数ということです。
sort
と異なるのは第二引数がフラグではなく、要素を比較するための関数であり、かつ必須であるという点です。無理やりシグネチャを上と同じように書くならば、第二引数のコールバック関数は以下のようになるでしょうか。
$callback (mixed $element1 , mixed $element2 ): int
配列の要素を引数として二つ取り、整数値を返す関数です。返り値の int は、最初の引数 $element1
のほうが二番目の引数 $element2
より大きい場合は正の数を、等しい場合はゼロを、そして小さい場合は負の数を返すように実装します。
このコールバック関数によって、好きなルールでソートを実現することが出来るという寸法です。要件に合わせて適切なルールを実装しましょう。ちなみに昇順降順の切替も、返り値の正負を反転させることで実現することが出来ます(ursort
が無いのはそのためでしょう)。
実装例:いろは順
せっかくなので ursort
を使った独自ソート関数の実装をやってみましょう。
ursort
を使うのであれば、配列の各要素文字列からソートキー(よみがな等)を別途作成して付け加え、それを使ってソート、なんてこともできるでしょうが、ここでは以下の簡単な条件で実装してみます。
- ソート対象配列の要素文字列はすべて「ひらがな」で構成される
- ただし濁音、半濁音、長音、拗音、促音に相当する文字は含まれないものとする
- 各文字列は辞書式順序で順序比較する
- 各文字の順序は「いろは順」に従う
できあがりはこちらです。
<?php class IrohaOrder { /** @var array $iroha いろは順マスタ */ private $iroha = [ 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ', 'も', 'せ', 'す', 'ん', ]; /** * 文字列配列をいろは順に並べる * @param array $arr * @return bool ソートできたかどうか */ public function sort(array &$arr): bool { try { usort($arr, fn ($a, $b) => $this->compare($a, $b)); return true; } catch (RangeException $e) { return false; } } /** * 文字列を二つ受け取っていろは順に従って順序を決める * @param string $a * @param string $b * @return int <=> の仕様に沿った整数値 * @throws RangeException 引数の文字列にいろは順が付けられない場合 */ public function compare(string $a, string $b): int { $lenA = mb_strlen($a); $lenB = mb_strlen($b); $ret = 0; for ($i = 0; $i < min([$lenA, $lenB]); $i++) { $c = $this->getCharIndex(mb_substr($a, $i, 1)); $d = $this->getCharIndex(mb_substr($b, $i, 1)); $ret = $c <=> $d; if ($ret !== 0) { break; } } return $ret ?: $lenA <=> $lenB; } /** * ひらがな一文字を取っていろは順での順位を返す * @param string $c ひらがな一文字 * @return int いろは順で何番目か * @throws RangeException 引数の文字列にいろは順が付けられない場合 */ private function getCharIndex(string $c): int { $ret = array_search($c, $this->iroha, true); if ($ret === false) { throw new RangeException("Character ${$c} is not supported!"); } return $ret; } }
使い方のサンプルも兼ねて、テストも簡単に作ってみました。
<?php namespace Tests\Unit; use PHPUnit\Framework\TestCase; use IrohaOrder; class IrohaOrderTest extends TestCase { /** @var IrohaOrder $irohaOrder */ private $irohaOrder; public function setUp(): void { $this->irohaOrder = new IrohaOrder(); } /** * ひらがな文字列配列がいろは順に並ぶことの確認 * @return void */ public function testIrohaOrderSort() { $expected = [ 'いしき', 'いんかん', 'ろうか', 'はにわ', 'におう', 'におうもん', 'ほとけ', 'ほんき', 'へいし', 'へんろ', 'とうき', ]; $sorted = $this->shuffleAndSort( fn (&$arr) => $this->irohaOrder->sort($arr), $expected ); $this->assertEquals($expected, $sorted); } /** * 配列をシャッフルしてソート関数を適用して返す * @param callable $sortFunc ソート関数 * @param array $arr 対象配列 * @return array */ private function shuffleAndSort(callable $sortFunc, array $arr): array { shuffle($arr); $sortFunc($arr); return $arr; } }
実行してみましょう。
PHPUnit 9.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:00.004, Memory: 4.00 MB OK (1 test, 1 assertion)
うまく動いているようです。
おわりに
いかがでしたでしょうか?
本記事では、日本語文字列を値として持つ一次元の連番整数値添字の配列を対象として、PHP で利用可能なソート方法をいくつか取り上げて具体例とともに紹介しました。ポイントをまとめると以下のようになります。
その 3 通りの方法は以下の通りです。
sort
関数でSORT_LOCAL_STRING
フラグを指定する- 最も低コスト
- 実際の並び順は処理系依存(?)
- intl 拡張の
Collator::sort
またはcollator_sort
を使う- 拡張の有効化が必要で少し面倒
- UCA に準拠した順序でのソートが可能
- 独自ルールを実装する
- 最も高コスト
- 並び順ルールを自由に決められる
それでどの方法が一番「イイ感じ」なのかというと、結局要件次第にはなってしまうものの、個人的には 2 番の intl 拡張の Collator::sort
または collator_sort
を使う方法が以下の点で「イイ感じ」だと思います。
- Unicode の技術標準として定められた仕様に準拠できる
- 外部のよくテストされたライブラリを使用できる
- 仕様の標準さと実装コストのバランスがよい
以上、PHP で日本語の文字列配列をイイ感じにソートする話でした。少しでもみなさんの PHP ライフの参考にしていただけたら幸いです。
※ 本記事に記載したサンプルコードは php 7.4.15 (cli) で動作を確認しています。
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
rakus.hubspotpagebuilder.comラクスDevelopers登録フォーム
https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/イベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
◆TECH PLAY
techplay.jp
◆connpass
rakus.connpass.com
-
言語によっては他にもっといい型があるかもしれませんが、その辺はスルーでお願いします。↩
-
https://github.com/php/php-src/blob/94d96b3c979a60e47abe209506c3947a16abff40/Zend/zend_operators.c#L2684↩
-
https://github.com/php/php-src/blob/94d96b3c979a60e47abe209506c3947a16abff40/Zend/zend_operators.c#L1976↩
-
https://github.com/php/php-src/blob/94d96b3c979a60e47abe209506c3947a16abff40/ext/intl/collator/collator_sort.c#L51↩
-
https://unicode-org.github.io/icu/userguide/collation/api.html#compare↩
-
https://github.com/php/php-src/blob/94d96b3c979a60e47abe209506c3947a16abff40/ext/intl/collator/collator_sort.c#L168↩