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

PHP

こんにちは!今回は、PHPの標準関数であるstrcmp()について詳しく解説していきます。2つの文字列をバイナリセーフに比較できる、基本的かつ重要な関数です!

strcmp関数とは?

strcmp()関数は、2つの文字列をバイナリセーフに比較する関数です。

単純な=====による比較とは異なり、辞書順(アルファベット順)での大小関係も判定できます。ソート処理や文字列の順序比較に非常に便利です!

基本的な構文

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

基本的な使用例

シンプルな比較

// 同じ文字列
$result = strcmp("hello", "hello");
echo $result . "\n";  // 0(等しい)

// 異なる文字列
$result = strcmp("apple", "banana");
echo $result . "\n";  // 負の数(appleの方が小さい)

$result = strcmp("zebra", "apple");
echo $result . "\n";  // 正の数(zebraの方が大きい)

// 大文字小文字は区別される
$result = strcmp("Hello", "hello");
echo $result . "\n";  // 負の数(Hはhより小さい)

等しいかどうかの判定

$password = "mySecretPass123";
$input = "mySecretPass123";

if (strcmp($password, $input) === 0) {
    echo "パスワードが一致しました\n";
} else {
    echo "パスワードが一致しません\n";
}
// 出力: パスワードが一致しました

辞書順の比較

$str1 = "apple";
$str2 = "banana";

$result = strcmp($str1, $str2);

if ($result < 0) {
    echo "{$str1} は {$str2} より前\n";
} elseif ($result > 0) {
    echo "{$str1} は {$str2} より後\n";
} else {
    echo "{$str1} と {$str2} は同じ\n";
}
// 出力: apple は banana より前

実践的な使用例

例1: 安全なパスワード比較

class SecureAuth {
    /**
     * タイミング攻撃に強い文字列比較
     */
    public static function comparePassword($stored, $input) {
        // strcmp()はバイナリセーフで一定時間で比較
        // ただし、PHP 5.5以降はhash_equals()を推奨
        
        if (function_exists('hash_equals')) {
            return hash_equals($stored, $input);
        }
        
        return strcmp($stored, $input) === 0;
    }
    
    public static function validateCredentials($username, $password, $database) {
        if (!isset($database[$username])) {
            return [
                'success' => false,
                'message' => 'ユーザーが見つかりません'
            ];
        }
        
        $storedHash = $database[$username]['password'];
        
        if (self::comparePassword($storedHash, $password)) {
            return [
                'success' => true,
                'message' => 'ログイン成功',
                'user' => $username
            ];
        }
        
        return [
            'success' => false,
            'message' => 'パスワードが正しくありません'
        ];
    }
}

// 使用例
$database = [
    'admin' => ['password' => 'hashed_password_123'],
    'user1' => ['password' => 'hashed_password_456']
];

$result = SecureAuth::validateCredentials('admin', 'hashed_password_123', $database);
print_r($result);
// success => true

例2: ソート処理

class StringSorter {
    /**
     * strcmp()を使った配列のソート
     */
    public static function sortAscending($array) {
        usort($array, function($a, $b) {
            return strcmp($a, $b);
        });
        
        return $array;
    }
    
    public static function sortDescending($array) {
        usort($array, function($a, $b) {
            return strcmp($b, $a);  // 引数の順序を逆に
        });
        
        return $array;
    }
    
    public static function sortByField($array, $field) {
        usort($array, function($a, $b) use ($field) {
            return strcmp($a[$field], $b[$field]);
        });
        
        return $array;
    }
    
    public static function sortMultipleFields($array, $fields) {
        usort($array, function($a, $b) use ($fields) {
            foreach ($fields as $field) {
                $result = strcmp($a[$field], $b[$field]);
                
                if ($result !== 0) {
                    return $result;
                }
            }
            
            return 0;
        });
        
        return $array;
    }
}

// 使用例
$names = ['Charlie', 'Alice', 'Bob', 'David'];

$sorted = StringSorter::sortAscending($names);
print_r($sorted);
// Array ( [0] => Alice [1] => Bob [2] => Charlie [3] => David )

$sorted = StringSorter::sortDescending($names);
print_r($sorted);
// Array ( [0] => David [1] => Charlie [2] => Bob [3] => Alice )

$users = [
    ['name' => 'Charlie', 'age' => 30],
    ['name' => 'Alice', 'age' => 25],
    ['name' => 'Bob', 'age' => 35]
];

$sorted = StringSorter::sortByField($users, 'name');
print_r($sorted);
// Alice, Bob, Charlie の順

