[PHP]strncmp関数を完全解説!最初のn文字を比較する方法

PHP

こんにちは!今回は、PHPの標準関数であるstrncmp()について詳しく解説していきます。文字列の最初のn文字だけを比較できる、便利な関数です!

strncmp関数とは?

strncmp()関数は、2つの文字列の最初のn文字を比較する関数です。

文字列全体ではなく、指定した長さの部分だけを比較したい場合に使用します。プレフィックスのチェックや部分的なマッチングに便利です!

基本的な構文

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

基本的な使用例

シンプルな比較

// 最初の3文字を比較
echo strncmp("Hello", "Hello", 3) . "\n";    // 0(Hel == Hel)
echo strncmp("Hello", "Help", 3) . "\n";     // 0(Hel == Hel)
echo strncmp("Hello", "World", 3) . "\n";    // 負の数(Hel < Wor)

// 文字列全体ではなく部分的な比較
$str1 = "HelloWorld";
$str2 = "HelloPHP";
echo strncmp($str1, $str2, 5) . "\n";  // 0(最初の5文字が同じ)
echo strncmp($str1, $str2, 10) . "\n"; // 正の数(6文字目以降が異なる)

strcmp()との違い

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

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

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

大文字小文字の区別

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

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

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

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

$str1 = "Hi";
$str2 = "Hello";

// 文字列より長い長さを指定しても動作する
echo strncmp($str1, $str2, 10) . "\n";  // 正の数('H'の次が'i' vs 'e')
echo strncmp($str1, $str2, 1) . "\n";   // 0(最初の1文字'H'が同じ)
echo strncmp($str1, $str2, 2) . "\n";   // 正の数(2文字目が異なる)

実践的な使用例

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

class FilePrefix {
    /**
     * プレフィックスが一致するかチェック
     */
    public static function hasPrefix($filename, $prefix) {
        return strncmp($filename, $prefix, strlen($prefix)) === 0;
    }
    
    /**
     * プレフィックスでファイルをフィルタリング
     */
    public static function filterByPrefix($files, $prefix) {
        return array_filter($files, function($file) use ($prefix) {
            return self::hasPrefix($file, $prefix);
        });
    }
    
    /**
     * プレフィックスでグループ化
     */
    public static function groupByPrefix($files, $prefixLength = 3) {
        $groups = [];
        
        foreach ($files as $file) {
            $prefix = substr($file, 0, $prefixLength);
            $groups[$prefix][] = $file;
        }
        
        ksort($groups);
        
        return $groups;
    }
    
    /**
     * プレフィックスを削除
     */
    public static function removePrefix($filename, $prefix) {
        if (self::hasPrefix($filename, $prefix)) {
            return substr($filename, strlen($prefix));
        }
        
        return $filename;
    }
    
    /**
     * 複数のプレフィックスをチェック
     */
    public static function matchesAnyPrefix($filename, $prefixes) {
        foreach ($prefixes as $prefix) {
            if (self::hasPrefix($filename, $prefix)) {
                return $prefix;
            }
        }
        
        return null;
    }
}

// 使用例
$files = [
    'IMG_001.jpg',
    'IMG_002.jpg',
    'DOC_report.pdf',
    'DOC_summary.pdf',
    'VID_clip.mp4',
    'TMP_cache.dat'
];

echo "=== プレフィックスチェック ===\n";
var_dump(FilePrefix::hasPrefix('IMG_001.jpg', 'IMG'));  // true
var_dump(FilePrefix::hasPrefix('IMG_001.jpg', 'DOC'));  // false

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

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

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

echo "\n=== 複数プレフィックスマッチ ===\n";
$match = FilePrefix::matchesAnyPrefix('DOC_report.pdf', ['IMG', 'DOC', 'VID']);
echo "マッチしたプレフィックス: {$match}\n";  // DOC

例2: コマンドパーサー

