負の数の剰余:-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 です。

参照した仕様とドキュメント

ブログへ