[PHP]strnatcmp関数を完全解説!自然順ソートで文字列を比較

PHP

こんにちは!今回は、PHPの標準関数であるstrnatcmp()について詳しく解説していきます。自然順(人間が期待する順序)で文字列を比較する関数です!

strnatcmp関数とは?

strnatcmp()関数は、2つの文字列を自然順アルゴリズムで比較する関数です。

通常の文字列比較では「file10.txt」が「file2.txt」より前に来ますが、自然順ソートでは人間が期待する順序(file2が先)で比較できます!

基本的な構文

strnatcmp(string $string1, string $string2): int
  • $string1: 比較する1つ目の文字列
  • $string2: 比較する2つ目の文字列
  • 戻り値:
    • 0: 両者が等しい
    • < 0: $string1が$string2より小さい
    • > 0: $string1が$string2より大きい

基本的な使用例

通常の比較との違い

$files = ['file10.txt', 'file2.txt', 'file1.txt', 'file20.txt'];

// 通常のソート(辞書順)
$normal = $files;
sort($normal);
print_r($normal);
/*
Array (
    [0] => file1.txt
    [1] => file10.txt    ← 2より前に来る
    [2] => file2.txt
    [3] => file20.txt
)
*/

// 自然順ソート
$natural = $files;
usort($natural, 'strnatcmp');
print_r($natural);
/*
Array (
    [0] => file1.txt
    [1] => file2.txt     ← 正しい順序
    [2] => file10.txt
    [3] => file20.txt
)
*/

シンプルな比較

// 数値を含む文字列の比較
echo strnatcmp('file2', 'file10') . "\n";    // -1(file2が小さい)
echo strnatcmp('file10', 'file2') . "\n";    // 1(file10が大きい)
echo strnatcmp('file2', 'file2') . "\n";     // 0(等しい)

// 通常のstrcmpとの違い
echo strcmp('file2', 'file10') . "\n";       // 1(辞書順では'2'>'1')
echo strnatcmp('file2', 'file10') . "\n";    // -1(自然順では2<10)

数値部分の認識

// 数値部分を正しく認識
echo strnatcmp('abc2def', 'abc10def') . "\n";     // -1
echo strnatcmp('version1.2', 'version1.10') . "\n";  // -1
echo strnatcmp('item100', 'item99') . "\n";       // 1

// 複数の数値部分
echo strnatcmp('v2.10.5', 'v2.9.8') . "\n";      // 1(2.10 > 2.9)
echo strnatcmp('2.10.5', '2.2.100') . "\n";      // 1(2.10 > 2.2)

実践的な使用例

例1: ファイル管理システム

class FileManager {
    /**
     * ファイルを自然順でソート
     */
    public static function sortFiles($files) {
        usort($files, 'strnatcmp');
        return $files;
    }
    
    /**
     * ディレクトリ内のファイルを取得してソート
     */
    public static function listDirectory($directory, $pattern = '*') {
        if (!is_dir($directory)) {
            return [];
        }
        
        $files = [];
        foreach (glob($directory . '/' . $pattern) as $file) {
            $files[] = basename($file);
        }
        
        usort($files, 'strnatcmp');
        
        return $files;
    }
    
    /**
     * ファイルをタイプ別にソート
     */
    public static function sortByType($files) {
        $images = [];
        $documents = [];
        $others = [];
        
        foreach ($files as $file) {
            $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
            
            if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
                $images[] = $file;
            } elseif (in_array($ext, ['pdf', 'doc', 'docx', 'txt'])) {
                $documents[] = $file;
            } else {
                $others[] = $file;
            }
        }
        
        usort($images, 'strnatcmp');
        usort($documents, 'strnatcmp');
        usort($others, 'strnatcmp');
        
        return [
            'images' => $images,
            'documents' => $documents,
            'others' => $others
        ];
    }
    
    /**
     * バックアップファイルを日付順でソート
     */
    public static function sortBackups($backups) {
        // backup_2024_01_15.zip のような形式を想定
        usort($backups, 'strnatcmp');
        return $backups;
    }
    
    /**
     * 最新のファイルを取得
     */
    public static function getLatestFile($files) {
        if (empty($files)) {
            return null;
        }
        
        usort($files, 'strnatcmp');
        return end($files);
    }
}

