[PHP]strnatcasecmp関数を完全解説!自然順ソートで大文字小文字を区別しない比較

PHP

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

strnatcasecmp関数とは?

strnatcasecmp()関数は、2つの文字列を自然順アルゴリズムで比較し、大文字小文字を区別しない関数です。

「file1.txt」「file10.txt」「file2.txt」のような文字列を、人間が期待する順序(file1、file2、file10)でソートしたい場合に非常に便利です!

基本的な構文

strnatcasecmp(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] => file2.txt
    [3] => file20.txt
)
*/

// 自然順ソート(大文字小文字区別なし)
$natural = $files;
usort($natural, 'strnatcasecmp');
print_r($natural);
/*
Array (
    [0] => FILE1.txt
    [1] => file2.txt
    [2] => file10.txt
    [3] => file20.txt
)
*/

シンプルな比較

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

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

strnatcmp()との違い

// strnatcmp(): 大文字小文字を区別
echo strnatcmp('File2', 'file10') . "\n";        // -1('F' < 'f')

// strnatcasecmp(): 大文字小文字を区別しない
echo strnatcasecmp('File2', 'file10') . "\n";    // -1(2 < 10)
echo strnatcasecmp('FILE2', 'file2') . "\n";     // 0(等しい)

実践的な使用例

例1: ファイルリストのソート

class FileListSorter {
    /**
     * ファイル名を自然順でソート
     */
    public static function sortFiles($files) {
        usort($files, 'strnatcasecmp');
        return $files;
    }
    
    /**
     * ファイルパスを自然順でソート
     */
    public static function sortPaths($paths) {
        usort($paths, function($a, $b) {
            return strnatcasecmp(basename($a), basename($b));
        });
        
        return $paths;
    }
    
    /**
     * バージョン番号を含むファイルをソート
     */
    public static function sortVersionedFiles($files) {
        usort($files, 'strnatcasecmp');
        return $files;
    }
    
    /**
     * ディレクトリ内のファイルを取得してソート
     */
    public static function getFilesFromDirectory($directory, $pattern = '*') {
        if (!is_dir($directory)) {
            return [];
        }
        
        $files = glob($directory . '/' . $pattern);
        
        usort($files, function($a, $b) {
            return strnatcasecmp(basename($a), basename($b));
        });
        
        return $files;
    }
    
    /**
     * 拡張子でグループ化してソート
     */
    public static function groupAndSort($files) {
        $grouped = [];
        
        foreach ($files as $file) {
            $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
            $grouped[$ext][] = $file;
        }
        
        // 各グループをソート
        foreach ($grouped as $ext => $fileList) {
            usort($grouped[$ext], 'strnatcasecmp');
        }
        
        // グループのキーもソート
        uksort($grouped, 'strnatcasecmp');
        
        return $grouped;
    }
}

// 使用例
$files = [
    'Report_2023.pdf',
    'report_2024.pdf',
    'Report_2022.pdf',
    'IMAGE10.jpg',
    'image2.jpg',
    'Image1.JPG',
    'document100.docx',
    'Document5.docx',
    'DOCUMENT20.docx'
];

echo "=== ソート前 ===\n";
print_r($files);

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

echo "\n=== 拡張子でグループ化 ===\n";
$grouped = FileListSorter::groupAndSort($files);
print_r($grouped);

例2: バージョン番号の比較

class VersionComparator {
    /**
     * バージョン番号を比較
     */
    public static function compare($version1, $version2) {
        return strnatcasecmp($version1, $version2);
    }
    
    /**
     * バージョン配列をソート
     */
    public static function sortVersions($versions) {
        usort($versions, 'strnatcasecmp');
        return $versions;
    }
    
    /**
     * 最新バージョンを取得
     */
    public static function getLatestVersion($versions) {
        if (empty($versions)) {
            return null;
        }
        
        $sorted = self::sortVersions($versions);
        return end($sorted);
    }
    
    /**
     * バージョン範囲のチェック
     */
    public static function isInRange($version, $min, $max) {
        return strnatcasecmp($version, $min) >= 0 && 
               strnatcasecmp($version, $max) <= 0;
    }
    
