[PHP]strcoll関数を完全解説!ロケールに基づいた文字列比較

PHP

こんにちは!今回は、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()は国際化対応が必要なアプリケーションで重要な役割を果たします。各言語の正しい並び順を実現するために、適切にロケールを設定して使用しましょう!

タイトルとURLをコピーしました