負の数の剰余:-7 % 3 の結果が言語によって違う理由
-7 % 3 が Python では 2、JavaScript・Java・C・Go では -1 になる理由を切り捨て・床・ユークリッド除算から説明し、負の数でも 0 以上の余りを得る方法をまとめました。
配列を環状にたどってインデックスを減らす処理を (i - 1) % n と書くと、i が 0 のとき Python は n - 1 を返しますが、JavaScript は -1 を返して undefined を読んでしまいます。負の数が入ると、% の結果が言語ごとに違う決まりになっているからです。
商をどちらに丸めるか
整数 a を 0 でない整数 b で割るとき、整数の商 q と余り r は a = b × q + r を満たす必要があります。|r| < |b| を加えても答えは 1 つに決まりません。-7 ÷ 3 なら (q = -2, r = -1) も (q = -3, r = 2) も条件に合います。余りを決めるのは、実数の商 -2.33… をどちらの向きに整数にするかです。
| 方式 | 商 q | 余り r の符号 | -7 ÷ 3 | 7 ÷ -3 |
|---|---|---|---|---|
| 切り捨て(truncated) | 0 方向に丸める | 被除数、つまり左側に従う | q -2, r -1 | q -2, r 1 |
| 床(floored) | −∞ 方向に丸める | 除数、つまり右側に従う | q -3, r 2 | q -3, r -2 |
| ユークリッド(Euclidean) | 0 ≤ r < |b| になるように決める | 常に 0 以上 | q -3, r 2 | q -2, r 1 |
3 つとも a = b × q + r を満たしますが、余りが必ず 0 以上になるのはユークリッド方式だけです。2 つの数がどちらも正なら答えは同じです。
各言語の選択
| 言語 | -7 % 3 |
方式 |
|---|---|---|
| Python | 2 |
床、// と対 |
| JavaScript、Java、C、C++、Go | -1 |
切り捨て |
| JavaScript の BigInt | -1n(-7n % 3n) |
切り捨て |
整数の商を返す演算子、つまり Python の //、Java・C・C++・Go の整数の /、BigInt の / があれば、その商と % は上の恒等式を満たします。C と C++ では、商を型で表現できる場合に限ります。
JavaScript の通常の数値(Number)は事情が違います。-7 / 3 は整数の商ではなく浮動小数点数の -2.33… なので、(a / b) * b + a % b はこの整数の恒等式ではありません。% は仕様上、正確な数学上の商を 0 方向に丸めて別に計算します。浮動小数点数そのものは浮動小数点の記事で扱いました。
つまずきやすい場面
奇数判定を n % 2 == 1 と書くと、切り捨て方式の言語では -3 の余りが -1 になり偽になります。n % 2 != 0 ならどの言語でも正しく判定できます。
環状のインデックスのように 0 以上 n 未満の値が必要なら、余りが負のときだけ n を足します。
// 環状インデックス用。n は正の整数
function wrapIndex(i, n) {
const r = i % n;
return r < 0 ? r + n : r + 0; // + 0 で -0 を 0 にする
}
wrapIndex(-1, 5); // 4
-5 % 5 のように負の数が割り切れると、JavaScript は -0 を返します。インデックスとしては問題ありませんが、Object.is(-0, 0) は false なので、上の関数では + 0 で符号をそろえました。
標準の関数もあります。Math.floorMod(-7, 3) と Rust の (-7i32).rem_euclid(3) はどちらも 2 です。除数が負になると分かれます。floorMod は床除算なので Math.floorMod(4, -3) は -2、rem_euclid はユークリッド方式なので 4i32.rem_euclid(-3) は 1 です。