[PHP]mt_rand関数完全マスター!高品質な乱数生成の決定版

PHP

はじめに

Webアプリケーション開発で、ランダムな値が必要になることは頻繁にあります。パスワード生成、抽選システム、ゲーム開発、テストデータ作成など、様々な場面で乱数が活用されています。PHPのmt_rand関数は、高品質なメルセンヌ・ツイスタアルゴリズムを使用した乱数生成関数で、多くの開発者に愛用されています。この記事では、mt_rand関数の基本から応用まで、実践的な使い方を詳しく解説していきます。

mt_rand関数とは

mt_rand関数は、メルセンヌ・ツイスタ(Mersenne Twister)アルゴリズムを使用してランダムな整数を生成するPHPの組み込み関数です。従来のrand()関数よりも統計的に優れた乱数を生成し、より大きな範囲の値を扱うことができます。

基本構文

mt_rand(): int
mt_rand(int $min, int $max): int
  • 引数なしの場合:0からmt_getrandmax()までのランダムな整数を返す
  • 引数ありの場合:$minから$max(両端含む)の範囲でランダムな整数を返す

基本的な使い方

1. 引数なしでの使用

<?php
// 0からmt_getrandmax()までのランダムな整数
$randomValue = mt_rand();
echo "ランダムな値: " . $randomValue . PHP_EOL;
echo "最大可能値: " . mt_getrandmax() . PHP_EOL;

// 例)出力結果
// ランダムな値: 1847562019
// 最大可能値: 2147483647
?>

2. 範囲を指定した使用

<?php
// 1から6までのサイコロの目をシミュレート
$dice = mt_rand(1, 6);
echo "サイコロの目: " . $dice . PHP_EOL;

// 1から100までの整数
$percentage = mt_rand(1, 100);
echo "パーセント値: " . $percentage . "%" . PHP_EOL;

// 負の数も扱える
$temperature = mt_rand(-10, 35);
echo "気温: " . $temperature . "℃" . PHP_EOL;
?>

randとmt_randの比較

従来のrand()関数とmt_rand()関数の違いを理解することは重要です。

<?php
echo "=== rand() vs mt_rand() 比較 ===" . PHP_EOL;

// 最大値の比較
echo "rand()の最大値: " . getrandmax() . PHP_EOL;
echo "mt_rand()の最大値: " . mt_getrandmax() . PHP_EOL;

// 実行速度の比較
$iterations = 100000;

$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    rand(1, 1000);
}
$randTime = microtime(true) - $start;

$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    mt_rand(1, 1000);
}
$mtRandTime = microtime(true) - $start;

echo "rand(): {$randTime}秒" . PHP_EOL;
echo "mt_rand(): {$mtRandTime}秒" . PHP_EOL;

// 品質の違いを視覚的に確認
echo "\n=== 分布の均一性テスト ===" . PHP_EOL;
$buckets = array_fill(0, 10, 0);

for ($i = 0; $i < 10000; $i++) {
    $bucket = (int)(mt_rand(0, 9));
    $buckets[$bucket]++;
}

foreach ($buckets as $index => $count) {
    echo "範囲{$index}: " . str_repeat('*', $count / 50) . " ({$count})" . PHP_EOL;
}
?>

実践的な活用例

例1: パスワード生成システム

<?php
class PasswordGenerator {
    private const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
    private const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    private const NUMBERS = '0123456789';
    private const SYMBOLS = '!@#$%^&*()_+-=[]{}|;:,.<>?';
    
