[PHP]substr_compare関数を完全解説!部分文字列を比較する方法

PHP

こんにちは!今回は、PHPの標準関数であるsubstr_compare()について詳しく解説していきます。文字列の一部を効率的に比較できる、非常に便利な関数です!

substr_compare関数とは?

substr_compare()関数は、文字列の指定した位置から別の文字列と比較する関数です。

substr() + strcmp()を一度に行える効率的な関数で、大文字小文字の区別やバイナリセーフな比較が可能です!

基本的な構文

substr_compare(
    string $haystack,
    string $needle,
    int $offset,
    ?int $length = null,
    bool $case_insensitive = false
): int
  • $haystack: 比較対象の文字列
  • $needle: 比較する文字列
  • $offset: 比較を開始する位置
  • $length: 比較する長さ(省略可能)
  • $case_insensitive: trueで大文字小文字を区別しない
  • 戻り値:
    • 0: 一致
    • < 0: haystackの部分がneedleより小さい
    • > 0: haystackの部分がneedleより大きい

基本的な使用例

シンプルな比較

$text = "Hello World";

// 位置0から"Hello"と比較
$result = substr_compare($text, "Hello", 0);
echo $result . "\n";  // 0(一致)

// 位置6から"World"と比較
$result = substr_compare($text, "World", 6);
echo $result . "\n";  // 0(一致)

// 位置0から"World"と比較
$result = substr_compare($text, "World", 0);
echo $result . "\n";  // -1(不一致、H < W)

長さを指定した比較

$text = "Hello World";

// 位置0から5文字を"Hello"と比較
$result = substr_compare($text, "Hello", 0, 5);
echo $result . "\n";  // 0(一致)

// 位置0から3文字を"Hel"と比較
$result = substr_compare($text, "Hel", 0, 3);
echo $result . "\n";  // 0(一致)

// 位置6から3文字を"Wor"と比較
$result = substr_compare($text, "Wor", 6, 3);
echo $result . "\n";  // 0(一致)

大文字小文字を区別しない比較

$text = "Hello World";

// 大文字小文字を区別する(デフォルト)
$result = substr_compare($text, "hello", 0, 5);
echo $result . "\n";  // 1(H > h)

// 大文字小文字を区別しない
$result = substr_compare($text, "hello", 0, 5, true);
echo $result . "\n";  // 0(一致)

// 位置6から、大文字小文字無視
$result = substr_compare($text, "WORLD", 6, 5, true);
echo $result . "\n";  // 0(一致)

負のオフセット

$text = "Hello World";

// 末尾から5文字を"World"と比較
$result = substr_compare($text, "World", -5);
echo $result . "\n";  // 0(一致)

// 末尾から5文字を"WORLD"と比較(大文字小文字無視)
$result = substr_compare($text, "WORLD", -5, 5, true);
echo $result . "\n";  // 0(一致)

実践的な使用例

例1: ファイル拡張子のチェック

class FileExtensionChecker {
    /**
     * 特定の拡張子か確認
     */
    public static function hasExtension($filename, $extension) {
        $extLength = strlen($extension);
        
        // 拡張子に.がない場合は追加
        if ($extension[0] !== '.') {
            $extension = '.' . $extension;
            $extLength++;
        }
        
        // 大文字小文字を区別せず比較
        return substr_compare($filename, $extension, -$extLength, $extLength, true) === 0;
    }
    