$data = [
    ['city' => 'Tokyo', 'name' => 'Bob'],
    ['city' => 'Tokyo', 'name' => 'Alice'],
    ['city' => 'Osaka', 'name' => 'Charlie']
];

$sorted = StringSorter::sortMultipleFields($data, ['city', 'name']);
print_r($sorted);
// Osaka-Charlie, Tokyo-Alice, Tokyo-Bob の順

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

class VersionComparator {
    /**
     * バージョン番号を比較
     */
    public static function compare($version1, $version2) {
        // セマンティックバージョニング(簡易版)
        $v1Parts = explode('.', $version1);
        $v2Parts = explode('.', $version2);
        
        $maxLength = max(count($v1Parts), count($v2Parts));
        
        for ($i = 0; $i < $maxLength; $i++) {
            $v1 = isset($v1Parts[$i]) ? $v1Parts[$i] : '0';
            $v2 = isset($v2Parts[$i]) ? $v2Parts[$i] : '0';
            
            // 数値として比較
            if (is_numeric($v1) && is_numeric($v2)) {
                $diff = (int)$v1 - (int)$v2;
                
                if ($diff !== 0) {
                    return $diff;
                }
            } else {
                // 文字列として比較
                $result = strcmp($v1, $v2);
                
                if ($result !== 0) {
                    return $result;
                }
            }
        }
        
        return 0;
    }
    
    public static function isNewer($version1, $version2) {
        return self::compare($version1, $version2) > 0;
    }
    
    public static function isOlder($version1, $version2) {
        return self::compare($version1, $version2) < 0;
    }
    
    public static function isSame($version1, $version2) {
        return self::compare($version1, $version2) === 0;
    }
    
    public static function sortVersions($versions) {
        usort($versions, [self::class, 'compare']);
        return $versions;
    }
}

// 使用例
echo VersionComparator::compare('1.0.0', '1.0.1') . "\n";  // 負の数
echo VersionComparator::compare('2.1.0', '2.0.5') . "\n";  // 正の数
echo VersionComparator::compare('1.5.0', '1.5.0') . "\n";  // 0

var_dump(VersionComparator::isNewer('2.0.0', '1.9.9'));  // true
var_dump(VersionComparator::isOlder('1.5.0', '2.0.0'));  // true

$versions = ['2.1.0', '1.0.0', '1.5.3', '2.0.0', '1.5.10'];
$sorted = VersionComparator::sortVersions($versions);
print_r($sorted);
// 1.0.0, 1.5.3, 1.5.10, 2.0.0, 2.1.0

例4: ファイル名の比較と検索

class FileManager {
    private $files = [];
    
    public function addFile($filename) {
        $this->files[] = $filename;
    }
    
    public function findFile($filename) {
        foreach ($this->files as $index => $file) {
            if (strcmp($file, $filename) === 0) {
                return [
                    'found' => true,
                    'index' => $index,
                    'filename' => $file
                ];
            }
        }
        
        return ['found' => false];
    }
    
    public function hasFile($filename) {
        return $this->findFile($filename)['found'];
    }
    
    public function sortFiles() {
        usort($this->files, function($a, $b) {
            return strcmp($a, $b);
        });
    }
    
    public function getFilesBetween($start, $end) {
        $result = [];
        
        foreach ($this->files as $file) {
            if (strcmp($file, $start) >= 0 && strcmp($file, $end) <= 0) {
                $result[] = $file;
            }
        }
        
        return $result;
    }
    
    public function findSimilar($filename, $maxDistance = 2) {
        $similar = [];
        
        foreach ($this->files as $file) {
            $distance = levenshtein($filename, $file);
            
            if ($distance <= $maxDistance && strcmp($file, $filename) !== 0) {
                $similar[] = [
                    'filename' => $file,
                    'distance' => $distance
                ];
            }
        }
        
        usort($similar, function($a, $b) {
            return $a['distance'] - $b['distance'];
        });
        
        return $similar;
    }
}

// 使用例
$manager = new FileManager();
$manager->addFile('document.txt');
$manager->addFile('image.jpg');
$manager->addFile('report.pdf');
$manager->addFile('data.csv');

var_dump($manager->hasFile('document.txt'));  // true
var_dump($manager->hasFile('missing.txt'));   // false

$manager->sortFiles();
$files = $manager->getFilesBetween('d', 'i');
print_r($files);
// data.csv, document.txt

$similar = $manager->findSimilar('document.txt', 3);
print_r($similar);

例5: データの検証と正規化

class DataValidator {
    private $validValues = [];
    
    public function __construct($validValues) {
        $this->validValues = $validValues;
    }
    