    /**
     * 指定条件でパスワードを生成
     */
    public static function generate($length = 12, $options = []) {
        $charset = '';
        $required = [];
        
        $options = array_merge([
            'lowercase' => true,
            'uppercase' => true,
            'numbers' => true,
            'symbols' => false
        ], $options);
        
        if ($options['lowercase']) {
            $charset .= self::LOWERCASE;
            $required[] = self::LOWERCASE[mt_rand(0, strlen(self::LOWERCASE) - 1)];
        }
        
        if ($options['uppercase']) {
            $charset .= self::UPPERCASE;
            $required[] = self::UPPERCASE[mt_rand(0, strlen(self::UPPERCASE) - 1)];
        }
        
        if ($options['numbers']) {
            $charset .= self::NUMBERS;
            $required[] = self::NUMBERS[mt_rand(0, strlen(self::NUMBERS) - 1)];
        }
        
        if ($options['symbols']) {
            $charset .= self::SYMBOLS;
            $required[] = self::SYMBOLS[mt_rand(0, strlen(self::SYMBOLS) - 1)];
        }
        
        if (empty($charset)) {
            throw new InvalidArgumentException('少なくとも1つの文字種類を選択してください');
        }
        
        $password = '';
        $charsetLength = strlen($charset);
        
        // 必須文字を追加
        foreach ($required as $char) {
            $password .= $char;
        }
        
        // 残りの文字数をランダムに埋める
        for ($i = strlen($password); $i < $length; $i++) {
            $password .= $charset[mt_rand(0, $charsetLength - 1)];
        }
        
        // パスワードをシャッフル
        return str_shuffle($password);
    }
    
    /**
     * パスワード強度をチェック
     */
    public static function checkStrength($password) {
        $score = 0;
        $feedback = [];
        
        if (strlen($password) >= 8) $score += 1;
        else $feedback[] = "8文字以上にしてください";
        
        if (preg_match('/[a-z]/', $password)) $score += 1;
        else $feedback[] = "小文字を含めてください";
        
        if (preg_match('/[A-Z]/', $password)) $score += 1;
        else $feedback[] = "大文字を含めてください";
        
        if (preg_match('/[0-9]/', $password)) $score += 1;
        else $feedback[] = "数字を含めてください";
        
        if (preg_match('/[^a-zA-Z0-9]/', $password)) $score += 1;
        else $feedback[] = "記号を含めてください";
        
        $strength = ['非常に弱い', '弱い', '普通', '強い', '非常に強い'];
        
        return [
            'score' => $score,
            'strength' => $strength[min($score, 4)],
            'feedback' => $feedback
        ];
    }
}

// 使用例
$password = PasswordGenerator::generate(16, [
    'lowercase' => true,
    'uppercase' => true,
    'numbers' => true,
    'symbols' => true
]);
echo "生成されたパスワード: " . $password . PHP_EOL;

$strength = PasswordGenerator::checkStrength($password);
echo "強度: " . $strength['strength'] . " (スコア: " . $strength['score'] . "/5)" . PHP_EOL;
?>

例2: ガチャ・抽選システム

<?php
class GachaSystem {
    private $items = [];
    private $totalWeight = 0;
    
    public function addItem($name, $weight, $rarity = 'common') {
        $this->items[] = [
            'name' => $name,
            'weight' => $weight,
            'rarity' => $rarity
        ];
        $this->totalWeight += $weight;
    }
    
    /**
     * 単発ガチャ
     */
    public function drawSingle() {
        $random = mt_rand(1, $this->totalWeight);
        $current = 0;
        
        foreach ($this->items as $item) {
            $current += $item['weight'];
            if ($random <= $current) {
                return $item;
            }
        }
        
        // フォールバック(通常は到達しない)
        return end($this->items);
    }
    
    /**
     * 連続ガチャ
     */
    public function drawMultiple($count) {
        $results = [];
        $rarityCount = [];
        
        for ($i = 0; $i < $count; $i++) {
            $item = $this->drawSingle();
            $results[] = $item;
            
            if (!isset($rarityCount[$item['rarity']])) {
                $rarityCount[$item['rarity']] = 0;
            }
            $rarityCount[$item['rarity']]++;
        }
        
        return [
            'items' => $results,
            'summary' => $rarityCount,
            'total' => $count
        ];
    }
    