    /**
     * メジャーバージョンでグループ化
     */
    public static function groupByMajor($versions) {
        $grouped = [];
        
        foreach ($versions as $version) {
            // メジャーバージョンを取得(最初のドットまで)
            $major = strstr($version, '.', true);
            if ($major === false) {
                $major = $version;
            }
            
            $grouped[$major][] = $version;
        }
        
        // 各グループをソート
        foreach ($grouped as $major => $versionList) {
            usort($grouped[$major], 'strnatcasecmp');
        }
        
        // グループのキーもソート
        uksort($grouped, 'strnatcasecmp');
        
        return $grouped;
    }
}

// 使用例
$versions = [
    'v1.0.0',
    'V2.10.1',
    'v2.2.0',
    'V1.10.5',
    'v2.1.3',
    'V1.2.4',
    'v10.0.0',
    'V3.5.2'
];

echo "=== ソート前 ===\n";
print_r($versions);

echo "\n=== 自然順ソート後 ===\n";
$sorted = VersionComparator::sortVersions($versions);
print_r($sorted);

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

echo "\nバージョン範囲チェック:\n";
var_dump(VersionComparator::isInRange('v2.5.0', 'v2.0.0', 'v3.0.0'));  // true

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

例3: ページ番号のソート

class PageSorter {
    /**
     * ページ番号を含む文字列をソート
     */
    public static function sortPages($pages) {
        usort($pages, 'strnatcasecmp');
        return $pages;
    }
    
    /**
     * 章とページをソート
     */
    public static function sortChapters($chapters) {
        usort($chapters, function($a, $b) {
            return strnatcasecmp($a, $b);
        });
        
        return $chapters;
    }
    
    /**
     * ページ範囲を生成
     */
    public static function generatePageRange($start, $end) {
        $pages = [];
        
        // 数値部分を抽出
        preg_match('/(\d+)/', $start, $startMatch);
        preg_match('/(\d+)/', $end, $endMatch);
        
        if (!empty($startMatch) && !empty($endMatch)) {
            $prefix = preg_replace('/\d+/', '', $start);
            
            for ($i = (int)$startMatch[1]; $i <= (int)$endMatch[1]; $i++) {
                $pages[] = $prefix . $i;
            }
        }
        
        return $pages;
    }
    
    /**
     * ページ番号を正規化
     */
    public static function normalizePage($page) {
        // "Page 5" → "page5" のように正規化
        return strtolower(preg_replace('/\s+/', '', $page));
    }
}

// 使用例
$pages = [
    'Page 1',
    'PAGE 10',
    'page 2',
    'Page 20',
    'PAGE 3',
    'page 100'
];

echo "=== ソート前 ===\n";
print_r($pages);

echo "\n=== 自然順ソート後 ===\n";
$sorted = PageSorter::sortPages($pages);
print_r($sorted);

$chapters = [
    'Chapter 1.1',
    'CHAPTER 1.10',
    'chapter 1.2',
    'Chapter 2.1',
    'CHAPTER 1.20'
];

echo "\n=== 章のソート ===\n";
$sortedChapters = PageSorter::sortChapters($chapters);
print_r($sortedChapters);

echo "\n=== ページ範囲生成 ===\n";
$range = PageSorter::generatePageRange('Page 5', 'Page 10');
print_r($range);

例4: データベースレコードのソート

class RecordSorter {
    /**
     * レコード配列をフィールドでソート
     */
    public static function sortByField($records, $field) {
        usort($records, function($a, $b) use ($field) {
            return strnatcasecmp($a[$field], $b[$field]);
        });
        
        return $records;
    }
    
    /**
     * 複数フィールドでソート
     */
    public static function sortByMultipleFields($records, $fields) {
        usort($records, function($a, $b) use ($fields) {
            foreach ($fields as $field) {
                $result = strnatcasecmp($a[$field], $b[$field]);
                
                if ($result !== 0) {
                    return $result;
                }
            }
            
            return 0;
        });
        
        return $records;
    }
    
    /**
     * IDでソート
     */
    public static function sortById($records) {
        return self::sortByField($records, 'id');
    }
}

// 使用例
$records = [
    ['id' => 'USER10', 'name' => 'Alice', 'code' => 'A100'],
    ['id' => 'USER2', 'name' => 'Bob', 'code' => 'A20'],
    ['id' => 'user1', 'name' => 'Charlie', 'code' => 'A3'],
    ['id' => 'USER20', 'name' => 'David', 'code' => 'A200'],
];