    /**
     * 複数の拡張子のいずれかか確認
     */
    public static function hasAnyExtension($filename, $extensions) {
        foreach ($extensions as $ext) {
            if (self::hasExtension($filename, $ext)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 画像ファイルか確認
     */
    public static function isImage($filename) {
        return self::hasAnyExtension($filename, [
            'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'
        ]);
    }
    
    /**
     * ドキュメントファイルか確認
     */
    public static function isDocument($filename) {
        return self::hasAnyExtension($filename, [
            'pdf', 'doc', 'docx', 'txt', 'rtf', 'odt'
        ]);
    }
    
    /**
     * 実行ファイルか確認
     */
    public static function isExecutable($filename) {
        return self::hasAnyExtension($filename, [
            'exe', 'bat', 'sh', 'cmd', 'com'
        ]);
    }
    
    /**
     * 拡張子を取得(大文字小文字統一)
     */
    public static function getExtension($filename) {
        $dotPos = strrpos($filename, '.');
        
        if ($dotPos === false) {
            return '';
        }
        
        return strtolower(substr($filename, $dotPos + 1));
    }
}

// 使用例
$files = [
    'photo.JPG',
    'document.PDF',
    'script.SH',
    'image.PNG',
    'README'
];

echo "=== 拡張子チェック ===\n";
foreach ($files as $file) {
    echo "\n{$file}:\n";
    echo "  .jpg: " . (FileExtensionChecker::hasExtension($file, '.jpg') ? 'Yes' : 'No') . "\n";
    echo "  .pdf: " . (FileExtensionChecker::hasExtension($file, '.pdf') ? 'Yes' : 'No') . "\n";
    echo "  画像: " . (FileExtensionChecker::isImage($file) ? 'Yes' : 'No') . "\n";
    echo "  ドキュメント: " . (FileExtensionChecker::isDocument($file) ? 'Yes' : 'No') . "\n";
    echo "  拡張子: " . FileExtensionChecker::getExtension($file) . "\n";
}

例2: URLとプロトコルの検証

class UrlValidator {
    /**
     * 特定のプロトコルで始まるか
     */
    public static function hasProtocol($url, $protocol) {
        $protocolLength = strlen($protocol);
        
        // :// を追加
        if (substr($protocol, -3) !== '://') {
            $protocol .= '://';
            $protocolLength += 3;
        }
        
        // 大文字小文字を区別せず比較
        return substr_compare($url, $protocol, 0, $protocolLength, true) === 0;
    }
    
    /**
     * HTTPSか確認
     */
    public static function isHttps($url) {
        return self::hasProtocol($url, 'https');
    }
    
    /**
     * HTTPか確認
     */
    public static function isHttp($url) {
        return self::hasProtocol($url, 'http');
    }
    
    /**
     * セキュアなプロトコルか
     */
    public static function isSecure($url) {
        $secureProtocols = ['https', 'ftps', 'sftp', 'ssh'];
        
        foreach ($secureProtocols as $protocol) {
            if (self::hasProtocol($url, $protocol)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 特定のドメインで終わるか
     */
    public static function endsWith($url, $domain) {
        $domainLength = strlen($domain);
        
        // プロトコルを除去
        $cleanUrl = preg_replace('#^[a-z]+://#i', '', $url);
        
        // パスを除去
        $slashPos = strpos($cleanUrl, '/');
        if ($slashPos !== false) {
            $cleanUrl = substr($cleanUrl, 0, $slashPos);
        }
        
        return substr_compare($cleanUrl, $domain, -$domainLength, $domainLength, true) === 0;
    }
}

// 使用例
$urls = [
    'https://example.com/path',
    'HTTP://test.org',
    'ftp://files.example.com',
    'https://secure.example.com'
];

echo "=== URLプロトコルチェック ===\n";
foreach ($urls as $url) {
    echo "\n{$url}:\n";
    echo "  HTTPS: " . (UrlValidator::isHttps($url) ? 'Yes' : 'No') . "\n";
    echo "  HTTP: " . (UrlValidator::isHttp($url) ? 'Yes' : 'No') . "\n";
    echo "  セキュア: " . (UrlValidator::isSecure($url) ? 'Yes' : 'No') . "\n";
}

echo "\n=== ドメイン終端チェック ===\n";
var_dump(UrlValidator::endsWith('https://example.com', 'example.com'));  // true
var_dump(UrlValidator::endsWith('https://test.example.com', 'example.com'));  // true

例3: プレフィックス・サフィックスの検証

class StringMatcher {
    /**
     * プレフィックス(前置詞)が一致するか
     */
    public static function startsWith($haystack, $needle, $caseInsensitive = false) {
        $needleLength = strlen($needle);
        
        return substr_compare($haystack, $needle, 0, $needleLength, $caseInsensitive) === 0;
    }
    
    /**
     * サフィックス(後置詞)が一致するか
     */
    public static function endsWith($haystack, $needle, $caseInsensitive = false) {
        $needleLength = strlen($needle);
        
        return substr_compare($haystack, $needle, -$needleLength, $needleLength, $caseInsensitive) === 0;
    }
    
    /**
     * 特定の位置に特定の文字列があるか
     */
    public static function hasAt($haystack, $needle, $offset, $caseInsensitive = false) {
        $needleLength = strlen($needle);
        
        return substr_compare($haystack, $needle, $offset, $needleLength, $caseInsensitive) === 0;
    }
    
    /**
     * いずれかのプレフィックスで始まるか
     */
    public static function startsWithAny($haystack, $needles, $caseInsensitive = false) {
        foreach ($needles as $needle) {
            if (self::startsWith($haystack, $needle, $caseInsensitive)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * いずれかのサフィックスで終わるか
     */
    public static function endsWithAny($haystack, $needles, $caseInsensitive = false) {
        foreach ($needles as $needle) {
            if (self::endsWith($haystack, $needle, $caseInsensitive)) {
                return true;
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== プレフィックスチェック ===\n";
var_dump(StringMatcher::startsWith("Hello World", "Hello"));       // true
var_dump(StringMatcher::startsWith("Hello World", "HELLO", true)); // true
var_dump(StringMatcher::startsWith("Hello World", "World"));       // false

echo "\n=== サフィックスチェック ===\n";
var_dump(StringMatcher::endsWith("Hello World", "World"));        // true
var_dump(StringMatcher::endsWith("Hello World", "WORLD", true));  // true
var_dump(StringMatcher::endsWith("Hello World", "Hello"));        // false

echo "\n=== 複数パターンチェック ===\n";
$text = "test_file.php";
var_dump(StringMatcher::startsWithAny($text, ['test_', 'prod_', 'dev_']));  // true
var_dump(StringMatcher::endsWithAny($text, ['.php', '.js', '.css']));       // true

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

class VersionComparator {
    /**
     * メジャーバージョンが一致するか
     */
    public static function sameMajor($version1, $version2) {
        $dotPos1 = strpos($version1, '.');
        $dotPos2 = strpos($version2, '.');
        
        if ($dotPos1 === false || $dotPos2 === false) {
            return $version1 === $version2;
        }
        
        $length = min($dotPos1, $dotPos2);
        
        return substr_compare($version1, $version2, 0, $length) === 0;
    }
    
    /**
     * バージョンプレフィックスが一致するか
     */
    public static function hasPrefix($version, $prefix) {
        $prefixLength = strlen($prefix);
        
        return substr_compare($version, $prefix, 0, $prefixLength) === 0;
    }
    
    /**
     * ビルド情報を除いたバージョンを比較
     */
    public static function compareWithoutBuild($version1, $version2) {
        // ビルド情報(+以降)を除去して比較
        $plusPos1 = strpos($version1, '+');
        $plusPos2 = strpos($version2, '+');
        
        $v1 = $plusPos1 !== false ? substr($version1, 0, $plusPos1) : $version1;
        $v2 = $plusPos2 !== false ? substr($version2, 0, $plusPos2) : $version2;
        
        return strcmp($v1, $v2);
    }
    
    /**
     * プレリリース版か確認
     */
    public static function isPreRelease($version) {
        $preReleaseTags = ['-alpha', '-beta', '-rc', '-dev'];
        
        foreach ($preReleaseTags as $tag) {
            $tagLength = strlen($tag);
            $versionLength = strlen($version);
            
            for ($i = 0; $i <= $versionLength - $tagLength; $i++) {
                if (substr_compare($version, $tag, $i, $tagLength, true) === 0) {
                    return true;
                }
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== バージョン比較 ===\n";
var_dump(VersionComparator::sameMajor('1.2.3', '1.5.8'));  // true
var_dump(VersionComparator::sameMajor('1.2.3', '2.0.0'));  // false

echo "\n=== バージョンプレフィックス ===\n";
var_dump(VersionComparator::hasPrefix('1.2.3', '1.2'));    // true
var_dump(VersionComparator::hasPrefix('2.0.0', '1.'));     // false

echo "\n=== プレリリース判定 ===\n";
$versions = ['1.0.0', '2.0.0-alpha', '3.0.0-beta.1', '4.0.0-rc.2'];
foreach ($versions as $version) {
    $isPreRelease = VersionComparator::isPreRelease($version) ? 'Yes' : 'No';
    echo "{$version}: {$isPreRelease}\n";
}

例5: パス操作

class PathMatcher {
    /**
     * 特定のディレクトリ内か確認
     */
    public static function isInDirectory($path, $directory) {
        // パス区切り文字を統一
        $path = str_replace('\\', '/', $path);
        $directory = str_replace('\\', '/', $directory);
        
        // 末尾のスラッシュを追加
        if (substr($directory, -1) !== '/') {
            $directory .= '/';
        }
        
        $dirLength = strlen($directory);
        
        return substr_compare($path, $directory, 0, $dirLength, true) === 0;
    }
    
    /**
     * ルートディレクトリが一致するか
     */
    public static function sameRoot($path1, $path2) {
        $path1 = str_replace('\\', '/', $path1);
        $path2 = str_replace('\\', '/', $path2);
        
        $slash1 = strpos($path1, '/');
        $slash2 = strpos($path2, '/');
        
        if ($slash1 === false || $slash2 === false) {
            return false;
        }
        
        $length = min($slash1, $slash2);
        
        return substr_compare($path1, $path2, 0, $length, true) === 0;
    }
    
    /**
     * 隠しファイルか確認
     */
    public static function isHidden($filename) {
        $basename = basename($filename);
        
        return substr_compare($basename, '.', 0, 1) === 0;
    }
    
    /**
     * バックアップファイルか確認
     */
    public static function isBackup($filename) {
        $backupSuffixes = ['.bak', '.backup', '~'];
        
        foreach ($backupSuffixes as $suffix) {
            $suffixLength = strlen($suffix);
            
            if (substr_compare($filename, $suffix, -$suffixLength, $suffixLength, true) === 0) {
                return true;
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== ディレクトリチェック ===\n";
var_dump(PathMatcher::isInDirectory('/var/www/html/index.php', '/var/www'));  // true
var_dump(PathMatcher::isInDirectory('/var/www/html/index.php', '/var/log')); // false

echo "\n=== ルート比較 ===\n";
var_dump(PathMatcher::sameRoot('/var/www/html', '/var/log/app'));  // true
var_dump(PathMatcher::sameRoot('/var/www/html', '/usr/local'));    // false

echo "\n=== 隠しファイル ===\n";
var_dump(PathMatcher::isHidden('.htaccess'));   // true
var_dump(PathMatcher::isHidden('index.php'));   // false

echo "\n=== バックアップファイル ===\n";
var_dump(PathMatcher::isBackup('file.bak'));    // true
var_dump(PathMatcher::isBackup('file.php~'));   // true
var_dump(PathMatcher::isBackup('file.txt'));    // false

例6: データ検証

class DataValidator {
    /**
     * 特定のプレフィックスを持つIDか
     */
    public static function hasValidIdPrefix($id, $prefix) {
        $prefixLength = strlen($prefix);
        
        return substr_compare($id, $prefix, 0, $prefixLength) === 0;
    }
    
    /**
     * ユーザーIDが有効か
     */
    public static function isValidUserId($id) {
        return self::hasValidIdPrefix($id, 'USER-');
    }
    
    /**
     * 注文IDが有効か
     */
    public static function isValidOrderId($id) {
        return self::hasValidIdPrefix($id, 'ORD-');
    }
    
    /**
     * 電話番号が特定の地域コードか
     */
    public static function hasAreaCode($phone, $areaCode) {
        // 数字のみ抽出
        $clean = preg_replace('/[^0-9]/', '', $phone);
        $codeLength = strlen($areaCode);
        
        return substr_compare($clean, $areaCode, 0, $codeLength) === 0;
    }
    
    /**
     * 郵便番号が特定の地域か
     */
    public static function hasZipPrefix($zip, $prefix) {
        $clean = preg_replace('/[^0-9]/', '', $zip);
        $prefixLength = strlen($prefix);
        
        return substr_compare($clean, $prefix, 0, $prefixLength) === 0;
    }
}

// 使用例
echo "=== ID検証 ===\n";
var_dump(DataValidator::isValidUserId('USER-12345'));   // true
var_dump(DataValidator::isValidUserId('ORD-67890'));    // false
var_dump(DataValidator::isValidOrderId('ORD-67890'));   // true

echo "\n=== 電話番号検証 ===\n";
var_dump(DataValidator::hasAreaCode('090-1234-5678', '090'));  // true
var_dump(DataValidator::hasAreaCode('080-1234-5678', '090'));  // false

echo "\n=== 郵便番号検証 ===\n";
var_dump(DataValidator::hasZipPrefix('123-4567', '123'));  // true
var_dump(DataValidator::hasZipPrefix('456-7890', '123'));  // false

例7: テキスト処理

class TextProcessor {
    /**
     * 引用符で始まるか
     */
    public static function isQuoted($text) {
        $quotes = ['"', "'", '`'];
        
        foreach ($quotes as $quote) {
            if (substr_compare($text, $quote, 0, 1) === 0 &&
                substr_compare($text, $quote, -1, 1) === 0) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * コメント行か確認
     */
    public static function isComment($line, $commentSymbol = '//') {
        $line = ltrim($line);
        $symbolLength = strlen($commentSymbol);
        
        return substr_compare($line, $commentSymbol, 0, $symbolLength) === 0;
    }
    
    /**
     * HTMLタグか確認
     */
    public static function isHtmlTag($text) {
        $trimmed = trim($text);
        
        return substr_compare($trimmed, '<', 0, 1) === 0 &&
               substr_compare($trimmed, '>', -1, 1) === 0;
    }
    
    /**
     * マークダウンヘッダーか確認
     */
    public static function isMarkdownHeader($line) {
        $trimmed = ltrim($line);
        $headerLevels = ['# ', '## ', '### ', '#### ', '##### ', '###### '];
        
        foreach ($headerLevels as $header) {
            $headerLength = strlen($header);
            
            if (substr_compare($trimmed, $header, 0, $headerLength) === 0) {
                return true;
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== 引用符チェック ===\n";
var_dump(TextProcessor::isQuoted('"Hello"'));    // true
var_dump(TextProcessor::isQuoted("'World'"));    // true
var_dump(TextProcessor::isQuoted('Hello'));      // false

echo "\n=== コメント行チェック ===\n";
var_dump(TextProcessor::isComment('// This is a comment'));  // true
var_dump(TextProcessor::isComment('/* Comment */', '/*'));   // true
var_dump(TextProcessor::isComment('Code here'));             // false

echo "\n=== HTMLタグチェック ===\n";
var_dump(TextProcessor::isHtmlTag('<div>'));      // true
var_dump(TextProcessor::isHtmlTag('<p>text</p>')); // false(テキスト含む)

echo "\n=== マークダウンヘッダー ===\n";
$lines = ['# Title', '## Subtitle', '### Section', 'Normal text'];
foreach ($lines as $line) {
    $isHeader = TextProcessor::isMarkdownHeader($line) ? 'Yes' : 'No';
    echo "{$line}: {$isHeader}\n";
}

substr() + strcmp()との比較

$text = "Hello World";
$needle = "Hello";

// substr_compare()を使用
$result1 = substr_compare($text, $needle, 0, strlen($needle));

// substr() + strcmp()を使用
$result2 = strcmp(substr($text, 0, strlen($needle)), $needle);

// 結果は同じ
var_dump($result1 === $result2);  // true

// しかしsubstr_compare()の方が効率的
// - 一度の関数呼び出しで済む
// - 内部で最適化されている
// - コードが簡潔

パフォーマンステスト

$text = str_repeat("abcdefghijklmnopqrstuvwxyz", 10000);
$needle = "abcdefg";

// substr_compare()
$start = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    substr_compare($text, $needle, 0, 7);
}
$time1 = microtime(true) - $start;

// substr() + strcmp()
$start = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    strcmp(substr($text, 0, 7), $needle);
}
$time2 = microtime(true) - $start;

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

// substr_compare()の方が高速

まとめ

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

できること:

  • 部分文字列の効率的な比較
  • 大文字小文字を区別しない比較
  • 負のオフセットに対応

戻り値:

  • 0: 一致
  • < 0: 比較元が小さい
  • > 0: 比較元が大きい

推奨される使用場面:

  • ファイル拡張子のチェック
  • URLプロトコルの検証
  • プレフィックス・サフィックスの確認
  • バージョン番号の比較
  • パスの検証
  • データフォーマットの確認

利点:

  • substr() + strcmp()より効率的
  • 一度の関数呼び出しで完結
  • 大文字小文字の区別を制御可能
  • バイナリセーフ

注意点:

  • バイト単位で処理(マルチバイト文字注意)
  • オフセットが範囲外の場合は警告
  • 長さを省略すると文字列の最後まで比較

関連関数:

  • strcmp(): 文字列全体を比較
  • strncmp(): 先頭n文字を比較
  • strcasecmp(): 大文字小文字を区別しない比較
  • substr(): 部分文字列を取得

よく使うパターン:

// プレフィックスチェック
substr_compare($str, $prefix, 0, strlen($prefix)) === 0

// サフィックスチェック(大文字小文字無視)
substr_compare($str, $suffix, -strlen($suffix), strlen($suffix), true) === 0

// 特定位置の比較
substr_compare($str, $needle, $pos, strlen($needle)) === 0

substr_compare()は、部分文字列の比較を効率的に行いたい場合に非常に便利です。特にファイル拡張子やURLプロトコルのチェックなど、文字列の先頭や末尾を頻繁に確認する処理で活躍します!

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