[PHP]strncasecmp関数を完全解説!最初のn文字を大文字小文字無視で比較

PHP

こんにちは!今回は、PHPの標準関数であるstrncasecmp()について詳しく解説していきます。文字列の最初のn文字だけを、大文字小文字を区別せずに比較できる関数です!

strncasecmp関数とは?

strncasecmp()関数は、2つの文字列の最初のn文字を、大文字小文字を区別せずに比較する関数です。

プレフィックスのチェック、部分的な文字列マッチング、前方一致検索など、様々な場面で活用できます!

基本的な構文

strncasecmp(string $string1, string $string2, int $length): int
  • $string1: 比較する1つ目の文字列
  • $string2: 比較する2つ目の文字列
  • $length: 比較する文字数
  • 戻り値:
    • 0: 指定した長さの範囲で等しい
    • < 0: $string1が$string2より小さい
    • > 0: $string1が$string2より大きい

基本的な使用例

シンプルな比較

// 最初の3文字を比較(大文字小文字無視)
echo strncasecmp("Hello", "HELLO", 3) . "\n";  // 0(Hel == HEL)
echo strncasecmp("Hello", "HELP", 3) . "\n";   // 0(Hel == HEL)
echo strncasecmp("Hello", "WORLD", 3) . "\n";  // 負の数(Hel < WOR)

// 文字列全体ではなく部分的な比較
$str1 = "HelloWorld";
$str2 = "HelloPHP";
echo strncasecmp($str1, $str2, 5) . "\n";  // 0(最初の5文字が同じ)

strcasecmp()との違い

$str1 = "Hello";
$str2 = "HelloWorld";

// strcasecmp(): 全体を比較
echo strcasecmp($str1, $str2) . "\n";  // 負の数(異なる)

// strncasecmp(): 最初の5文字のみ比較
echo strncasecmp($str1, $str2, 5) . "\n";  // 0(最初の5文字は同じ)

strncmp()との違い

$str1 = "hello";
$str2 = "HELLO";

// strncmp(): 大文字小文字を区別
echo strncmp($str1, $str2, 5) . "\n";  // 0以外(異なる)

// strncasecmp(): 大文字小文字を区別しない
echo strncasecmp($str1, $str2, 5) . "\n";  // 0(同じ)

長さが文字列より大きい場合

$str1 = "Hi";
$str2 = "HELLO";

// 文字列より長い長さを指定
echo strncasecmp($str1, $str2, 10) . "\n";  // 0以外(2文字しかないが比較)
echo strncasecmp($str1, $str2, 1) . "\n";   // 0(最初の1文字'H'が同じ)

実践的な使用例

例1: プレフィックスチェック

class PrefixChecker {
    /**
     * プレフィックスが一致するかチェック
     */
    public static function hasPrefix($string, $prefix) {
        return strncasecmp($string, $prefix, strlen($prefix)) === 0;
    }
    