class CommandParser {
    private $commands = [
        'GET' => ['min_args' => 1, 'action' => 'retrieve'],
        'SET' => ['min_args' => 2, 'action' => 'store'],
        'DELETE' => ['min_args' => 1, 'action' => 'remove'],
        'LIST' => ['min_args' => 0, 'action' => 'list_all']
    ];
    
    /**
     * コマンドを解析
     */
    public function parse($input) {
        $input = trim($input);
        
        foreach ($this->commands as $cmd => $config) {
            $cmdLength = strlen($cmd);
            
            if (strncmp($input, $cmd, $cmdLength) === 0) {
                // コマンドの後ろがスペースまたは終端かチェック
                if (strlen($input) === $cmdLength || 
                    $input[$cmdLength] === ' ') {
                    
                    $args = trim(substr($input, $cmdLength));
                    $argArray = $args ? explode(' ', $args) : [];
                    
                    if (count($argArray) < $config['min_args']) {
                        return [
                            'success' => false,
                            'error' => "{$cmd} requires at least {$config['min_args']} argument(s)"
                        ];
                    }
                    
                    return [
                        'success' => true,
                        'command' => $cmd,
                        'action' => $config['action'],
                        'arguments' => $argArray
                    ];
                }
            }
        }
        
        return ['success' => false, 'error' => 'Unknown command'];
    }
    
    /**
     * コマンド補完候補を取得
     */
    public function getCompletions($partial) {
        $completions = [];
        $partialLength = strlen($partial);
        
        foreach (array_keys($this->commands) as $cmd) {
            if (strncmp($cmd, $partial, $partialLength) === 0) {
                $completions[] = $cmd;
            }
        }
        
        return $completions;
    }
    
    /**
     * ヘルプを表示
     */
    public function getHelp($commandPrefix = null) {
        $help = [];
        
        foreach ($this->commands as $cmd => $config) {
            if ($commandPrefix === null) {
                $help[] = sprintf(
                    "%-10s - %s (min args: %d)",
                    $cmd,
                    $config['action'],
                    $config['min_args']
                );
            } else {
                $prefixLength = strlen($commandPrefix);
                if (strncmp($cmd, $commandPrefix, $prefixLength) === 0) {
                    $help[] = sprintf(
                        "%-10s - %s",
                        $cmd,
                        $config['action']
                    );
                }
            }
        }
        
        return $help;
    }
}

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

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

$result = $parser->parse('SET key value');
print_r($result);
// success => true, command => SET, action => store

$result = $parser->parse('DELETE');
print_r($result);
// success => false, error => DELETE requires at least 1 argument(s)

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

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

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

class VersionComparator {
    /**
     * メジャーバージョンを比較
     */
    public static function compareMajor($version1, $version2) {
        // v2.1.3 と v2.5.0 の最初の2文字(v2)を比較
        return strncmp($version1, $version2, 2);
    }
    
    /**
     * 特定のメジャーバージョンかチェック
     */
    public static function isMajorVersion($version, $major) {
        return strncmp($version, $major, strlen($major)) === 0;
    }
    
    /**
     * メジャーバージョンでフィルタリング
     */
    public static function filterByMajor($versions, $majorVersion) {
        return array_filter($versions, function($version) use ($majorVersion) {
            return self::isMajorVersion($version, $majorVersion);
        });
    }
    
    /**
     * バージョンプレフィックスをチェック
     */
    public static function hasVersionPrefix($version) {
        return strncmp($version, 'v', 1) === 0 || 
               strncmp($version, 'V', 1) === 0;
    }
    
    /**
     * バージョン範囲をチェック
     */
    public static function isInMajorRange($version, $minMajor, $maxMajor) {
        $majorLength = max(strlen($minMajor), strlen($maxMajor));
        
        return strncmp($version, $minMajor, strlen($minMajor)) >= 0 && 
               strncmp($version, $maxMajor, strlen($maxMajor)) <= 0;
    }
}

// 使用例
$versions = [
    'v1.0.0',
    'v1.5.2',
    'v2.0.0',
    'v2.3.1',
    'v3.0.0',
    'v10.1.0'
];

