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