    /**
     * 天井システム付きガチャ
     */
    public function drawWithPity($maxDraws, $guaranteedRarity = 'legendary') {
        $results = [];
        $drawCount = 0;
        $guaranteedFound = false;
        
        while ($drawCount < $maxDraws && !$guaranteedFound) {
            $item = $this->drawSingle();
            $results[] = $item;
            $drawCount++;
            
            if ($item['rarity'] === $guaranteedRarity) {
                $guaranteedFound = true;
            }
        }
        
        // 天井到達時の保証
        if (!$guaranteedFound && $drawCount >= $maxDraws) {
            $legendaryItems = array_filter($this->items, function($item) use ($guaranteedRarity) {
                return $item['rarity'] === $guaranteedRarity;
            });
            
            if (!empty($legendaryItems)) {
                $randomLegendary = $legendaryItems[array_rand($legendaryItems)];
                $results[] = $randomLegendary;
                $drawCount++;
            }
        }
        
        return [
            'items' => $results,
            'draws' => $drawCount,
            'pity_activated' => !$guaranteedFound && $drawCount > $maxDraws
        ];
    }
}

// ガチャアイテムの設定
$gacha = new GachaSystem();
$gacha->addItem('鉄の剣', 500, 'common');
$gacha->addItem('鋼の剣', 300, 'common');
$gacha->addItem('銀の剣', 150, 'rare');
$gacha->addItem('金の剣', 45, 'epic');
$gacha->addItem('伝説の剣', 4, 'legendary');
$gacha->addItem('神器の剣', 1, 'mythic');

// 単発ガチャ
echo "=== 単発ガチャ ===" . PHP_EOL;
$result = $gacha->drawSingle();
echo "獲得アイテム: {$result['name']} (レア度: {$result['rarity']})" . PHP_EOL;

// 10連ガチャ
echo "\n=== 10連ガチャ ===" . PHP_EOL;
$results = $gacha->drawMultiple(10);
foreach ($results['items'] as $index => $item) {
    echo ($index + 1) . ": {$item['name']} ({$item['rarity']})" . PHP_EOL;
}
echo "\n=== サマリー ===" . PHP_EOL;
foreach ($results['summary'] as $rarity => $count) {
    echo "{$rarity}: {$count}個" . PHP_EOL;
}
?>

例3: テストデータ生成

<?php
class TestDataGenerator {
    private const FIRST_NAMES = ['太郎', '花子', '次郎', '美咲', '健太', '愛子', '博', '恵美'];
    private const LAST_NAMES = ['田中', '佐藤', '高橋', '山田', '渡辺', '伊藤', '中村', '小林'];
    private const PREFECTURES = ['東京都', '神奈川県', '大阪府', '愛知県', '埼玉県', '千葉県', '兵庫県', '北海道'];
    private const DOMAINS = ['example.com', 'test.co.jp', 'sample.org', 'demo.net'];
    
    /**
     * ランダムな日本人名を生成
     */
    public static function generateName() {
        $lastName = self::LAST_NAMES[mt_rand(0, count(self::LAST_NAMES) - 1)];
        $firstName = self::FIRST_NAMES[mt_rand(0, count(self::FIRST_NAMES) - 1)];
        return $lastName . ' ' . $firstName;
    }
    
    /**
     * ランダムな年齢を生成
     */
    public static function generateAge($min = 18, $max = 65) {
        return mt_rand($min, $max);
    }
    
    /**
     * ランダムな生年月日を生成
     */
    public static function generateBirthdate($minAge = 18, $maxAge = 65) {
        $currentYear = date('Y');
        $birthYear = $currentYear - mt_rand($minAge, $maxAge);
        $month = mt_rand(1, 12);
        $day = mt_rand(1, cal_days_in_month(CAL_GREGORIAN, $month, $birthYear));
        
        return sprintf('%04d-%02d-%02d', $birthYear, $month, $day);
    }
    
    /**
     * ランダムなメールアドレスを生成
     */
    public static function generateEmail($name = null) {
        if ($name === null) {
            $name = 'user' . mt_rand(1000, 9999);
        } else {
            $name = strtolower(str_replace(' ', '.', $name));
        }
        
        $domain = self::DOMAINS[mt_rand(0, count(self::DOMAINS) - 1)];
        return $name . '@' . $domain;
    }
    