// 使用例
$files = [
    'photo1.jpg',
    'photo10.jpg',
    'photo2.jpg',
    'photo20.jpg',
    'photo100.jpg',
    'document5.pdf',
    'document50.pdf'
];

echo "=== 元のリスト ===\n";
print_r($files);

echo "\n=== 自然順ソート ===\n";
$sorted = FileManager::sortFiles($files);
print_r($sorted);

echo "\n=== タイプ別ソート ===\n";
$byType = FileManager::sortByType($files);
print_r($byType);

$backups = [
    'backup_2024_01_10.zip',
    'backup_2024_01_2.zip',
    'backup_2024_01_20.zip',
    'backup_2024_02_1.zip'
];

echo "\n=== バックアップファイル ===\n";
$sortedBackups = FileManager::sortBackups($backups);
print_r($sortedBackups);

echo "\n最新のバックアップ: " . FileManager::getLatestFile($backups) . "\n";

例2: バージョン管理

class VersionManager {
    /**
     * バージョン番号を比較
     */
    public static function compare($version1, $version2) {
        return strnatcmp($version1, $version2);
    }
    
    /**
     * バージョンをソート
     */
    public static function sortVersions($versions) {
        usort($versions, 'strnatcmp');
        return $versions;
    }
    
    /**
     * 最新バージョンを取得
     */
    public static function getLatestVersion($versions) {
        if (empty($versions)) {
            return null;
        }
        
        $sorted = self::sortVersions($versions);
        return end($sorted);
    }
    
    /**
     * 最小要求バージョンをチェック
     */
    public static function meetsRequirement($currentVersion, $requiredVersion) {
        return strnatcmp($currentVersion, $requiredVersion) >= 0;
    }
    
    /**
     * バージョン範囲内かチェック
     */
    public static function isInRange($version, $minVersion, $maxVersion) {
        return strnatcmp($version, $minVersion) >= 0 && 
               strnatcmp($version, $maxVersion) <= 0;
    }
    
    /**
     * メジャーバージョンでグループ化
     */
    public static function groupByMajor($versions) {
        $grouped = [];
        
        foreach ($versions as $version) {
            // v1.2.3 → 1
            preg_match('/^v?(\d+)/', $version, $matches);
            $major = $matches[1] ?? '0';
            
            $grouped[$major][] = $version;
        }
        
        // 各グループをソート
        foreach ($grouped as $major => $versionList) {
            usort($grouped[$major], 'strnatcmp');
        }
        
        // キーを数値順にソート
        ksort($grouped, SORT_NUMERIC);
        
        return $grouped;
    }
    
    /**
     * 互換性のあるバージョンを検索
     */
    public static function findCompatibleVersions($versions, $targetVersion) {
        // 同じメジャーバージョンを互換性ありとする
        preg_match('/^v?(\d+)/', $targetVersion, $matches);
        $targetMajor = $matches[1] ?? null;
        
        if ($targetMajor === null) {
            return [];
        }
        
        $compatible = [];
        
        foreach ($versions as $version) {
            preg_match('/^v?(\d+)/', $version, $matches);
            $major = $matches[1] ?? null;
            
            if ($major === $targetMajor) {
                $compatible[] = $version;
            }
        }
        
        usort($compatible, 'strnatcmp');
        
        return $compatible;
    }
}

// 使用例
$versions = [
    'v1.0.0',
    'v1.10.0',
    'v1.2.0',
    'v2.0.0',
    'v2.1.0',
    'v10.0.0',
    'v2.10.0',
    'v1.20.0'
];

echo "=== バージョンソート ===\n";
$sorted = VersionManager::sortVersions($versions);
print_r($sorted);

echo "\n最新バージョン: " . VersionManager::getLatestVersion($versions) . "\n";

echo "\nバージョンチェック:\n";
var_dump(VersionManager::meetsRequirement('v2.5.0', 'v2.0.0'));  // true
var_dump(VersionManager::meetsRequirement('v1.5.0', 'v2.0.0'));  // false

echo "\n範囲チェック:\n";
var_dump(VersionManager::isInRange('v1.15.0', 'v1.10.0', 'v1.20.0'));  // true

echo "\n=== メジャーバージョンでグループ化 ===\n";
$grouped = VersionManager::groupByMajor($versions);
print_r($grouped);

echo "\n=== 互換性のあるバージョン(v2系) ===\n";
$compatible = VersionManager::findCompatibleVersions($versions, 'v2.5.0');
print_r($compatible);

