できない.dev

数値を指定した範囲に収めるには

数値を下限以上・上限以下に収める(clamp する)基本形を各言語で示す。
標準の clamp 関数があるかどうかと、下限と上限を逆に渡したときや NaN を渡したときの振る舞いが言語ごとに違う点までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

def clamp(value, lo, hi):
    if lo > hi:
        raise ValueError(f"lo ({lo}) が hi ({hi}) より大きい")
    return max(lo, min(value, hi))
 
 
print(clamp(150, 0, 100))      # 100
print(clamp(-5, 0, 100))       # 0
print(clamp(42, 0, 100))       # 42
print(clamp(0.25, 0.5, 1.0))   # 0.5
 
try:
    clamp(5, 10, 0)
except ValueError as e:
    print(e)                   # lo (10) が hi (0) より大きい
 
# 下限と上限を逆に渡しても、max と min の組み合わせだけではエラーにならない
print(max(10, min(5, 0)))      # 10
 
# NaN との比較は常に False なので、組み合わせる順番で結果が変わる
nan = float("nan")
print(clamp(nan, 0, 100))      # 0
print(min(max(nan, 0), 100))   # nan

組み込み関数に clamp は無いので、min で上限に抑えてから max で下限に持ち上げる。
max と min の組み合わせだけでは下限と上限を逆に渡してもエラーにならず下限が返るため、関数にまとめて先に大小を検査しておく。

JavaScript 実行確認済み

function clamp(value, min, max) {
  if (min > max) {
    throw new RangeError(`min (${min}) が max (${max}) より大きい`);
  }
  return Math.min(Math.max(value, min), max);
}
 
console.log(clamp(150, 0, 100));    // 100
console.log(clamp(-5, 0, 100));     // 0
console.log(clamp(0.25, 0.5, 1));   // 0.5
console.log(clamp(NaN, 0, 100));    // NaN(Math.min と Math.max は NaN をそのまま返す)
 
// 文字列は数値に変換されてから比べられる
console.log(clamp("150", 0, 100));  // 100
console.log(clamp("abc", 0, 100));  // NaN
 
try {
  clamp(5, 10, 0);
} catch (e) {
  console.log(e.message);           // min (10) が max (0) より大きい
}
 
console.log(typeof Math.clamp);     // undefined(Node.js 22 の Math には clamp が無い)

Node.js 22 の Math には clamp が無いので、Math.max で下限に持ち上げてから Math.min で上限に抑える。
Math.max と Math.min は引数を数値に変換してから比べるため、"150" は 100 になり、数値にならない "abc" は NaN のまま返る。

TypeScript 実行確認済み

function clamp(value: number, min: number, max: number): number {
  if (min > max) {
    throw new RangeError(`min (${min}) が max (${max}) より大きい`);
  }
  return Math.min(Math.max(value, min), max);
}
 
console.log(clamp(150, 0, 100)); // 100
console.log(clamp(0.25, 0.5, 1)); // 0.5
 
// 入力欄から受け取った値は string なので、そのままでは渡せない
const input: string = "150";
// clamp(input, 0, 100); // TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
const n: number = Number(input);
console.log(Number.isNaN(n) ? "数値ではない" : clamp(n, 0, 100)); // 100
 
try {
  clamp(5, 10, 0);
} catch (e) {
  console.log((e as Error).message); // min (10) が max (0) より大きい
}

引数と戻り値を number に絞った clamp を用意すると、入力欄から受け取った string をそのまま渡したときに TS2345 でコンパイルが止まる。
Number で変換した結果は NaN になりうるので、Number.isNaN で確かめてから渡す。

うまくいかない時: TypeScript で「Argument of type ... is not assignable」が解消できない

Go 実行確認済み

package main
 
import (
	"cmp"
	"fmt"
	"math"
)
 
// clamp は value を lo 以上 hi 以下に収める(組み込みの min と max は Go 1.21 以降)
func clamp[T cmp.Ordered](value, lo, hi T) T {
	if lo > hi {
		panic(fmt.Sprintf("clamp: lo (%v) が hi (%v) より大きい", lo, hi))
	}
	return max(lo, min(value, hi))
}
 
func main() {
	fmt.Println(clamp(150, 0, 100))    // 100
	fmt.Println(clamp(-5, 0, 100))     // 0
	fmt.Println(clamp(0.25, 0.5, 1.0)) // 0.5
	fmt.Println(clamp("x", "a", "m"))  // m(文字列も辞書順で収まる)
 
	// 組み込みの min と max は、引数に NaN があると NaN を返す
	fmt.Println(clamp(math.NaN(), 0, 100)) // NaN
}

Go 1.21 で入った組み込みの min と max を組み合わせ、cmp.Ordered の型パラメータで整数・浮動小数点数・文字列のどれにも使えるようにする。
go.mod の go ディレクティブが 1.20 以下だと built-in max requires go1.21 or later でコンパイルが止まるので、go 1.21 以上にしておく。

Rust 実行確認済み

