31st October 2024
The latest release of Pyro, my hobby programming language, adds support for two new remainder operators for integer division — rem
and mod
.
These new operators replace the old %
operator, which is now deprecated.
So what's the difference?
rem
operator returns the remainder left behind by truncated-division. It works like the %
operator in C.
mod
operator returns the remainder left behind by floored-division. It works like the %
operator in Python and corresponds to the modulo operation in mathematics.
That explanation might not be immediately enlightening but I'll provide some concrete examples below which should clear up the distinction.
Before we dig in any further, do note that if both operands are positive, then the two operators will produce the same result — e.g. 5 rem 3
and 5 mod 3
both produce the same result, namely 2
.
We only have the potential for different results if one or other of the operands is negative.
Let's start by defining some terminology.
When we calculate an integer division, we start with two values — the numerator
and the denominator
.
In standard mathematical notation, we want to perform the operation numerator/denominator
.
This operation has two outputs — the quotient
(the count of how many times the denominator
divides into the numerator
), and the remainder
(the bit that's left over).
Importantly, we always want this condition to be true:
assert quotient * denominator + remainder == numerator;
The complicating factor is that there are (at least) two different ways we can define the division operation. In programming languages, the important options are "truncated-division" and "floored-division".
In truncated-division, the quotient is rounded toward zero.
(This is Pyro's default kind of integer division — i.e. it's the kind of integer division performed by the //
operator.)
Here are some examples using the std::math
module's trunc_div()
function which takes the numerator
and denominator
as arguments and returns a (quotient, remainder)
tuple:
import std::math; assert math:trunc_div(5, 3) == (1, 2); # 1 * 3 + 2 == 5 assert math:trunc_div(5, -3) == (-1, 2); # -1 * -3 + 2 == 5 assert math:trunc_div(-5, 3) == (-1, -2); # -1 * 3 - 2 == -5 assert math:trunc_div(-5, -3) == (1, -2); # 1 * -3 - 2 == -5
The remainder
in this operation is the same as the output of the rem
operator in Pyro and the same as the output of the %
operator in C.1
In floored-division, the quotient is rounded toward negative infinity.
Here are some examples using the std::math
module's floor_div()
function which takes the numerator
and denominator
as arguments and returns a (quotient, remainder)
tuple:
import std::math; assert math:floor_div(5, 3) == (1, 2); # 1 * 3 + 2 == 5 assert math:floor_div(5, -3) == (-2, -1); # -2 * -3 - 1 == 5 assert math:floor_div(-5, 3) == (-2, 1); # -2 * 3 + 1 == -5 assert math:floor_div(-5, -3) == (1, -2); # 1 * -3 - 2 == -5
The remainder
in this operation is the same as the output of the mod
operator in Pyro and the same as the output of the %
operator in Python.2
This remainder
corresponds to the result of the modulo operation in mathematics.
(I don't know of any actual use-case for modulo with a negative denominator, but the result is at least internally consistent for this possibility.)
At least from C99 onwards. In older versions of C, the division and remainder operations for integer division were implementation-defined and could use either truncated-division or floored-division. From C99, the standard mandates the use of truncated-division.