こんにちは!今回は、PHPの標準関数であるstrcoll()について詳しく解説していきます。現在のロケール設定に基づいて文字列を比較できる、国際化対応に重要な関数です!
strcoll関数とは?
strcoll()関数は、現在のロケール設定に基づいて2つの文字列を比較する関数です。
通常のstrcmp()はバイト単位で比較しますが、strcoll()はロケール(言語や地域の設定)に応じた正しい順序で比較します。これにより、各言語の文字の並び順を正しく扱えます!
基本的な構文
strcoll(string $string1, string $string2): int
- $string1: 比較する1つ目の文字列
- $string2: 比較する2つ目の文字列
- 戻り値:
0: 両者が等しい< 0: $string1が$string2より小さい(ロケールの順序で前)> 0: $string1が$string2より大きい(ロケールの順序で後)
ロケールの設定
// ロケールを設定
setlocale(LC_COLLATE, 'ja_JP.UTF-8'); // 日本語
// または
setlocale(LC_COLLATE, 'en_US.UTF-8'); // 英語(アメリカ)
// 現在のロケールを確認
$currentLocale = setlocale(LC_COLLATE, 0);
echo "現在のロケール: {$currentLocale}\n";
基本的な使用例
シンプルな比較
// ロケールを日本語に設定
setlocale(LC_COLLATE, 'ja_JP.UTF-8');
$str1 = "あいうえお";
$str2 = "かきくけこ";
$result = strcoll($str1, $str2);
if ($result < 0) {
echo "{$str1} は {$str2} より前\n";
} elseif ($result > 0) {
echo "{$str1} は {$str2} より後\n";
} else {
echo "{$str1} と {$str2} は同じ\n";
}
// 出力: あいうえお は かきくけこ より前
strcmp()との違い
$str1 = "café";
$str2 = "cafe";
// strcmp(): バイト単位で比較
echo "strcmp: " . strcmp($str1, $str2) . "\n";
// strcoll(): ロケールに基づいて比較
setlocale(LC_COLLATE, 'fr_FR.UTF-8'); // フランス語
echo "strcoll: " . strcoll($str1, $str2) . "\n";
// フランス語のロケールでは、アクセント付き文字も正しく扱われる
実践的な使用例
例1: 多言語対応のソート
class LocaleSorter {
private $locale;
public function __construct($locale = 'ja_JP.UTF-8') {
$this->setLocale($locale);
}
public function setLocale($locale) {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function sort($array) {
usort($array, function($a, $b) {
return strcoll($a, $b);
});
return $array;
}
public function sortByField($array, $field) {
usort($array, function($a, $b) use ($field) {
return strcoll($a[$field], $b[$field]);
});
return $array;
}
public function sortDescending($array) {
usort($array, function($a, $b) {
return strcoll($b, $a); // 順序を逆に
});
return $array;
}
}
// 使用例:日本語のソート
$sorter = new LocaleSorter('ja_JP.UTF-8');
$names = ['田中', '佐藤', '鈴木', '高橋', '伊藤'];
$sorted = $sorter->sort($names);
print_r($sorted);
// 日本語の五十音順でソート
// 使用例:フランス語のソート
$sorter->setLocale('fr_FR.UTF-8');
$words = ['café', 'étudiant', 'école', 'âge', 'être'];
$sorted = $sorter->sort($words);
print_r($sorted);
// フランス語のアルファベット順でソート
// 使用例:中国語のソート
$sorter->setLocale('zh_CN.UTF-8');
$chinese = ['北京', '上海', '广州', '深圳'];
$sorted = $sorter->sort($chinese);
print_r($sorted);
例2: 国際化対応の名簿管理
class InternationalDirectory {
private $entries = [];
private $locale;
public function __construct($locale = 'en_US.UTF-8') {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function addEntry($name, $data) {
$this->entries[] = [
'name' => $name,
'data' => $data
];
}
public function findEntry($name) {
foreach ($this->entries as $entry) {
if (strcoll($entry['name'], $name) === 0) {
return $entry;
}
}
return null;
}
public function getSortedEntries() {
$sorted = $this->entries;
usort($sorted, function($a, $b) {
return strcoll($a['name'], $b['name']);
});
return $sorted;
}
public function getEntriesBetween($start, $end) {
$result = [];
foreach ($this->entries as $entry) {
$compareStart = strcoll($entry['name'], $start);
$compareEnd = strcoll($entry['name'], $end);
if ($compareStart >= 0 && $compareEnd <= 0) {
$result[] = $entry;
}
}
// ソートして返す
usort($result, function($a, $b) {
return strcoll($a['name'], $b['name']);
});
return $result;
}
public function groupByInitial() {
$grouped = [];
foreach ($this->entries as $entry) {
$initial = mb_substr($entry['name'], 0, 1);
if (!isset($grouped[$initial])) {
$grouped[$initial] = [];
}
$grouped[$initial][] = $entry;
}
// 各グループ内をソート
foreach ($grouped as $initial => $entries) {
usort($grouped[$initial], function($a, $b) {
return strcoll($a['name'], $b['name']);
});
}
// グループのキーをソート
uksort($grouped, function($a, $b) {
return strcoll($a, $b);
});
return $grouped;
}
}
// 使用例
$directory = new InternationalDirectory('ja_JP.UTF-8');
$directory->addEntry('田中太郎', ['phone' => '090-1234-5678']);
$directory->addEntry('佐藤花子', ['phone' => '080-9876-5432']);
$directory->addEntry('鈴木一郎', ['phone' => '070-1111-2222']);
$directory->addEntry('高橋美咲', ['phone' => '090-3333-4444']);
$directory->addEntry('伊藤健太', ['phone' => '080-5555-6666']);
// ソート済みエントリー取得
$sorted = $directory->getSortedEntries();
foreach ($sorted as $entry) {
echo "{$entry['name']}: {$entry['data']['phone']}\n";
}
// 範囲検索
echo "\n「さ」行の名前:\n";
$saEntries = $directory->getEntriesBetween('さ', 'そ');
foreach ($saEntries as $entry) {
echo "{$entry['name']}\n";
}
// イニシャルでグループ化
echo "\nイニシャルでグループ化:\n";
$grouped = $directory->groupByInitial();
foreach ($grouped as $initial => $entries) {
echo "\n【{$initial}】\n";
foreach ($entries as $entry) {
echo " {$entry['name']}\n";
}
}
例3: 多言語対応の検索システム
class MultilingualSearch {
private $data = [];
private $locale;
public function __construct($locale) {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function addData($items) {
$this->data = array_merge($this->data, $items);
}
public function exactMatch($query) {
$results = [];
foreach ($this->data as $item) {
if (strcoll($item, $query) === 0) {
$results[] = $item;
}
}
return $results;
}
public function startsWith($prefix) {
$results = [];
$prefixLen = mb_strlen($prefix);
foreach ($this->data as $item) {
$itemPrefix = mb_substr($item, 0, $prefixLen);
if (strcoll($itemPrefix, $prefix) === 0) {
$results[] = $item;
}
}
// ソートして返す
usort($results, function($a, $b) {
return strcoll($a, $b);
});
return $results;
}
public function rangeSearch($min, $max) {
$results = [];
foreach ($this->data as $item) {
$compareMin = strcoll($item, $min);
$compareMax = strcoll($item, $max);
if ($compareMin >= 0 && $compareMax <= 0) {
$results[] = $item;
}
}
usort($results, function($a, $b) {
return strcoll($a, $b);
});
return $results;
}
public function findClosest($query, $limit = 5) {
$scored = [];
foreach ($this->data as $item) {
// レーベンシュタイン距離で類似度を計算
$distance = levenshtein($query, $item);
$scored[] = [
'item' => $item,
'distance' => $distance
];
}
// 距離でソート
usort($scored, function($a, $b) {
if ($a['distance'] === $b['distance']) {
return strcoll($a['item'], $b['item']);
}
return $a['distance'] - $b['distance'];
});
return array_slice(array_column($scored, 'item'), 0, $limit);
}
}
// 使用例:日本語の都市名検索
$search = new MultilingualSearch('ja_JP.UTF-8');
$cities = [
'東京', '大阪', '名古屋', '札幌', '福岡',
'京都', '横浜', '神戸', '広島', '仙台'
];
$search->addData($cities);
echo "完全一致検索(東京):\n";
$results = $search->exactMatch('東京');
print_r($results);
echo "\n前方一致検索(「大」で始まる):\n";
$results = $search->startsWith('大');
print_r($results);
echo "\n範囲検索(「さ」から「た」):\n";
$results = $search->rangeSearch('さ', 'た');
print_r($results);
echo "\n類似検索(「おおさか」に近い):\n";
$results = $search->findClosest('おおさか', 3);
print_r($results);
例4: ロケール対応のデータ比較
class LocaleComparator {
private $locale;
public function __construct($locale) {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function compare($str1, $str2) {
return strcoll($str1, $str2);
}
public function equals($str1, $str2) {
return strcoll($str1, $str2) === 0;
}
public function lessThan($str1, $str2) {
return strcoll($str1, $str2) < 0;
}
public function greaterThan($str1, $str2) {
return strcoll($str1, $str2) > 0;
}
public function min($strings) {
if (empty($strings)) {
return null;
}
$min = $strings[0];
foreach ($strings as $str) {
if (strcoll($str, $min) < 0) {
$min = $str;
}
}
return $min;
}
public function max($strings) {
if (empty($strings)) {
return null;
}
$max = $strings[0];
foreach ($strings as $str) {
if (strcoll($str, $max) > 0) {
$max = $str;
}
}
return $max;
}
public function median($strings) {
if (empty($strings)) {
return null;
}
$sorted = $strings;
usort($sorted, function($a, $b) {
return strcoll($a, $b);
});
$count = count($sorted);
$middle = floor($count / 2);
return $sorted[$middle];
}
}
// 使用例
$comparator = new LocaleComparator('ja_JP.UTF-8');
$names = ['田中', '佐藤', '鈴木', '高橋', '伊藤'];
echo "最小: " . $comparator->min($names) . "\n";
echo "最大: " . $comparator->max($names) . "\n";
echo "中央値: " . $comparator->median($names) . "\n";
var_dump($comparator->equals('田中', '田中')); // true
var_dump($comparator->lessThan('あ', 'い')); // true
var_dump($comparator->greaterThan('ん', 'あ')); // true
例5: 辞書アプリケーション
class Dictionary {
private $entries = [];
private $locale;
public function __construct($locale) {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function addWord($word, $definition) {
$this->entries[] = [
'word' => $word,
'definition' => $definition
];
}
public function lookup($word) {
foreach ($this->entries as $entry) {
if (strcoll($entry['word'], $word) === 0) {
return $entry['definition'];
}
}
return null;
}
public function getSortedWords() {
$words = array_column($this->entries, 'word');
usort($words, function($a, $b) {
return strcoll($a, $b);
});
return $words;
}
public function getWordsStartingWith($prefix) {
$results = [];
$prefixLen = mb_strlen($prefix);
foreach ($this->entries as $entry) {
$wordPrefix = mb_substr($entry['word'], 0, $prefixLen);
if (strcoll($wordPrefix, $prefix) === 0) {
$results[] = $entry;
}
}
usort($results, function($a, $b) {
return strcoll($a['word'], $b['word']);
});
return $results;
}
public function getAlphabeticalIndex() {
$index = [];
foreach ($this->entries as $entry) {
$initial = mb_substr($entry['word'], 0, 1);
if (!isset($index[$initial])) {
$index[$initial] = [];
}
$index[$initial][] = $entry['word'];
}
// 各グループをソート
foreach ($index as $initial => $words) {
usort($index[$initial], function($a, $b) {
return strcoll($a, $b);
});
}
// インデックスキーをソート
uksort($index, function($a, $b) {
return strcoll($a, $b);
});
return $index;
}
public function findSimilar($word, $maxResults = 5) {
$similar = [];
foreach ($this->entries as $entry) {
$distance = levenshtein($word, $entry['word']);
if ($distance <= 3 && strcoll($word, $entry['word']) !== 0) {
$similar[] = [
'word' => $entry['word'],
'distance' => $distance
];
}
}
usort($similar, function($a, $b) {
if ($a['distance'] === $b['distance']) {
return strcoll($a['word'], $b['word']);
}
return $a['distance'] - $b['distance'];
});
return array_slice(array_column($similar, 'word'), 0, $maxResults);
}
}
// 使用例
$dict = new Dictionary('ja_JP.UTF-8');
$dict->addWord('りんご', 'apple');
$dict->addWord('みかん', 'orange');
$dict->addWord('ばなな', 'banana');
$dict->addWord('ぶどう', 'grape');
$dict->addWord('いちご', 'strawberry');
echo "「みかん」の意味: " . $dict->lookup('みかん') . "\n";
echo "\nすべての単語(ソート済み):\n";
$words = $dict->getSortedWords();
print_r($words);
echo "\n「り」で始まる単語:\n";
$results = $dict->getWordsStartingWith('り');
foreach ($results as $entry) {
echo "{$entry['word']}: {$entry['definition']}\n";
}
echo "\nアルファベットインデックス:\n";
$index = $dict->getAlphabeticalIndex();
foreach ($index as $initial => $words) {
echo "{$initial}: " . implode(', ', $words) . "\n";
}
例6: ファイル名の自然な並び替え
class NaturalFileSorter {
private $locale;
public function __construct($locale) {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function sort($filenames) {
usort($filenames, function($a, $b) {
// 拡張子を除いたベース名で比較
$baseA = pathinfo($a, PATHINFO_FILENAME);
$baseB = pathinfo($b, PATHINFO_FILENAME);
$result = strcoll($baseA, $baseB);
// ベース名が同じ場合は拡張子で比較
if ($result === 0) {
$extA = pathinfo($a, PATHINFO_EXTENSION);
$extB = pathinfo($b, PATHINFO_EXTENSION);
return strcoll($extA, $extB);
}
return $result;
});
return $filenames;
}
public function groupByExtension($filenames) {
$grouped = [];
foreach ($filenames as $filename) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if (!isset($grouped[$ext])) {
$grouped[$ext] = [];
}
$grouped[$ext][] = $filename;
}
// 各グループ内をソート
foreach ($grouped as $ext => $files) {
usort($grouped[$ext], function($a, $b) {
return strcoll($a, $b);
});
}
// 拡張子でソート
uksort($grouped, function($a, $b) {
return strcoll($a, $b);
});
return $grouped;
}
}
// 使用例
$sorter = new NaturalFileSorter('ja_JP.UTF-8');
$files = [
'レポート2024.pdf',
'プレゼン資料.pptx',
'データ分析.xlsx',
'あいさつ文.docx',
'レポート2023.pdf',
'写真アルバム.pdf'
];
$sorted = $sorter->sort($files);
print_r($sorted);
$grouped = $sorter->groupByExtension($files);
print_r($grouped);
例7: 多言語対応のバリデーション
class LocaleValidator {
private $locale;
public function __construct($locale) {
$this->locale = $locale;
setlocale(LC_COLLATE, $locale);
}
public function isInRange($value, $min, $max) {
$compareMin = strcoll($value, $min);
$compareMax = strcoll($value, $max);
return $compareMin >= 0 && $compareMax <= 0;
}
public function validateOrder($items) {
for ($i = 0; $i < count($items) - 1; $i++) {
if (strcoll($items[$i], $items[$i + 1]) > 0) {
return [
'valid' => false,
'error' => "順序エラー: {$items[$i]} と {$items[$i + 1]}",
'position' => $i
];
}
}
return ['valid' => true];
}
public function findDuplicates($items) {
$duplicates = [];
for ($i = 0; $i < count($items); $i++) {
for ($j = $i + 1; $j < count($items); $j++) {
if (strcoll($items[$i], $items[$j]) === 0) {
$duplicates[] = [
'value' => $items[$i],
'positions' => [$i, $j]
];
}
}
}
return $duplicates;
}
public function validateUniqueness($items) {
$duplicates = $this->findDuplicates($items);
if (empty($duplicates)) {
return ['valid' => true];
}
return [
'valid' => false,
'duplicates' => $duplicates
];
}
}
// 使用例
$validator = new LocaleValidator('ja_JP.UTF-8');
// 範囲チェック
var_dump($validator->isInRange('き', 'か', 'こ')); // true
var_dump($validator->isInRange('た', 'か', 'こ')); // false
// 順序チェック
$items = ['あ', 'い', 'う', 'え', 'お'];
$result = $validator->validateOrder($items);
print_r($result); // valid => true
$items = ['あ', 'う', 'い', 'え', 'お'];
$result = $validator->validateOrder($items);
print_r($result); // valid => false
// 重複チェック
$items = ['田中', '佐藤', '田中', '鈴木'];
$duplicates = $validator->findDuplicates($items);
print_r($duplicates);
利用可能なロケール
// システムで利用可能なロケールを確認
function getAvailableLocales() {
$locales = [];
$commonLocales = [
'ja_JP.UTF-8', // 日本語
'en_US.UTF-8', // 英語(アメリカ)
'en_GB.UTF-8', // 英語(イギリス)
'fr_FR.UTF-8', // フランス語
'de_DE.UTF-8', // ドイツ語
'es_ES.UTF-8', // スペイン語
'it_IT.UTF-8', // イタリア語
'zh_CN.UTF-8', // 中国語(簡体字)
'zh_TW.UTF-8', // 中国語(繁体字)
'ko_KR.UTF-8', // 韓国語
'ru_RU.UTF-8', // ロシア語
'ar_SA.UTF-8', // アラビア語
];
foreach ($commonLocales as $locale) {
$result = setlocale(LC_COLLATE, $locale);
if ($result !== false) {
$locales[] = $locale;
}
}
return $locales;
}
$available = getAvailableLocales();
echo "利用可能なロケール:\n";
foreach ($available as $locale) {
echo " - {$locale}\n";
}
注意点と制限事項
パフォーマンスの考慮
// strcoll()はstrcmp()より遅い
$iterations = 10000;
$str1 = "test string one";
$str2 = "test string two";
// strcmp()
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
strcmp($str1, $str2);
}
$time1 = microtime(true) - $start;
// strcoll()
setlocale(LC_COLLATE, 'en_US.UTF-8');
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
strcoll($str1, $str2);
}
$time2 = microtime(true) - $start;
echo "strcmp(): {$time1}秒\n";
echo "strcoll(): {$time2}秒\n";
// strcoll()の方が遅いが、正確な言語順序が必要な場合は必要
ロケールが設定されていない場合
// ロケールが正しく設定されていないと、正しく動作しない
$result = setlocale(LC_COLLATE, 'invalid_locale');
if ($result === false) {
echo "ロケールの設定に失敗しました\n";
echo "デフォルトのロケールを使用します\n";
setlocale(LC_COLLATE, ''); // システムデフォルト
}
マルチバイト文字の扱い
// strcoll()は正しく設定されていればマルチバイト文字も扱える
setlocale(LC_COLLATE, 'ja_JP.UTF-8');
$str1 = "日本語";
$str2 = "にほんご";
$result = strcoll($str1, $str2);
echo "比較結果: {$result}\n";
// ただし、文字エンコーディングには注意が必要
まとめ
strcoll()関数の特徴をまとめると:
できること:
- ロケールに基づいた文字列比較
- 各言語の正しい並び順での比較
- 国際化対応のソート処理
strcmp()との違い:
strcmp(): バイト単位で比較(高速)strcoll(): ロケールの規則に従って比較(正確)
推奨される使用場面:
- 多言語対応のソート
- 国際化されたアプリケーション
- 名簿や辞書の並び替え
- 各言語の文字順序が重要な場合
注意点:
strcmp()より遅い- 正しいロケール設定が必要
- システムでロケールがサポートされている必要がある
- 文字エンコーディングに注意
関連関数:
strcmp(): バイト単位の比較strcasecmp(): 大文字小文字を区別しない比較strnatcmp(): 自然順ソートsetlocale(): ロケールの設定
strcoll()は国際化対応が必要なアプリケーションで重要な役割を果たします。各言語の正しい並び順を実現するために、適切にロケールを設定して使用しましょう!
