いままで何気なく剰余演算, 例えば 5.5 % 2.0 == 1.5
を書いてましたが, 実は結構危うかったので報告します. 本記事の内容は全部 wikipedia に載っているんですけどね.
剰余演算
2 つの正の数 $x$, $l$ (整数でも整数でなくてもよい) が与えられたとき
$$x = n l + r$$
を満たす非負の整数 $n$ と実数 $r \in [ 0, l )$ が一意に存在します. そこで $n$ を 商 (quotient), $r$ を 剰余 (remainder) と呼び, $r$ を $x % l$ と書きます. また, 等式
$$r = x % l$$
において $x$ を 被除数 (dividend), $l$ を 除数 (divisor) と呼びます. ここまでは人類の常識ですね.
問題は, $x$ または $l$ が負の数を取り得るように剰余演算を拡張する際に生じます. つまり, 2 つのゼロでない数 $x$, $l$ が与えられたとき, やはり $n \in \mathbb{Z}$ および適当な区間 $I$ に含まれる $r$ を用いる一意的な表示
$$x = n l + r$$
が存在しますが, 「適当な区間」$I$ つまり剰余 $r$ の取り得る範囲はどのように設定すべきでしょうか?
この問題に対する考えられる解答としては次の 4 パターンがあり得ます.
- 常に正: $r \in [ 0, | l | )$.
- 除数と同符号: $l > 0$ のとき $r \in [ 0, l )$, $l < 0$ のとき $r \in ( l, 0 ]$.
- 被除数と同符号: $x > 0$ のとき $r \in [ 0, | l | )$, $x < 0$ のとき $x \in (-|l|, 0 ]$.
- 0 に近い側: $r \in ( -|l|/2, |l|/2 ]$.
どの選択肢を採用するかは純粋に定義/仕様の問題です. そして困ったことに, 言語ごとに仕様が異なるのです.
整数型の場合
%
演算子として提供される整数剰余演算は, 多くの言語で 被除数と同符号 が採用されています. これには C/C++, Go, Java, JavaScript, Julia, OCaml, PHP, Rust などが含まれます. Fortran の mod
関数もそうです. しかし, そうでない言語も多くあります:
- 除数と同符号: Lua, Mathematica, Python, Ruby など.
- Lisp, Prolog, Haskell, Julia: これらの言語はいずれも
mod
が除数と同符号,rem
が被除数と同符号の演算になっています. なお Julia では%
はrem
の意味です.
そう, Python/Ruby と C/C++/Julia/Rust で仕様が違う のです. さらに言うと, これに伴って 除算の結果 (上の記法では整数 $n$) も両グループ間で一致しないということになります. なぜならば, (整数の場合) 商 $n = x/l$ とは $x = n l + r$ を満たす整数 $n$ ですから, 剰余 $r$ が違えば商 $n$ も変わります. 実際, Python では -3//2 == -2
, C/C++/Rust 等では -3/2 == -1
になります.
浮動小数点数の場合
浮動小数点数に対しても演算子 %
が定義されている言語は限られていますが Rust と Python, Julia ではちゃんと定義されていて, 整数型のときと同じ仕様になっています. なお C/C++ は fmod
/std::fmod
関数がこの機能を提供します.
補足
- Python の
math.fmod
関数は 被除数と同符号 な剰余演算を提供します. - Ruby の
Numeric#remainder
関数は 被除数と同符号 な剰余演算を提供します. (コメントに基づいて追記 2019-09-15) - Fortran の
mod
関数は 被除数と同符号,modulo
は 除数と同符号 な剰余演算を提供します. (コメントに基づいて追記 2019-09-15) - ゼロによる剰余 ($x%0$) はゼロ除算と同じく定義されません. Rust で検証したところ, コンパイラが気づいたらコンパイルが通らず, 通ってしまった場合, 整数ならパニックし, 浮動小数点数なら NaN になりました. Python では整数/浮動小数点数どちらもエラーになります.
参考文献
- 剰余演算 - Wikipedia
- 先行研究その1: C++における負の整数の割り算および余りについて
- 先行研究その2: 余り(剰余)の性質をプログラムに活かす
- 先行研究その3: 負数の剰余を計算してはならない