    public function isValid($value) {
        foreach ($this->validValues as $valid) {
            if (strcmp($value, $valid) === 0) {
                return true;
            }
        }
        
        return false;
    }
    
    public function validate($value) {
        if ($this->isValid($value)) {
            return [
                'valid' => true,
                'value' => $value,
                'normalized' => $value
            ];
        }
        
        // 候補を探す
        $suggestions = $this->findSuggestions($value);
        
        return [
            'valid' => false,
            'value' => $value,
            'suggestions' => $suggestions
        ];
    }
    
    private function findSuggestions($value, $maxSuggestions = 3) {
        $suggestions = [];
        
        foreach ($this->validValues as $valid) {
            $distance = levenshtein(strtolower($value), strtolower($valid));
            
            if ($distance <= 3) {
                $suggestions[] = [
                    'value' => $valid,
                    'distance' => $distance
                ];
            }
        }
        
        usort($suggestions, function($a, $b) {
            return $a['distance'] - $b['distance'];
        });
        
        return array_slice(array_column($suggestions, 'value'), 0, $maxSuggestions);
    }
}

class CountryValidator extends DataValidator {
    public function __construct() {
        $countries = ['Japan', 'USA', 'UK', 'France', 'Germany', 'China'];
        parent::__construct($countries);
    }
}

// 使用例
$validator = new CountryValidator();

$result = $validator->validate('Japan');
print_r($result);
// valid => true

$result = $validator->validate('Japn');  // タイポ
print_r($result);
// valid => false, suggestions => ['Japan']

$result = $validator->validate('United States');
print_r($result);
// valid => false, suggestions => ['USA']

例6: データベースのカラム比較

class DatabaseComparator {
    public static function compareSchemas($schema1, $schema2) {
        $differences = [
            'added' => [],
            'removed' => [],
            'unchanged' => []
        ];
        
        // schema1にあってschema2にない列
        foreach ($schema1 as $column1) {
            $found = false;
            
            foreach ($schema2 as $column2) {
                if (strcmp($column1, $column2) === 0) {
                    $found = true;
                    $differences['unchanged'][] = $column1;
                    break;
                }
            }
            
            if (!$found) {
                $differences['removed'][] = $column1;
            }
        }
        
        // schema2にあってschema1にない列
        foreach ($schema2 as $column2) {
            $found = false;
            
            foreach ($schema1 as $column1) {
                if (strcmp($column1, $column2) === 0) {
                    $found = true;
                    break;
                }
            }
            
            if (!$found) {
                $differences['added'][] = $column2;
            }
        }
        
        return $differences;
    }
    
    public static function generateMigration($oldSchema, $newSchema) {
        $diff = self::compareSchemas($oldSchema, $newSchema);
        $sql = [];
        
        foreach ($diff['added'] as $column) {
            $sql[] = "ALTER TABLE example ADD COLUMN {$column} VARCHAR(255);";
        }
        
        foreach ($diff['removed'] as $column) {
            $sql[] = "ALTER TABLE example DROP COLUMN {$column};";
        }
        
        return $sql;
    }
}

// 使用例
$oldSchema = ['id', 'name', 'email', 'created_at'];
$newSchema = ['id', 'username', 'email', 'phone', 'created_at'];

$diff = DatabaseComparator::compareSchemas($oldSchema, $newSchema);
print_r($diff);
// added => ['username', 'phone']
// removed => ['name']
// unchanged => ['id', 'email', 'created_at']

$migration = DatabaseComparator::generateMigration($oldSchema, $newSchema);
print_r($migration);

例7: 設定値の比較

class ConfigComparator {
    public static function compareConfigs($config1, $config2) {
        $changes = [];
        
        // すべてのキーを取得
        $allKeys = array_unique(array_merge(
            array_keys($config1),
            array_keys($config2)
        ));
        
        foreach ($allKeys as $key) {
            $value1 = isset($config1[$key]) ? $config1[$key] : null;
            $value2 = isset($config2[$key]) ? $config2[$key] : null;
            
            if ($value1 === null) {
                $changes[$key] = [
                    'status' => 'added',
                    'old' => null,
                    'new' => $value2
                ];
            } elseif ($value2 === null) {
                $changes[$key] = [
                    'status' => 'removed',
                    'old' => $value1,
                    'new' => null
                ];
            } elseif (strcmp((string)$value1, (string)$value2) !== 0) {
                $changes[$key] = [
                    'status' => 'modified',
                    'old' => $value1,
                    'new' => $value2
                ];
            }
        }
        
        return $changes;
    }
    