echo "=== メジャーバージョン v2 のフィルタリング ===\n";
$v2Versions = VersionComparator::filterByMajor($versions, 'v2');
print_r($v2Versions);

echo "\n=== バージョンプレフィックスチェック ===\n";
var_dump(VersionComparator::hasVersionPrefix('v2.0.0'));  // true
var_dump(VersionComparator::hasVersionPrefix('2.0.0'));   // false

echo "\n=== メジャーバージョン比較 ===\n";
var_dump(VersionComparator::isMajorVersion('v2.5.0', 'v2'));  // true
var_dump(VersionComparator::isMajorVersion('v3.0.0', 'v2'));  // false

echo "\n=== 範囲チェック(v2.x から v3.x) ===\n";
foreach ($versions as $version) {
    $inRange = VersionComparator::isInMajorRange($version, 'v2', 'v3');
    echo "{$version}: " . ($inRange ? '範囲内' : '範囲外') . "\n";
}

例4: URLパス解析

class PathMatcher {
    /**
     * パスがプレフィックスに一致するかチェック
     */
    public static function matchesPrefix($path, $prefix) {
        return strncmp($path, $prefix, strlen($prefix)) === 0;
    }
    
    /**
     * ルートパスをマッチング
     */
    public static function matchRoute($path, $routes) {
        foreach ($routes as $route => $handler) {
            if (self::matchesPrefix($path, $route)) {
                return [
                    'matched' => true,
                    'route' => $route,
                    'handler' => $handler,
                    'remaining' => substr($path, strlen($route))
                ];
            }
        }
        
        return ['matched' => false];
    }
    
    /**
     * 最長一致ルートを見つける
     */
    public static function findLongestMatch($path, $routes) {
        $bestMatch = null;
        $longestLength = 0;
        
        foreach ($routes as $route => $handler) {
            $routeLength = strlen($route);
            
            if (strncmp($path, $route, $routeLength) === 0 && 
                $routeLength > $longestLength) {
                
                $longestLength = $routeLength;
                $bestMatch = [
                    'matched' => true,
                    'route' => $route,
                    'handler' => $handler,
                    'remaining' => substr($path, $routeLength)
                ];
            }
        }
        
        return $bestMatch ?? ['matched' => false];
    }
    
    /**
     * パスセグメントを取得
     */
    public static function getFirstSegment($path) {
        // /api/users/123 → /api
        $length = strcspn($path, '/', 1);  // 最初の/をスキップ
        return substr($path, 0, $length + 1);
    }
}

// 使用例
$routes = [
    '/api/' => 'ApiController',
    '/api/v1/' => 'ApiV1Controller',
    '/api/v2/' => 'ApiV2Controller',
    '/admin/' => 'AdminController',
    '/user/' => 'UserController'
];

echo "=== パスマッチング ===\n";
$result = PathMatcher::matchRoute('/api/users', $routes);
print_r($result);

echo "\n=== 最長一致 ===\n";
$result = PathMatcher::findLongestMatch('/api/v1/users', $routes);
print_r($result);
// route => /api/v1/, handler => ApiV1Controller

$result = PathMatcher::findLongestMatch('/api/v2/products', $routes);
print_r($result);
// route => /api/v2/, handler => ApiV2Controller

例5: データ検証

class DataValidator {
    /**
     * IDプレフィックスを検証
     */
    public static function validateIdPrefix($id, $requiredPrefix) {
        $prefixLength = strlen($requiredPrefix);
        
        if (strncmp($id, $requiredPrefix, $prefixLength) !== 0) {
            return [
                'valid' => false,
                'error' => "IDは {$requiredPrefix} で始まる必要があります",
                'actual_prefix' => substr($id, 0, $prefixLength)
            ];
        }
        
        return ['valid' => true];
    }
    