echo "=== 元のデータ ===\n";
print_r($records);

echo "\n=== IDでソート ===\n";
$sortedById = RecordSorter::sortById($records);
print_r($sortedById);

echo "\n=== CODEでソート ===\n";
$sortedByCode = RecordSorter::sortByField($records, 'code');
print_r($sortedByCode);

echo "\n=== 複数フィールドでソート ===\n";
$multiSort = RecordSorter::sortByMultipleFields($records, ['code', 'id']);
print_r($multiSort);

例5: 製品カタログのソート

class ProductCatalogSorter {
    /**
     * 製品コードでソート
     */
    public static function sortByProductCode($products) {
        usort($products, function($a, $b) {
            return strnatcasecmp($a['code'], $b['code']);
        });
        
        return $products;
    }
    
    /**
     * カテゴリと製品コードでソート
     */
    public static function sortByCategoryAndCode($products) {
        usort($products, function($a, $b) {
            // まずカテゴリで比較
            $categoryCompare = strnatcasecmp($a['category'], $b['category']);
            
            if ($categoryCompare !== 0) {
                return $categoryCompare;
            }
            
            // 同じカテゴリなら製品コードで比較
            return strnatcasecmp($a['code'], $b['code']);
        });
        
        return $products;
    }
    
    /**
     * カテゴリでグループ化してソート
     */
    public static function groupByCategory($products) {
        $grouped = [];
        
        foreach ($products as $product) {
            $category = $product['category'];
            $grouped[$category][] = $product;
        }
        
        // 各カテゴリ内をソート
        foreach ($grouped as $category => $productList) {
            usort($grouped[$category], function($a, $b) {
                return strnatcasecmp($a['code'], $b['code']);
            });
        }
        
        // カテゴリキーをソート
        uksort($grouped, 'strnatcasecmp');
        
        return $grouped;
    }
}

// 使用例
$products = [
    ['code' => 'ELEC-100', 'name' => 'TV', 'category' => 'Electronics'],
    ['code' => 'ELEC-20', 'name' => 'Radio', 'category' => 'Electronics'],
    ['code' => 'BOOK-5', 'name' => 'Novel', 'category' => 'Books'],
    ['code' => 'ELEC-3', 'name' => 'Phone', 'category' => 'electronics'],
    ['code' => 'BOOK-100', 'name' => 'Dictionary', 'category' => 'Books'],
    ['code' => 'CLOTH-10', 'name' => 'Shirt', 'category' => 'Clothing'],
];

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

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

echo "\n=== カテゴリでグループ化 ===\n";
$grouped = ProductCatalogSorter::groupByCategory($products);
foreach ($grouped as $category => $productList) {
    echo "\n{$category}:\n";
    foreach ($productList as $product) {
        echo "  {$product['code']}: {$product['name']}\n";
    }
}

例6: IPアドレスのソート

class IpAddressSorter {
    /**
     * IPアドレスをソート
     */
    public static function sortIpAddresses($ips) {
        usort($ips, function($a, $b) {
            // 各オクテットを数値として比較
            $aParts = explode('.', $a);
            $bParts = explode('.', $b);
            
            for ($i = 0; $i < 4; $i++) {
                $aVal = isset($aParts[$i]) ? (int)$aParts[$i] : 0;
                $bVal = isset($bParts[$i]) ? (int)$bParts[$i] : 0;
                
                if ($aVal !== $bVal) {
                    return $aVal - $bVal;
                }
            }
            
            return 0;
        });
        
        return $ips;
    }
    
    /**
     * サブネットマスク付きIPをソート
     */
    public static function sortWithSubnet($ips) {
        usort($ips, function($a, $b) {
            // サブネットを除いてIPアドレス部分を比較
            $aIp = strstr($a, '/', true) ?: $a;
            $bIp = strstr($b, '/', true) ?: $b;
            
            return strnatcasecmp($aIp, $bIp);
        });
        
        return $ips;
    }
}

// 使用例
$ips = [
    '192.168.1.10',
    '192.168.1.2',
    '192.168.1.100',
    '10.0.0.50',
    '10.0.0.5',
    '172.16.0.1'
];

echo "=== IPアドレスソート ===\n";
$sorted = IpAddressSorter::sortIpAddresses($ips);
print_r($sorted);