    /**
     * 複数のプレフィックスのいずれかに一致するかチェック
     */
    public static function hasAnyPrefix($string, $prefixes) {
        foreach ($prefixes as $prefix) {
            if (self::hasPrefix($string, $prefix)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * プレフィックスでフィルタリング
     */
    public static function filterByPrefix($strings, $prefix) {
        return array_filter($strings, function($str) use ($prefix) {
            return self::hasPrefix($str, $prefix);
        });
    }
    
    /**
     * プレフィックスでグループ化
     */
    public static function groupByPrefix($strings, $prefixLength = 3) {
        $groups = [];
        
        foreach ($strings as $string) {
            $prefix = strtoupper(substr($string, 0, $prefixLength));
            $groups[$prefix][] = $string;
        }
        
        ksort($groups);
        
        return $groups;
    }
    
    /**
     * プレフィックスを削除
     */
    public static function removePrefix($string, $prefix) {
        if (self::hasPrefix($string, $prefix)) {
            return substr($string, strlen($prefix));
        }
        
        return $string;
    }
}

// 使用例
echo "=== プレフィックスチェック ===\n";
var_dump(PrefixChecker::hasPrefix("HelloWorld", "hello"));  // true
var_dump(PrefixChecker::hasPrefix("HelloWorld", "HELLO"));  // true
var_dump(PrefixChecker::hasPrefix("HelloWorld", "world"));  // false

$files = [
    'IMG_001.jpg',
    'img_002.jpg',
    'DOC_report.pdf',
    'doc_summary.pdf',
    'VID_clip.mp4'
];

echo "\n=== IMG で始まるファイル ===\n";
$images = PrefixChecker::filterByPrefix($files, 'IMG');
print_r($images);

echo "\n=== プレフィックスでグループ化 ===\n";
$grouped = PrefixChecker::groupByPrefix($files);
print_r($grouped);

echo "\n=== プレフィックス削除 ===\n";
echo PrefixChecker::removePrefix("IMG_001.jpg", "IMG_") . "\n";  // 001.jpg

例2: URLルーティング

class SimpleRouter {
    private $routes = [];
    
    /**
     * ルートを追加
     */
    public function addRoute($prefix, $handler) {
        $this->routes[] = [
            'prefix' => $prefix,
            'prefix_length' => strlen($prefix),
            'handler' => $handler
        ];
    }
    
    /**
     * URLをルーティング
     */
    public function route($url) {
        foreach ($this->routes as $route) {
            if (strncasecmp($url, $route['prefix'], $route['prefix_length']) === 0) {
                return [
                    'matched' => true,
                    'prefix' => $route['prefix'],
                    'handler' => $route['handler'],
                    'remaining' => substr($url, $route['prefix_length'])
                ];
            }
        }
        
        return ['matched' => false];
    }
    
    /**
     * 最長一致でルーティング
     */
    public function routeLongestMatch($url) {
        $bestMatch = null;
        $longestLength = 0;
        
        foreach ($this->routes as $route) {
            if (strncasecmp($url, $route['prefix'], $route['prefix_length']) === 0) {
                if ($route['prefix_length'] > $longestLength) {
                    $longestLength = $route['prefix_length'];
                    $bestMatch = [
                        'matched' => true,
                        'prefix' => $route['prefix'],
                        'handler' => $route['handler'],
                        'remaining' => substr($url, $route['prefix_length'])
                    ];
                }
            }
        }
        
        return $bestMatch ?? ['matched' => false];
    }
}

// 使用例
$router = new SimpleRouter();
$router->addRoute('/api/', 'ApiController');
$router->addRoute('/admin/', 'AdminController');
$router->addRoute('/user/', 'UserController');
$router->addRoute('/api/v1/', 'ApiV1Controller');

echo "=== ルーティング例 ===\n";

$result = $router->route('/API/users');
print_r($result);
// matched => true, prefix => /api/, handler => ApiController

$result = $router->route('/ADMIN/dashboard');
print_r($result);
// matched => true, prefix => /admin/, handler => AdminController

echo "\n=== 最長一致 ===\n";
$result = $router->routeLongestMatch('/api/v1/users');
print_r($result);
// prefix => /api/v1/, handler => ApiV1Controller

例3: ファイルタイプの判定

class FileTypeDetector {
    private static $types = [
        'IMG' => ['jpg', 'jpeg', 'png', 'gif', 'bmp'],
        'DOC' => ['pdf', 'doc', 'docx', 'txt'],
        'VID' => ['mp4', 'avi', 'mov', 'wmv'],
        'AUD' => ['mp3', 'wav', 'flac', 'aac']
    ];
    
    /**
     * ファイル名からタイプを判定
     */
    public static function detectType($filename) {
        foreach (self::$types as $prefix => $extensions) {
            if (strncasecmp($filename, $prefix, strlen($prefix)) === 0) {
                return strtoupper($prefix);
            }
        }
        
        return 'UNKNOWN';
    }
    
    /**
     * プレフィックスが正しいかチェック
     */
    public static function hasValidPrefix($filename) {
        $type = self::detectType($filename);
        
        if ($type === 'UNKNOWN') {
            return false;
        }
        
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        
        return in_array($ext, self::$types[$type]);
    }
    
    /**
     * ファイル名を正規化
     */
    public static function normalizeFilename($filename) {
        $type = self::detectType($filename);
        
        if ($type === 'UNKNOWN') {
            return $filename;
        }
        
        // プレフィックスを大文字に統一
        $prefixLength = strlen($type);
        return strtoupper(substr($filename, 0, $prefixLength)) . 
               substr($filename, $prefixLength);
    }
    
    /**
     * タイプ別にグループ化
     */
    public static function groupByType($files) {
        $grouped = [];
        
        foreach ($files as $file) {
            $type = self::detectType($file);
            $grouped[$type][] = $file;
        }
        
        return $grouped;
    }
}

// 使用例
$files = [
    'img_001.jpg',
    'IMG_002.png',
    'doc_report.pdf',
    'DOC_summary.docx',
    'vid_clip.mp4',
    'random_file.txt'
];

echo "=== ファイルタイプ判定 ===\n";
foreach ($files as $file) {
    echo "{$file}: " . FileTypeDetector::detectType($file) . "\n";
}

echo "\n=== プレフィックス検証 ===\n";
var_dump(FileTypeDetector::hasValidPrefix('IMG_001.jpg'));  // true
var_dump(FileTypeDetector::hasValidPrefix('IMG_001.mp4'));  // false

echo "\n=== ファイル名正規化 ===\n";
foreach ($files as $file) {
    echo FileTypeDetector::normalizeFilename($file) . "\n";
}

echo "\n=== タイプ別グループ化 ===\n";
$grouped = FileTypeDetector::groupByType($files);
print_r($grouped);

例4: コマンド解析

class CommandParser {
    private $commands = [
        'GET' => 'read',
        'SET' => 'write',
        'DEL' => 'delete',
        'LIST' => 'list'
    ];
    
    /**
     * コマンドを解析
     */
    public function parse($input) {
        foreach ($this->commands as $cmd => $action) {
            if (strncasecmp($input, $cmd, strlen($cmd)) === 0) {
                $args = trim(substr($input, strlen($cmd)));
                
                return [
                    'command' => strtoupper($cmd),
                    'action' => $action,
                    'arguments' => $args
                ];
            }
        }
        
        return ['command' => null, 'error' => 'Unknown command'];
    }
    
    /**
     * 補完候補を取得
     */
    public function getCompletions($partial) {
        $completions = [];
        $partialLength = strlen($partial);
        
        foreach (array_keys($this->commands) as $cmd) {
            if (strncasecmp($cmd, $partial, $partialLength) === 0) {
                $completions[] = $cmd;
            }
        }
        
        return $completions;
    }
    
    /**
     * コマンドのヘルプを表示
     */
    public function getHelp($commandPrefix = null) {
        $help = [];
        
        foreach ($this->commands as $cmd => $action) {
            if ($commandPrefix === null || 
                strncasecmp($cmd, $commandPrefix, strlen($commandPrefix)) === 0) {
                $help[] = sprintf("%-10s - %s", $cmd, $action);
            }
        }
        
        return $help;
    }
}

// 使用例
$parser = new CommandParser();

echo "=== コマンド解析 ===\n";
$result = $parser->parse('get user:123');
print_r($result);
// command => GET, action => read, arguments => user:123

$result = $parser->parse('SET key value');
print_r($result);
// command => SET, action => write, arguments => key value

echo "\n=== 補完候補 ===\n";
$completions = $parser->getCompletions('ge');
print_r($completions);  // ['GET']

$completions = $parser->getCompletions('l');
print_r($completions);  // ['LIST']

echo "\n=== ヘルプ ===\n";
$help = $parser->getHelp();
foreach ($help as $line) {
    echo $line . "\n";
}

例5: プロトコル判定

class ProtocolDetector {
    private static $protocols = [
        'HTTP://' => 'http',
        'HTTPS://' => 'https',
        'FTP://' => 'ftp',
        'FTPS://' => 'ftps',
        'SSH://' => 'ssh',
        'MAILTO:' => 'mailto',
        'TEL:' => 'tel'
    ];
    
    /**
     * プロトコルを検出
     */
    public static function detect($url) {
        foreach (self::$protocols as $prefix => $protocol) {
            if (strncasecmp($url, $prefix, strlen($prefix)) === 0) {
                return [
                    'protocol' => $protocol,
                    'prefix' => $prefix,
                    'url_without_protocol' => substr($url, strlen($prefix))
                ];
            }
        }
        
        return ['protocol' => null];
    }
    
    /**
     * プロトコルが安全かチェック
     */
    public static function isSecure($url) {
        $info = self::detect($url);
        
        if (!isset($info['protocol'])) {
            return false;
        }
        
        return in_array($info['protocol'], ['https', 'ftps', 'ssh']);
    }
    
    /**
     * HTTPまたはHTTPSかチェック
     */
    public static function isHttp($url) {
        $info = self::detect($url);
        
        if (!isset($info['protocol'])) {
            return false;
        }
        
        return in_array($info['protocol'], ['http', 'https']);
    }
    
    /**
     * URLを正規化
     */
    public static function normalizeUrl($url) {
        $info = self::detect($url);
        
        if (!isset($info['protocol'])) {
            return $url;
        }
        
        $prefix = strtolower($info['prefix']);
        
        return $prefix . $info['url_without_protocol'];
    }
}

// 使用例
$urls = [
    'HTTP://example.com',
    'https://secure.example.com',
    'FTP://ftp.example.com',
    'mailto:user@example.com',
    'tel:+1234567890'
];

echo "=== プロトコル検出 ===\n";
foreach ($urls as $url) {
    $info = ProtocolDetector::detect($url);
    echo "{$url}: " . ($info['protocol'] ?? 'unknown') . "\n";
}

echo "\n=== セキュリティチェック ===\n";
foreach ($urls as $url) {
    $secure = ProtocolDetector::isSecure($url) ? '安全' : '非安全';
    echo "{$url}: {$secure}\n";
}

echo "\n=== URL正規化 ===\n";
foreach ($urls as $url) {
    echo ProtocolDetector::normalizeUrl($url) . "\n";
}

例6: データ検証

class DataValidator {
    /**
     * 電話番号の国コードをチェック
     */
    public static function validateCountryCode($phone, $expectedCode) {
        // +81, +1 などの国コード
        $codeLength = strlen($expectedCode);
        
        return strncasecmp($phone, $expectedCode, $codeLength) === 0;
    }
    
    /**
     * 郵便番号のプレフィックスをチェック
     */
    public static function validateZipPrefix($zip, $validPrefixes) {
        foreach ($validPrefixes as $prefix) {
            if (strncasecmp($zip, $prefix, strlen($prefix)) === 0) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 製品コードのフォーマットをチェック
     */
    public static function validateProductCode($code, $requiredPrefix) {
        $prefixLength = strlen($requiredPrefix);
        
        if (strncasecmp($code, $requiredPrefix, $prefixLength) !== 0) {
            return [
                'valid' => false,
                'error' => "製品コードは {$requiredPrefix} で始まる必要があります"
            ];
        }
        
        return ['valid' => true];
    }
    
    /**
     * バージョン番号のメジャーバージョンをチェック
     */
    public static function validateMajorVersion($version, $requiredMajor) {
        // v2.1.3 の場合、v2 かどうかチェック
        $requiredLength = strlen($requiredMajor);
        
        return strncasecmp($version, $requiredMajor, $requiredLength) === 0;
    }
}

// 使用例
echo "=== 国コード検証 ===\n";
var_dump(DataValidator::validateCountryCode('+81-90-1234-5678', '+81'));  // true
var_dump(DataValidator::validateCountryCode('+1-555-1234', '+81'));        // false

echo "\n=== 郵便番号プレフィックス検証 ===\n";
$validPrefixes = ['100', '101', '102'];  // 東京都千代田区
var_dump(DataValidator::validateZipPrefix('100-0001', $validPrefixes));   // true
var_dump(DataValidator::validateZipPrefix('200-0001', $validPrefixes));   // false

echo "\n=== 製品コード検証 ===\n";
$result = DataValidator::validateProductCode('PROD-12345', 'PROD');
print_r($result);  // valid => true

$result = DataValidator::validateProductCode('ITEM-12345', 'PROD');
print_r($result);  // valid => false

echo "\n=== メジャーバージョン検証 ===\n";
var_dump(DataValidator::validateMajorVersion('v2.1.3', 'v2'));   // true
var_dump(DataValidator::validateMajorVersion('V2.5.0', 'v2'));   // true
var_dump(DataValidator::validateMajorVersion('v3.0.0', 'v2'));   // false

例7: テキスト検索

class TextSearcher {
    /**
     * 前方一致検索
     */
    public static function searchByPrefix($texts, $prefix) {
        $results = [];
        $prefixLength = strlen($prefix);
        
        foreach ($texts as $text) {
            if (strncasecmp($text, $prefix, $prefixLength) === 0) {
                $results[] = $text;
            }
        }
        
        return $results;
    }
    
    /**
     * オートコンプリート候補を生成
     */
    public static function autocomplete($words, $partial, $maxResults = 10) {
        $matches = self::searchByPrefix($words, $partial);
        
        // 結果を制限
        return array_slice($matches, 0, $maxResults);
    }
    
    /**
     * 複数の候補からベストマッチを探す
     */
    public static function findBestMatch($query, $candidates) {
        $queryLength = strlen($query);
        $matches = [];
        
        foreach ($candidates as $candidate) {
            if (strncasecmp($query, $candidate, $queryLength) === 0) {
                $matches[] = [
                    'text' => $candidate,
                    'score' => $queryLength / strlen($candidate)  // 類似度スコア
                ];
            }
        }
        
        // スコアでソート
        usort($matches, function($a, $b) {
            return $b['score'] <=> $a['score'];
        });
        
        return $matches;
    }
}

// 使用例
$words = [
    'Apple', 'Application', 'Apply', 'Appreciate',
    'Banana', 'Band', 'Bank', 'Banner'
];

echo "=== 前方一致検索 ===\n";
$results = TextSearcher::searchByPrefix($words, 'app');
print_r($results);
// Apple, Application, Apply, Appreciate

echo "\n=== オートコンプリート ===\n";
$completions = TextSearcher::autocomplete($words, 'ban', 3);
print_r($completions);
// Banana, Band, Bank

echo "\n=== ベストマッチ ===\n";
$bestMatches = TextSearcher::findBestMatch('app', $words);
foreach ($bestMatches as $match) {
    echo "{$match['text']}: スコア {$match['score']}\n";
}

パフォーマンスの考慮

// 大量の前方一致チェック
$data = [];
for ($i = 0; $i < 10000; $i++) {
    $data[] = 'PREFIX' . $i;
}

// strncasecmp()を使用
$start = microtime(true);
$count = 0;
foreach ($data as $item) {
    if (strncasecmp($item, 'PREFIX', 6) === 0) {
        $count++;
    }
}
$time1 = microtime(true) - $start;

// substr() + strcasecmp()を使用
$start = microtime(true);
$count = 0;
foreach ($data as $item) {
    if (strcasecmp(substr($item, 0, 6), 'PREFIX') === 0) {
        $count++;
    }
}
$time2 = microtime(true) - $start;

echo "strncasecmp(): {$time1}秒\n";
echo "substr() + strcasecmp(): {$time2}秒\n";

// strncasecmp()の方が効率的

まとめ

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

できること:

  • 文字列の最初のn文字を比較
  • 大文字小文字を区別しない比較
  • プレフィックスのチェック

他の関数との違い:

  • strcmp(): 全体を比較、大文字小文字を区別
  • strcasecmp(): 全体を比較、大文字小文字を区別しない
  • strncmp(): 最初のn文字を比較、大文字小文字を区別
  • strncasecmp(): 最初のn文字を比較、大文字小文字を区別しない

推奨される使用場面:

  • プレフィックスのチェック
  • URLルーティング
  • ファイル名の判定
  • コマンド解析
  • プロトコル判定
  • オートコンプリート
  • 前方一致検索

注意点:

  • 指定した長さ分だけ比較(文字列全体ではない)
  • マルチバイト文字には注意(mb_strncasecmpは存在しない)
  • 長さが文字列より大きい場合でも動作する

関連関数:

  • strncmp(): 大文字小文字を区別する部分比較
  • strcasecmp(): 大文字小文字を区別しない全体比較
  • substr(): 部分文字列の取得
  • strpos(): 文字列の位置検索

strncasecmp()は、プレフィックスチェックや前方一致検索など、文字列の先頭部分だけを柔軟に比較したい場面で非常に便利です。URLルーティングやコマンド解析など、実用的な場面で積極的に活用しましょう!

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