    /**
     * 電話番号の国コードを検証
     */
    public static function validatePhonePrefix($phone, $countryCode) {
        $codeLength = strlen($countryCode);
        
        if (strncmp($phone, $countryCode, $codeLength) === 0) {
            return [
                'valid' => true,
                'country_code' => $countryCode,
                'number' => substr($phone, $codeLength)
            ];
        }
        
        return [
            'valid' => false,
            'error' => "電話番号は {$countryCode} で始まる必要があります"
        ];
    }
    
    /**
     * 複数のプレフィックスのいずれかをチェック
     */
    public static function hasValidPrefix($value, $validPrefixes) {
        foreach ($validPrefixes as $prefix) {
            if (strncmp($value, $prefix, strlen($prefix)) === 0) {
                return [
                    'valid' => true,
                    'matched_prefix' => $prefix
                ];
            }
        }
        
        return [
            'valid' => false,
            'error' => '有効なプレフィックスが見つかりません',
            'valid_prefixes' => $validPrefixes
        ];
    }
    
    /**
     * 日付フォーマットのプレフィックスをチェック
     */
    public static function validateDatePrefix($date, $yearPrefix) {
        // 2024-01-15 が 2024 で始まるかチェック
        return strncmp($date, $yearPrefix, strlen($yearPrefix)) === 0;
    }
}

// 使用例
echo "=== IDプレフィックス検証 ===\n";
$result = DataValidator::validateIdPrefix('USER12345', 'USER');
print_r($result);  // valid => true

$result = DataValidator::validateIdPrefix('ADMIN12345', 'USER');
print_r($result);  // valid => false

echo "\n=== 電話番号検証 ===\n";
$result = DataValidator::validatePhonePrefix('+81-90-1234-5678', '+81');
print_r($result);
// valid => true, country_code => +81, number => -90-1234-5678

echo "\n=== 複数プレフィックス検証 ===\n";
$validPrefixes = ['PROD', 'TEST', 'DEV'];
$result = DataValidator::hasValidPrefix('PROD-12345', $validPrefixes);
print_r($result);
// valid => true, matched_prefix => PROD

echo "\n=== 日付プレフィックス検証 ===\n";
var_dump(DataValidator::validateDatePrefix('2024-01-15', '2024'));  // true
var_dump(DataValidator::validateDatePrefix('2023-12-31', '2024'));  // false

例6: プロトコル判定

class ProtocolChecker {
    private static $protocols = [
        'http://' => 'HTTP',
        'https://' => 'HTTPS',
        'ftp://' => 'FTP',
        'ftps://' => 'FTPS',
        'ssh://' => 'SSH',
        'mailto:' => 'MAILTO',
        'tel:' => 'TEL',
        'file://' => 'FILE'
    ];
    
    /**
     * プロトコルを検出
     */
    public static function detect($url) {
        foreach (self::$protocols as $prefix => $protocol) {
            if (strncmp($url, $prefix, strlen($prefix)) === 0) {
                return [
                    'protocol' => $protocol,
                    'prefix' => $prefix,
                    'url_without_protocol' => substr($url, strlen($prefix))
                ];
            }
        }
        
        return ['protocol' => null];
    }
    
    /**
     * HTTPまたはHTTPSかチェック
     */
    public static function isHttp($url) {
        return strncmp($url, 'http://', 7) === 0 || 
               strncmp($url, 'https://', 8) === 0;
    }
    
    /**
     * セキュアプロトコルかチェック
     */
    public static function isSecure($url) {
        return strncmp($url, 'https://', 8) === 0 || 
               strncmp($url, 'ftps://', 7) === 0 || 
               strncmp($url, 'ssh://', 6) === 0;
    }
    
    /**
     * プロトコルを削除
     */
    public static function removeProtocol($url) {
        $info = self::detect($url);
        
        if (isset($info['protocol'])) {
            return $info['url_without_protocol'];
        }
        
        return $url;
    }
}

// 使用例
$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 = ProtocolChecker::detect($url);
    echo "{$url}: " . ($info['protocol'] ?? 'unknown') . "\n";
}