$subnets = [
    '192.168.1.0/24',
    '192.168.10.0/24',
    '192.168.2.0/24',
    '10.0.0.0/8'
];

echo "\n=== サブネット付きIPソート ===\n";
$sortedSubnets = IpAddressSorter::sortWithSubnet($subnets);
print_r($sortedSubnets);

例7: タスク管理

class TaskSorter {
    /**
     * タスクを優先度でソート
     */
    public static function sortByPriority($tasks) {
        usort($tasks, function($a, $b) {
            return strnatcasecmp($a['priority'], $b['priority']);
        });
        
        return $tasks;
    }
    
    /**
     * タスクをステータスと番号でソート
     */
    public static function sortByStatusAndNumber($tasks) {
        $statusOrder = ['TODO' => 1, 'IN_PROGRESS' => 2, 'DONE' => 3];
        
        usort($tasks, function($a, $b) use ($statusOrder) {
            $aStatus = $statusOrder[$a['status']] ?? 999;
            $bStatus = $statusOrder[$b['status']] ?? 999;
            
            if ($aStatus !== $bStatus) {
                return $aStatus - $bStatus;
            }
            
            return strnatcasecmp($a['number'], $b['number']);
        });
        
        return $tasks;
    }
    
    /**
     * タスク番号でソート
     */
    public static function sortByTaskNumber($tasks) {
        usort($tasks, function($a, $b) {
            return strnatcasecmp($a['number'], $b['number']);
        });
        
        return $tasks;
    }
}

// 使用例
$tasks = [
    ['number' => 'TASK-10', 'title' => 'Fix bug', 'status' => 'TODO', 'priority' => 'P1'],
    ['number' => 'TASK-2', 'title' => 'Add feature', 'status' => 'IN_PROGRESS', 'priority' => 'P2'],
    ['number' => 'task-100', 'title' => 'Update docs', 'status' => 'TODO', 'priority' => 'P3'],
    ['number' => 'TASK-20', 'title' => 'Refactor code', 'status' => 'DONE', 'priority' => 'P1'],
];

echo "=== タスク番号でソート ===\n";
$sorted = TaskSorter::sortByTaskNumber($tasks);
foreach ($sorted as $task) {
    echo "{$task['number']}: {$task['title']}\n";
}

echo "\n=== ステータスと番号でソート ===\n";
$sorted = TaskSorter::sortByStatusAndNumber($tasks);
foreach ($sorted as $task) {
    echo "{$task['status']} - {$task['number']}: {$task['title']}\n";
}

他の比較関数との違い

$strings = ['file10', 'File2', 'FILE1', 'file20'];

// strcmp(): 大文字小文字を区別、辞書順
$result = $strings;
usort($result, 'strcmp');
print_r($result);
// FILE1, File2, file10, file20

// strcasecmp(): 大文字小文字を区別しない、辞書順
$result = $strings;
usort($result, 'strcasecmp');
print_r($result);
// FILE1, file10, File2, file20

// strnatcmp(): 大文字小文字を区別、自然順
$result = $strings;
usort($result, 'strnatcmp');
print_r($result);
// FILE1, File2, file10, file20

// strnatcasecmp(): 大文字小文字を区別しない、自然順
$result = $strings;
usort($result, 'strnatcasecmp');
print_r($result);
// FILE1, File2, file10, file20

パフォーマンスの考慮

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

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

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

echo "strnatcasecmp(): {$time1}秒\n";
echo "strcasecmp(): {$time2}秒\n";

// 自然順ソートは若干遅いが、通常は問題にならない

まとめ

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

できること:

  • 自然順(人間が期待する順序)での比較
  • 大文字小文字を区別しない比較
  • 数値を含む文字列の適切なソート

他の関数との違い:

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

推奨される使用場面:

  • ファイル名のソート
  • バージョン番号の比較
  • ページ番号のソート
  • 製品コードのソート
  • タスク番号のソート

自然順ソートの利点:

  • “file2″が”file10″より前に来る
  • 人間の直感に合った順序
  • ユーザーフレンドリー

関連関数:

  • strnatcmp(): 大文字小文字を区別する自然順比較
  • natsort(): 配列を自然順でソート
  • natcasesort(): 配列を自然順でソート(大文字小文字無視)

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

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