例3: ドキュメント管理

class DocumentManager {
    /**
     * ドキュメントをソート
     */
    public static function sortDocuments($documents) {
        usort($documents, function($a, $b) {
            return strnatcmp($a['title'], $b['title']);
        });
        
        return $documents;
    }
    
    /**
     * 章番号でソート
     */
    public static function sortChapters($chapters) {
        usort($chapters, function($a, $b) {
            return strnatcmp($a['chapter'], $b['chapter']);
        });
        
        return $chapters;
    }
    
    /**
     * ページ番号でソート
     */
    public static function sortPages($pages) {
        usort($pages, 'strnatcmp');
        return $pages;
    }
    
    /**
     * 階層構造を考慮したソート
     */
    public static function sortHierarchical($items) {
        usort($items, function($a, $b) {
            // "1.1.1" のような番号を比較
            return strnatcmp($a['number'], $b['number']);
        });
        
        return $items;
    }
    
    /**
     * 目次を生成
     */
    public static function generateTableOfContents($chapters) {
        $sorted = self::sortChapters($chapters);
        $toc = [];
        
        foreach ($sorted as $chapter) {
            $toc[] = sprintf(
                "%s. %s .................. p.%d",
                $chapter['chapter'],
                $chapter['title'],
                $chapter['page']
            );
        }
        
        return $toc;
    }
}

// 使用例
$chapters = [
    ['chapter' => '1.1', 'title' => 'Introduction', 'page' => 1],
    ['chapter' => '1.10', 'title' => 'Summary', 'page' => 95],
    ['chapter' => '1.2', 'title' => 'Background', 'page' => 5],
    ['chapter' => '2.1', 'title' => 'Methods', 'page' => 100],
    ['chapter' => '1.20', 'title' => 'Conclusion', 'page' => 200],
];

echo "=== 章番号でソート ===\n";
$sorted = DocumentManager::sortChapters($chapters);
foreach ($sorted as $chapter) {
    echo "{$chapter['chapter']}: {$chapter['title']}\n";
}

echo "\n=== 目次生成 ===\n";
$toc = DocumentManager::generateTableOfContents($chapters);
foreach ($toc as $entry) {
    echo $entry . "\n";
}

$pages = ['Page 1', 'Page 10', 'Page 2', 'Page 20', 'Page 100'];
echo "\n=== ページソート ===\n";
$sortedPages = DocumentManager::sortPages($pages);
print_r($sortedPages);

例4: データベースIDのソート

class IdSorter {
    /**
     * IDをソート
     */
    public static function sortIds($ids) {
        usort($ids, 'strnatcmp');
        return $ids;
    }
    
    /**
     * レコードをIDでソート
     */
    public static function sortRecordsById($records, $idField = 'id') {
        usort($records, function($a, $b) use ($idField) {
            return strnatcmp($a[$idField], $b[$idField]);
        });
        
        return $records;
    }
    
    /**
     * プレフィックス付きIDをソート
     */
    public static function sortPrefixedIds($ids) {
        usort($ids, 'strnatcmp');
        return $ids;
    }
    
    /**
     * 複合キーでソート
     */
    public static function sortByCompositeKey($records, $fields) {
        usort($records, function($a, $b) use ($fields) {
            foreach ($fields as $field) {
                $result = strnatcmp($a[$field], $b[$field]);
                
                if ($result !== 0) {
                    return $result;
                }
            }
            
            return 0;
        });
        
        return $records;
    }
}

// 使用例
$ids = ['USER10', 'USER2', 'USER1', 'USER100', 'USER20'];

echo "=== IDソート ===\n";
$sorted = IdSorter::sortIds($ids);
print_r($sorted);

$records = [
    ['id' => 'ORDER10', 'amount' => 1000],
    ['id' => 'ORDER2', 'amount' => 500],
    ['id' => 'ORDER100', 'amount' => 5000],
    ['id' => 'ORDER1', 'amount' => 250],
];

echo "\n=== レコードソート ===\n";
$sortedRecords = IdSorter::sortRecordsById($records);
foreach ($sortedRecords as $record) {
    echo "{$record['id']}: {$record['amount']}\n";
}

$prefixedIds = [
    'A10B5',
    'A2B10',
    'A10B2',
    'A2B2'
];

echo "\n=== プレフィックス付きID ===\n";
$sortedPrefixed = IdSorter::sortPrefixedIds($prefixedIds);
print_r($sortedPrefixed);