echo "\n=== HTTP/HTTPSチェック ===\n";
foreach ($urls as $url) {
    $isHttp = ProtocolChecker::isHttp($url) ? 'Yes' : 'No';
    echo "{$url}: {$isHttp}\n";
}

echo "\n=== セキュアチェック ===\n";
foreach ($urls as $url) {
    $isSecure = ProtocolChecker::isSecure($url) ? 'Secure' : 'Not secure';
    echo "{$url}: {$isSecure}\n";
}

例7: ログエントリの解析

class LogParser {
    /**
     * ログレベルを抽出
     */
    public static function extractLevel($logLine) {
        $levels = ['ERROR', 'WARN', 'INFO', 'DEBUG'];
        
        foreach ($levels as $level) {
            $levelLength = strlen($level);
            
            // ログの先頭または [の後ろでレベルをチェック
            $pos = strpos($logLine, '[');
            if ($pos !== false) {
                $afterBracket = substr($logLine, $pos + 1);
                
                if (strncmp($afterBracket, $level, $levelLength) === 0) {
                    return [
                        'level' => $level,
                        'position' => $pos + 1
                    ];
                }
            }
        }
        
        return null;
    }
    
    /**
     * タイムスタンプをチェック
     */
    public static function hasTimestamp($logLine) {
        // [2024-01-15 または 2024-01-15 で始まるかチェック
        $formats = ['[2024', '[2023', '2024', '2023'];
        
        foreach ($formats as $format) {
            if (strncmp($logLine, $format, strlen($format)) === 0) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 特定のレベルのログをフィルタ
     */
    public static function filterByLevel($logs, $level) {
        return array_filter($logs, function($log) use ($level) {
            $info = self::extractLevel($log);
            return $info && $info['level'] === $level;
        });
    }
}

// 使用例
$logs = [
    '[2024-01-15 10:30:00] [ERROR] Database connection failed',
    '[2024-01-15 10:31:00] [INFO] User logged in',
    '[2024-01-15 10:32:00] [WARN] High memory usage',
    '[2024-01-15 10:33:00] [DEBUG] Processing request',
    '[2024-01-15 10:34:00] [ERROR] File not found'
];

echo "=== レベル抽出 ===\n";
foreach ($logs as $log) {
    $info = LogParser::extractLevel($log);
    if ($info) {
        echo "{$info['level']}: " . substr($log, 0, 50) . "...\n";
    }
}

echo "\n=== タイムスタンプチェック ===\n";
foreach ($logs as $log) {
    $hasTs = LogParser::hasTimestamp($log) ? 'Yes' : 'No';
    echo substr($log, 0, 30) . "... : {$hasTs}\n";
}

echo "\n=== エラーログのみ ===\n";
$errors = LogParser::filterByLevel($logs, 'ERROR');
print_r($errors);

パフォーマンスの考慮

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

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

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

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

echo "strncmp(): {$time1}秒\n";
echo "substr() + strcmp(): {$time2}秒\n";
echo "strpos(): {$time3}秒\n";

// strncmp()が最も効率的

まとめ

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

できること:

  • 文字列の最初のn文字を比較
  • プレフィックスのチェック
  • 部分的な文字列マッチング

他の関数との違い:

  • strcmp(): 文字列全体を比較
  • strncmp(): 最初のn文字のみ比較
  • strcasecmp(): 全体を比較、大文字小文字無視
  • strncasecmp(): 最初のn文字を比較、大文字小文字無視

推奨される使用場面:

  • ファイル名のプレフィックスチェック
  • コマンドパーサー
  • URLルーティング
  • バージョン番号の比較
  • プロトコル判定
  • IDの検証

利点:

  • 効率的(全体を比較する必要がない)
  • バイナリセーフ
  • シンプルで直感的

注意点:

  • 大文字小文字を区別する
  • マルチバイト文字には注意
  • 指定した長さが文字列より大きくても動作する

関連関数:

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

strncmp()は、プレフィックスチェックや部分的な文字列マッチングが必要な場面で非常に便利です。効率的で直感的な比較が可能なので、適切な場面で積極的に活用しましょう!

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