    /**
     * ランダムな住所を生成
     */
    public static function generateAddress() {
        $prefecture = self::PREFECTURES[mt_rand(0, count(self::PREFECTURES) - 1)];
        $city = '○○市';
        $ward = '△△区';
        $block = mt_rand(1, 9) . '-' . mt_rand(1, 99) . '-' . mt_rand(1, 99);
        
        return $prefecture . $city . $ward . $block;
    }
    
    /**
     * ランダムな電話番号を生成
     */
    public static function generatePhoneNumber() {
        $area = ['03', '06', '052', '092'][mt_rand(0, 3)];
        $middle = str_pad(mt_rand(1000, 9999), 4, '0', STR_PAD_LEFT);
        $last = str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT);
        
        return $area . '-' . $middle . '-' . $last;
    }
    
    /**
     * ランダムな給与を生成
     */
    public static function generateSalary($min = 200000, $max = 1000000) {
        // 10000円単位で丸める
        $salary = mt_rand($min / 10000, $max / 10000) * 10000;
        return $salary;
    }
    
    /**
     * 完全なユーザーデータを生成
     */
    public static function generateUser() {
        $name = self::generateName();
        return [
            'id' => mt_rand(1000, 99999),
            'name' => $name,
            'email' => self::generateEmail($name),
            'age' => self::generateAge(),
            'birthdate' => self::generateBirthdate(),
            'phone' => self::generatePhoneNumber(),
            'address' => self::generateAddress(),
            'salary' => self::generateSalary(),
            'created_at' => date('Y-m-d H:i:s', mt_rand(strtotime('-1 year'), time()))
        ];
    }
    
    /**
     * 複数のユーザーデータを一括生成
     */
    public static function generateUsers($count = 10) {
        $users = [];
        for ($i = 0; $i < $count; $i++) {
            $users[] = self::generateUser();
        }
        return $users;
    }
}

// 使用例
echo "=== テストデータ生成 ===" . PHP_EOL;
$users = TestDataGenerator::generateUsers(5);

foreach ($users as $index => $user) {
    echo "\n--- ユーザー" . ($index + 1) . " ---" . PHP_EOL;
    foreach ($user as $key => $value) {
        echo $key . ": " . $value . PHP_EOL;
    }
}
?>

セキュリティと暗号学的用途

重要なセキュリティ考慮点:mt_randは暗号学的に安全ではありません。

<?php
class SecurityAwareRandom {
    /**
     * 一般的な用途向け(非セキュリティ)
     */
    public static function casualRandom($min, $max) {
        return mt_rand($min, $max);
    }
    
    /**
     * セキュリティが重要な用途向け
     */
    public static function secureRandom($min, $max) {
        try {
            return random_int($min, $max);
        } catch (Exception $e) {
            // フォールバック(推奨されない)
            return mt_rand($min, $max);
        }
    }
    
    /**
     * 用途別の推奨関数
     */
    public static function getRecommendation($purpose) {
        $recommendations = [
            'password' => 'random_bytes() または random_int()',
            'session_token' => 'random_bytes()',
            'csrf_token' => 'random_bytes()',
            'game_dice' => 'mt_rand()',
            'test_data' => 'mt_rand()',
            'lottery' => 'random_int()(公平性が重要な場合)',
            'shuffle_playlist' => 'mt_rand()'
        ];
        
        return $recommendations[$purpose] ?? 'mt_rand()(一般用途)';
    }
}

// 使用例の比較
echo "=== セキュリティレベル別の使い分け ===" . PHP_EOL;

echo "ゲームのサイコロ: " . SecurityAwareRandom::casualRandom(1, 6) . PHP_EOL;
echo "セキュアなOTP: " . SecurityAwareRandom::secureRandom(100000, 999999) . PHP_EOL;

echo "\n=== 用途別推奨関数 ===" . PHP_EOL;
$purposes = ['password', 'game_dice', 'csrf_token', 'test_data'];
foreach ($purposes as $purpose) {
    echo $purpose . ": " . SecurityAwareRandom::getRecommendation($purpose) . PHP_EOL;
}
?>

パフォーマンス最適化

<?php
class OptimizedRandom {
    private static $buffer = [];
    private static $bufferSize = 1000;
    