fn main() {
    println!("{}", 150.clamp(0, 100)); // 100
    println!("{}", (-5).clamp(0, 100)); // 0
    println!("{}", 0.25_f64.clamp(0.5, 1.0)); // 0.5
    println!("{}", f64::NAN.clamp(0.0, 1.0)); // NaN(値が NaN なら NaN のまま)
 
    // メソッド呼び出しは単項の - より先に結び付くので、括弧が無いと -(5.clamp(0, 100)) になる
    println!("{}", -5.clamp(0, 100)); // -5
 
    // 下限が上限より大きいと panic する
    // println!("{}", 5.clamp(10, 0)); // panic: min > max. min = 10, max = 0
}

整数には Ord::clamp、浮動小数点数には f64::clamp があり、下限が上限より大きいと panic する。
-5.clamp(0, 100) はメソッド呼び出しが単項の - より先に結び付いて -(5.clamp(0, 100)) と解釈され -5 になるので、負の数は括弧で囲む。

Java 実行確認済み

public class ClampNumber {
    public static void main(String[] args) {
        // Math.clamp は Java 21 で追加された
        System.out.println(Math.clamp(150, 0, 100));        // 100
        System.out.println(Math.clamp(-5, 0, 100));         // 0
        System.out.println(Math.clamp(0.25, 0.5, 1.0));     // 0.5
        System.out.println(Math.clamp(Double.NaN, 0.0, 1.0)); // NaN
 
        // long の値を int の範囲に収めると、戻り値をそのまま int で受け取れる
        long big = 5_000_000_000L;
        int n = Math.clamp(big, 0, Integer.MAX_VALUE);
        System.out.println(n); // 2147483647
 
        // 下限が上限より大きいと例外になる
        try {
            Math.clamp(5, 10, 0);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage()); // 10 > 0
        }
 
        // Java 20 以前は Math.min と Math.max を組み合わせる
        int v = 150;
        System.out.println(Math.max(0, Math.min(v, 100))); // 100
    }
}

Math.clamp は Java 21 で追加されたメソッドで、javac --release 20 でコンパイルすると cannot find symbol になる。
long の値と int の範囲を渡す版は int を返すので、キャストせずに int の変数で受け取れる。
下限が上限より大きいと IllegalArgumentException になる。

C# 実行確認済み

Console.WriteLine(Math.Clamp(150, 0, 100));        // 100
Console.WriteLine(Math.Clamp(-5, 0, 100));         // 0
Console.WriteLine(Math.Clamp(0.25, 0.5, 1.0));     // 0.5
Console.WriteLine(Math.Clamp(double.NaN, 0.0, 1.0)); // NaN
 
// decimal や long など、数値の型ごとにオーバーロードがある
Console.WriteLine(Math.Clamp(1234.5m, 0m, 999.99m)); // 999.99
 
// 下限が上限より大きいと ArgumentException になる
try
{
    Math.Clamp(5, 10, 0);
}
catch (ArgumentException e)
{
    Console.WriteLine(e.Message); // '10' cannot be greater than 0.
}

Math.Clamp は int・double・decimal など数値の型ごとにオーバーロードがあり、値を下限と上限の間に収めて返す。
下限が上限より大きいと ArgumentException になり、メッセージは '10' cannot be greater than 0. になる。

つまずき

min と max を組み合わせるときは、内側の min で上限に抑えてから外側の max で下限に持ち上げる(max(lo, min(value, hi)))か、内側の max で下限に持ち上げてから外側の min で上限に抑える(min(max(value, lo), hi))。
下限と上限を渡す相手を取り違えて Math.max(Math.min(value, 0), 100) と書くと、-5 を渡しても 42 を渡しても 100 が返る。
どちらの順で書いても普通の数値なら同じ結果になるが、テストで範囲の内側・下限の外側・上限の外側の 3 つの値を必ず確かめる。

下限と上限を逆に渡したときの振る舞いが言語で違う

標準の clamp は、下限が上限より大きいと処理を止める。
Rust は min > max. min = 10, max = 0 で panic し、Java は IllegalArgumentException(メッセージは 10 > 0)、C# は ArgumentException('10' cannot be greater than 0.)を投げる。
一方で min と max を組み合わせただけの式はエラーにならず、Python と Go で max(10, min(5, 0)) を実行すると 10 が返る。
設定ファイルや引数から下限と上限を受け取る場合は、自作の関数でも先に大小を検査して、取り違えに気付けるようにする。

NaN を渡したときの結果が言語で違う

値に NaN を渡すと、JavaScript の Math.min と Math.max、Go の組み込みの min と max、Rust の f64::clamp、Java の Math.clamp、C# の Math.Clamp はどれも NaN を返す。
Python の min と max は、後ろの引数の方が小さい(大きい)と判定されたときだけ入れ替える。
NaN との比較は常に False なので先に書いた引数が残り、組み合わせる順番で結果が変わって、max(0, min(nan, 100)) は 0、min(max(nan, 0), 100) は nan になる。
NaN が紛れ込みうる入力では、範囲に収める前に math.isnan や Number.isNaN で弾いておく。

標準の clamp があるかは言語とバージョンで違う

Rust・Java・C# には標準の clamp があり、Java の Math.clamp は Java 21 から使える。
Go には clamp という関数は無いが、Go 1.21 で組み込みの min と max が入ったので 1 行で書ける。
Python の組み込み関数と Node.js 22 の Math には clamp が無く、min と max を組み合わせて自分で書く。
古い実行環境に合わせる必要があるときは、どの言語でも min と max の組み合わせで同じ結果が得られる。

この記事は役立ちましたか?