例5: タイムスタンプとシーケンス

class SequenceSorter {
    /**
     * シーケンス番号でソート
     */
    public static function sortBySequence($items) {
        usort($items, function($a, $b) {
            return strnatcmp($a['sequence'], $b['sequence']);
        });
        
        return $items;
    }
    
    /**
     * 日付とシーケンスでソート
     */
    public static function sortByDateAndSequence($items) {
        usort($items, function($a, $b) {
            // まず日付で比較
            $dateCompare = strcmp($a['date'], $b['date']);
            
            if ($dateCompare !== 0) {
                return $dateCompare;
            }
            
            // 日付が同じならシーケンスで比較
            return strnatcmp($a['sequence'], $b['sequence']);
        });
        
        return $items;
    }
    
    /**
     * タイムスタンプ付きファイルをソート
     */
    public static function sortTimestampedFiles($files) {
        // log_20240101_001.txt のような形式
        usort($files, 'strnatcmp');
        return $files;
    }
}

// 使用例
$transactions = [
    ['date' => '2024-01-15', 'sequence' => 'TXN10', 'amount' => 1000],
    ['date' => '2024-01-15', 'sequence' => 'TXN2', 'amount' => 500],
    ['date' => '2024-01-14', 'sequence' => 'TXN100', 'amount' => 2000],
    ['date' => '2024-01-15', 'sequence' => 'TXN1', 'amount' => 250],
];

echo "=== 日付とシーケンスでソート ===\n";
$sorted = SequenceSorter::sortByDateAndSequence($transactions);
foreach ($sorted as $txn) {
    echo "{$txn['date']} - {$txn['sequence']}: {$txn['amount']}\n";
}

$logFiles = [
    'log_20240115_010.txt',
    'log_20240115_002.txt',
    'log_20240114_100.txt',
    'log_20240115_020.txt'
];

echo "\n=== ログファイルソート ===\n";
$sortedLogs = SequenceSorter::sortTimestampedFiles($logFiles);
print_r($sortedLogs);

例6: 製品カタログ

class ProductCatalog {
    /**
     * 製品コードでソート
     */
    public static function sortByCode($products) {
        usort($products, function($a, $b) {
            return strnatcmp($a['code'], $b['code']);
        });
        
        return $products;
    }
    
    /**
     * カテゴリと製品コードでソート
     */
    public static function sortByCategoryAndCode($products) {
        usort($products, function($a, $b) {
            $categoryCompare = strcmp($a['category'], $b['category']);
            
            if ($categoryCompare !== 0) {
                return $categoryCompare;
            }
            
            return strnatcmp($a['code'], $b['code']);
        });
        
        return $products;
    }
    
    /**
     * モデル番号でソート
     */
    public static function sortByModel($products) {
        usort($products, function($a, $b) {
            return strnatcmp($a['model'], $b['model']);
        });
        
        return $products;
    }
}

// 使用例
$products = [
    ['code' => 'PROD10', 'model' => 'Model 2', 'category' => 'Electronics'],
    ['code' => 'PROD2', 'model' => 'Model 10', 'category' => 'Electronics'],
    ['code' => 'PROD100', 'model' => 'Model 1', 'category' => 'Books'],
    ['code' => 'PROD1', 'model' => 'Model 20', 'category' => 'Electronics'],
];

echo "=== 製品コードでソート ===\n";
$sorted = ProductCatalog::sortByCode($products);
foreach ($sorted as $product) {
    echo "{$product['code']}: {$product['model']}\n";
}

echo "\n=== カテゴリとコードでソート ===\n";
$sorted = ProductCatalog::sortByCategoryAndCode($products);
foreach ($sorted as $product) {
    echo "{$product['category']} - {$product['code']}\n";
}

例7: テストケース管理

class TestCaseManager {
    /**
     * テストケースをソート
     */
    public static function sortTestCases($testCases) {
        usort($testCases, function($a, $b) {
            return strnatcmp($a['id'], $b['id']);
        });
        
        return $testCases;
    }
    
    /**
     * テストスイートをソート
     */
    public static function sortTestSuites($suites) {
        usort($suites, function($a, $b) {
            return strnatcmp($a['suite'], $b['suite']);
        });
        
        return $suites;
    }
    