    /**
     * バッファリング付き乱数生成(頻繁な呼び出し用)
     */
    public static function bufferedRand($min, $max) {
        $key = $min . '_' . $max;
        
        if (!isset(self::$buffer[$key]) || empty(self::$buffer[$key])) {
            self::$buffer[$key] = [];
            for ($i = 0; $i < self::$bufferSize; $i++) {
                self::$buffer[$key][] = mt_rand($min, $max);
            }
        }
        
        return array_pop(self::$buffer[$key]);
    }
    
    /**
     * 事前計算済み乱数テーブル
     */
    public static function precomputedRand($size = 10000) {
        static $table = null;
        static $index = 0;
        
        if ($table === null) {
            $table = [];
            for ($i = 0; $i < $size; $i++) {
                $table[] = mt_rand();
            }
        }
        
        $value = $table[$index];
        $index = ($index + 1) % $size;
        
        return $value;
    }
    
    /**
     * パフォーマンステスト
     */
    public static function benchmark($iterations = 100000) {
        echo "=== パフォーマンステスト({$iterations}回) ===" . PHP_EOL;
        
        // 通常のmt_rand
        $start = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            mt_rand(1, 100);
        }
        $normalTime = microtime(true) - $start;
        
        // バッファリング版
        $start = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            self::bufferedRand(1, 100);
        }
        $bufferedTime = microtime(true) - $start;
        
        // 事前計算版
        $start = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            self::precomputedRand();
        }
        $precomputedTime = microtime(true) - $start;
        
        echo "通常のmt_rand: {$normalTime}秒" . PHP_EOL;
        echo "バッファリング版: {$bufferedTime}秒" . PHP_EOL;
        echo "事前計算版: {$precomputedTime}秒" . PHP_EOL;
        
        echo "バッファリング版の改善率: " . round(($normalTime / $bufferedTime), 2) . "倍" . PHP_EOL;
        echo "事前計算版の改善率: " . round(($normalTime / $precomputedTime), 2) . "倍" . PHP_EOL;
    }
}

// パフォーマンステスト実行
OptimizedRandom::benchmark(50000);
?>

注意点とベストプラクティス

1. シード値の管理

<?php
// 再現可能な結果が必要な場合
mt_srand(12345); // 固定シード値

echo "シード固定後の結果:" . PHP_EOL;
for ($i = 0; $i < 5; $i++) {
    echo mt_rand(1, 100) . " ";
}
echo PHP_EOL;

// シードをリセット(現在時刻ベース)
mt_srand();

echo "シードリセット後の結果:" . PHP_EOL;
for ($i = 0; $i < 5; $i++) {
    echo mt_rand(1, 100) . " ";
}
echo PHP_EOL;
?>

2. 範囲指定の注意点

<?php
// 正しい範囲指定
$correct = mt_rand(1, 10); // 1から10まで(両端含む)

// よくある間違い
$wrong = mt_rand(0, 9) + 1; // これも1から10だが、分布が微妙に異なる可能性

// 浮動小数点への変換
$float = mt_rand(0, mt_getrandmax()) / mt_getrandmax(); // 0.0-1.0
$rangeFloat = $float * (10.0 - 1.0) + 1.0; // 1.0-10.0

echo "整数: {$correct}" . PHP_EOL;
echo "浮動小数点: {$rangeFloat}" . PHP_EOL;
?>

まとめ

mt_rand関数は、PHPにおける高品質な乱数生成の標準的な選択肢です。メルセンヌ・ツイスタアルゴリズムにより、統計的に優れた乱数を効率的に生成できます。

主なポイント:

  • 従来のrand()よりも高品質で大きな範囲をサポート
  • パスワード生成、ガチャシステム、テストデータ作成など幅広い用途で活用可能
  • セキュリティが重要な場面ではrandom_int()random_bytes()を使用
  • シード値の管理により再現可能な結果も実現可能
  • パフォーマンスが重要な場合はバッファリングや事前計算も検討

乱数を使用するアプリケーションを開発する際は、用途に応じて適切な関数を選択し、mt_randを効果的に活用してください。品質の高いランダム性が、より良いユーザー体験につながります。

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