できない.dev

範囲を指定して乱数を得るには

最小値と最大値を指定して整数の乱数を得る方法を各言語で示す。
上限を含む関数と含まない関数が言語ごとに混在する点と、暗号用途に使える乱数の関数への切り替えまでを扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import random
import secrets
 
low, high = 1, 6
print(random.randint(low, high))               # 例: 4(1〜6 のどれか。上限を含む)
print(random.randrange(low, high + 1))         # 例: 2(randrange は上限を含まないので +1 する)
print(secrets.randbelow(high - low + 1) + low) # 例: 6(暗号用途ではこちらを使う)

random.randint(a, b) は両端を含む整数を返す。
randrange は range と同じく上限を含まないので、同じ範囲にするには上限に 1 を足す。
random モジュールの乱数は予測できる擬似乱数なので、トークンやパスワードに使う値は secrets で作る。

JavaScript

const { randomInt } = require("node:crypto");
 
function randomBetween(min, max) {
  // min 以上 max 以下の整数を返す(両端を含む)
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
 
console.log(randomBetween(1, 6)); // 例: 4(実行のたびに変わる)
 
// 暗号用途は crypto の randomInt を使う。こちらは上限を含まない
console.log(randomInt(1, 7));     // 例: 2(1〜6 のどれか)

Math.random() は 0 以上 1 未満の小数を返すので、幅(max - min + 1)を掛けて切り捨て、min を足すと両端を含む整数になる。
幅の + 1 を忘れると max が一度も出ない。
トークンなどには crypto.randomInt を使う。
こちらは上限を含まない。

できないときReact のハイドレーションエラー(Hydration mismatch)の原因と直し方

TypeScript

function randomBetween(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
 
// 暗号用途向け。ブラウザでも Node.js でも使える
function secureRandomBetween(min: number, max: number): number {
  const range = max - min + 1;
  const limit = Math.floor(0x100000000 / range) * range; // これ以上の値は偏るので引き直す
  const buf = new Uint32Array(1);
  do {
    crypto.getRandomValues(buf);
  } while (buf[0] >= limit);
  return min + (buf[0] % range);
}
 
console.log(randomBetween(1, 6));       // 例: 4(実行のたびに変わる)
console.log(secureRandomBetween(1, 6)); // 例: 3

引数と戻り値に型を付けた関数にしておくと、min と max の取り違えに気づきやすい。
crypto.getRandomValues を使う版は OS の乱数を使うので、ブラウザでも Node.js でも暗号用途に使える。
32 ビットの値を剰余で割るだけだと小さい値が出やすくなるため、割り切れる範囲を超えた値は引き直している。

できないときReact のハイドレーションエラー(Hydration mismatch)の原因と直し方

Go

package main
 
import (
	"crypto/rand"
	"fmt"
	"math/big"
	mrand "math/rand/v2"
)
 
func main() {
	low, high := 1, 6
	fmt.Println(mrand.IntN(high-low+1) + low) // 例: 4(IntN(n) は 0 以上 n 未満)
 
	// 暗号用途は crypto/rand を使う
	n, err := rand.Int(rand.Reader, big.NewInt(int64(high-low+1)))
	if err != nil {
		panic(err)
	}
	fmt.Println(int(n.Int64()) + low) // 例: 2
}

Go 1.22 以降の math/rand/v2 は、種を設定しなくても実行のたびに違う値が出る。
IntN(n) は 0 以上 n 未満の整数を返すので、範囲の幅を渡して下限を足す。
暗号用途には crypto/rand の rand.Int を使い、上限は math/big の整数で渡す。

Rust

// Cargo.toml: rand = "0.10"
use rand::RngExt;
 
fn main() {
    let mut rng = rand::rng();
    println!("{}", rng.random_range(1..=6)); // 例: 4(..= で上限を含める)
    println!("{}", rng.random_range(1..7));  // 例: 2(.. は上限を含まない)
}

標準ライブラリに乱数は無いため、rand クレートを Cargo.toml に追加して使う。
0.10 系では random_range を呼ぶのに RngExt トレイトの use が要り、忘れるとメソッドが見つからないというコンパイルエラー(E0599)になる。
範囲は 1..=6 なら上限を含み、1..7 なら含まない。

Java

import java.security.SecureRandom;
import java.util.concurrent.ThreadLocalRandom;
 
public class Main {
    public static void main(String[] args) {
        int min = 1, max = 6;
        System.out.println(ThreadLocalRandom.current().nextInt(min, max + 1)); // 例: 4(上限は含まれないので +1 する)
        System.out.println(new SecureRandom().nextInt(min, max + 1));          // 例: 2(暗号用途はこちら)
    }
}

ThreadLocalRandom.current().nextInt(min, max) は上限を含まないので、max + 1 を渡す。
暗号用途には SecureRandom を使う。
Java 17 以降は SecureRandom でも同じ nextInt(min, max) の形で範囲を指定できる。

C#

using System;
using System.Security.Cryptography;
 
var min = 1;
var max = 6;
Console.WriteLine(Random.Shared.Next(min, max + 1));               // 例: 4(Next の上限は含まれないので +1 する)
Console.WriteLine(RandomNumberGenerator.GetInt32(min, max + 1));   // 例: 2(暗号用途はこちら)

Random.Shared は .NET 6 以降で使えるスレッドセーフな共有インスタンスで、Next(min, max) は上限を含まないので max + 1 を渡す。
暗号用途には RandomNumberGenerator.GetInt32 を使う。

上限を含むかは言語で違う

1〜6 の整数を得る書き方を 7 言語で並べると、上限を含む関数と含まない関数が混在していた。

  • Python は randint が含み、randrange が含まない。
  • JavaScript は自分で式を書くので幅の取り方で決まり、crypto.randomInt は含まない。
  • Go の IntN(n) は 0 以上 n 未満である。
  • Rust は範囲の書き方で決まり、1..=6 が含み、1..7 が含まない。
  • Java の nextInt(min, max) と C# の Next(min, max) は含まない。

上限を含まない関数に 6 をそのまま渡すと、6 が一度も出ない。
7 言語のコードを 60,000 回ずつ繰り返し、出る値が 1〜6 の 6 通りだけで、それぞれ 9,700〜10,200 回前後に収まることを確かめた。

暗号用途には使わない

random・Math.random()・math/rand・java.util.Random・System.Random の乱数は、各言語のドキュメントで暗号用途には向かないとされている。
ログイン用のトークン、パスワード、ワンタイムコードなど、推測されると困る値には OS の乱数に基づく関数を使う。

  • Python は secrets.randbelow
  • JavaScript は crypto.randomInt
  • Go は crypto/rand
  • Java は SecureRandom
  • C# は RandomNumberGenerator.GetInt32

Rust の rand::rng() は、OS から種を得る ChaCha 系の暗号論的擬似乱数として設計されている(rand 0.10 のドキュメントによる)。

剰余で範囲に収める自作は、範囲の幅が乱数の幅を割り切れないと小さい値が出やすくなる偏りを生む。
TypeScript のコードのように引き直す処理が要るので、用意された関数を使うほうが安全である。

React の描画に直接書くと hydration エラーになる

Next.js などのサーバー描画で Math.random() の結果を JSX に直接書くと、サーバーが作った HTML とブラウザの初回描画で値が食い違い、hydration mismatch の警告が出る。
乱数は useEffect の中で作って useState に入れ、初回の描画はサーバーと同じ内容にそろえる。