数値を指定した範囲に収めるには
数値を下限以上・上限以下に収める(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 の組み合わせで同じ結果が得られる。