    /**
     * 実行順序を生成
     */
    public static function generateExecutionOrder($testCases) {
        $sorted = self::sortTestCases($testCases);
        $order = [];
        
        foreach ($sorted as $index => $testCase) {
            $order[] = sprintf(
                "%d. %s - %s",
                $index + 1,
                $testCase['id'],
                $testCase['name']
            );
        }
        
        return $order;
    }
}

// 使用例
$testCases = [
    ['id' => 'TC10', 'name' => 'Login test', 'priority' => 'high'],
    ['id' => 'TC2', 'name' => 'Signup test', 'priority' => 'high'],
    ['id' => 'TC100', 'name' => 'Performance test', 'priority' => 'low'],
    ['id' => 'TC1', 'name' => 'Homepage test', 'priority' => 'medium'],
];

echo "=== テストケースソート ===\n";
$sorted = TestCaseManager::sortTestCases($testCases);
foreach ($sorted as $tc) {
    echo "{$tc['id']}: {$tc['name']}\n";
}

echo "\n=== 実行順序 ===\n";
$order = TestCaseManager::generateExecutionOrder($testCases);
foreach ($order as $item) {
    echo $item . "\n";
}

他の比較関数との違い

$strings = ['item10', 'item2', 'item1', 'item20'];

// strcmp(): 辞書順
$result = $strings;
usort($result, 'strcmp');
print_r($result);
// item1, item10, item2, item20

// strnatcmp(): 自然順
$result = $strings;
usort($result, 'strnatcmp');
print_r($result);
// item1, item2, item10, item20

// strcasecmp(): 辞書順、大文字小文字無視
$strings2 = ['Item10', 'item2', 'ITEM1', 'item20'];
$result = $strings2;
usort($result, 'strcasecmp');
print_r($result);
// ITEM1, Item10, item2, item20

// strnatcasecmp(): 自然順、大文字小文字無視
$result = $strings2;
usort($result, 'strnatcasecmp');
print_r($result);
// ITEM1, item2, Item10, item20

natsort()関数との関係

$files = ['file10.txt', 'file2.txt', 'file1.txt'];

// usort() + strnatcmp()
$result1 = $files;
usort($result1, 'strnatcmp');

// natsort()(インデックスを保持)
$result2 = $files;
natsort($result2);

// 両方とも同じ順序になる
print_r($result1);  // インデックスは0, 1, 2
print_r($result2);  // 元のインデックスを保持

パフォーマンスの考慮

// 大量のデータでのソート
$data = [];
for ($i = 1; $i <= 1000; $i++) {
    $data[] = 'item' . rand(1, 100);
}

// strnatcmp()
$test1 = $data;
$start = microtime(true);
usort($test1, 'strnatcmp');
$time1 = microtime(true) - $start;

// strcmp()
$test2 = $data;
$start = microtime(true);
usort($test2, 'strcmp');
$time2 = microtime(true) - $start;

echo "strnatcmp(): {$time1}秒\n";
echo "strcmp(): {$time2}秒\n";

// 自然順ソートは若干遅いが、数値を含む文字列では必要

まとめ

strnatcmp()関数の特徴をまとめると:

できること:

  • 自然順(人間が期待する順序)での文字列比較
  • 数値を含む文字列の適切なソート
  • バージョン番号やファイル名の比較

他の関数との違い:

  • strcmp(): 辞書順、大文字小文字を区別
  • strcasecmp(): 辞書順、大文字小文字を区別しない
  • strnatcmp(): 自然順、大文字小文字を区別
  • strnatcasecmp(): 自然順、大文字小文字を区別しない

推奨される使用場面:

  • ファイル名のソート
  • バージョン番号の比較
  • ドキュメントの章番号
  • 製品コードのソート
  • データベースIDのソート
  • テストケースの管理

自然順ソートの原理:

  • “file2.txt”の”2″を数値として認識
  • “file10.txt”の”10″を数値として認識
  • 2 < 10 なので正しい順序になる

関連関数:

  • strnatcasecmp(): 大文字小文字を区別しない自然順比較
  • natsort(): 配列を自然順でソート
  • natcasesort(): 配列を自然順でソート(大文字小文字無視)

strnatcmp()は、数値を含むファイル名、バージョン番号、製品コードなどを扱う際に、人間が期待する順序で並べ替えることができる非常に便利な関数です。ユーザーフレンドリーなアプリケーション開発に活用しましょう!

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