    public static function generateDiff($config1, $config2) {
        $changes = self::compareConfigs($config1, $config2);
        $diff = [];
        
        foreach ($changes as $key => $change) {
            switch ($change['status']) {
                case 'added':
                    $diff[] = "+ {$key}: {$change['new']}";
                    break;
                case 'removed':
                    $diff[] = "- {$key}: {$change['old']}";
                    break;
                case 'modified':
                    $diff[] = "~ {$key}: {$change['old']} → {$change['new']}";
                    break;
            }
        }
        
        return $diff;
    }
}

// 使用例
$config1 = [
    'database_host' => 'localhost',
    'database_port' => '3306',
    'cache_enabled' => 'true',
    'debug_mode' => 'false'
];

$config2 = [
    'database_host' => 'db.example.com',
    'database_port' => '3306',
    'cache_enabled' => 'true',
    'log_level' => 'info'
];

$changes = ConfigComparator::compareConfigs($config1, $config2);
print_r($changes);

$diff = ConfigComparator::generateDiff($config1, $config2);
foreach ($diff as $line) {
    echo $line . "\n";
}
// ~ database_host: localhost → db.example.com
// - debug_mode: false
// + log_level: info

==や===との違い

$str1 = "10";
$str2 = "2";

// ==: 型変換後に比較
var_dump($str1 == $str2);   // false

// ===: 型と値の両方を比較
var_dump($str1 === $str2);  // false

// strcmp(): 辞書順で比較
echo strcmp($str1, $str2) . "\n";  // 負の数("10"は"2"より前)

// 数値としての比較
var_dump(10 > 2);  // true

// 文字列としての辞書順比較では"10"は"2"より小さい
// ("1"が"2"より小さいため)

strcasecmp()との違い

$str1 = "Hello";
$str2 = "hello";

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

// strcasecmp(): 大文字小文字を無視
echo strcasecmp($str1, $str2) . "\n";  // 0(等しい)

strncmp()との違い

$str1 = "Hello World";
$str2 = "Hello PHP";

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

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

バイナリセーフな比較

// strcmp()はバイナリセーフ
$str1 = "hello\0world";
$str2 = "hello\0php";

// NULL文字も正しく比較される
$result = strcmp($str1, $str2);
echo "結果: {$result}\n";

// 文字列長も考慮される
var_dump(strlen($str1));  // 11
var_dump(strlen($str2));  // 9

パフォーマンスの考慮

// 大量の比較を行う場合
$iterations = 100000;

$str1 = "test string one";
$str2 = "test string two";

// strcmp()
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    strcmp($str1, $str2);
}
$time1 = microtime(true) - $start;

// ===
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    $str1 === $str2;
}
$time2 = microtime(true) - $start;

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

// ===の方が高速だが、strcmp()は辞書順比較が必要な場合に使用

エラーハンドリング

class SafeStringCompare {
    public static function compare($str1, $str2) {
        // 型チェック
        if (!is_string($str1) || !is_string($str2)) {
            throw new InvalidArgumentException('両方の引数は文字列である必要があります');
        }
        
        return strcmp($str1, $str2);
    }
    
    public static function equals($str1, $str2) {
        try {
            return self::compare($str1, $str2) === 0;
        } catch (InvalidArgumentException $e) {
            return false;
        }
    }
}

// 使用例
try {
    $result = SafeStringCompare::compare("test", "test");
    echo "比較結果: {$result}\n";
} catch (InvalidArgumentException $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}

var_dump(SafeStringCompare::equals("hello", "hello"));  // true
var_dump(SafeStringCompare::equals("hello", 123));      // false(エラーをキャッチ)

まとめ

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

できること:

  • 2つの文字列の比較
  • 辞書順での大小判定
  • バイナリセーフな比較
  • ソート処理での使用

戻り値:

  • 0: 等しい
  • < 0: 第1引数が小さい
  • > 0: 第1引数が大きい

推奨される使用場面:

  • パスワードの安全な比較
  • 文字列のソート処理
  • バージョン番号の比較
  • ファイル名の検索と比較
  • データベーススキーマの比較

関連関数:

  • strcasecmp(): 大文字小文字を区別しない比較
  • strncmp(): 最初のn文字のみ比較
  • strnatcmp(): 自然順ソート
  • hash_equals(): タイミング攻撃に強い比較(PHP 5.5+)

注意点:

  • 大文字小文字を区別する
  • マルチバイト文字列にはmb_strcmp()を検討
  • 単純な等価判定には===の方が高速
  • タイミング攻撃対策が必要な場合はhash_equals()を使用

strcmp()は文字列比較の基本的な関数であり、特にソート処理や辞書順比較が必要な場面で威力を発揮します。適切に使いこなすことで、効率的で正確な文字列処理が可能になります!

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